Microsoft Store API — v2 Reference
/api.php) still works but is deprecated and will be retired on 1 December 2026. New integrations should use v2. ← Back to the API overview & pricing.
The XBXprices Microsoft Store API v2 is a REST API returning clean JSON for Xbox Series X|S & Xbox One prices, price history, live sales/discounts and achievement data across 70+ regions. The base URL is:
/api/v2
How regions work — read this first
This is the one thing almost everyone trips over on day one, so here it is up front. Xbox has no single global store — every country runs its own Microsoft Store, with its own prices, currency, release dates and even its own list of which games exist. The API mirrors that exactly.
The mental model: picture each region as one country’s shop. “Halo Infinite” in the US store (in USD) is a separate record from the same game in the UK store (in GBP). Whenever you call the API, you’re asking about one shop at a time.
Each request picks one region
Add region=us, region=gb, region=jp… to any call — the 2-letter country code. Leave it out and it defaults to US.
Your key unlocks a set of regions
Your plan includes a number of regions (Free 2 → Business all 47) and you choose which ones. Asking for one you didn’t enable returns 403 REGION_NOT_IN_PLAN — that’s a setting, not a broken key.
Pick them once, then query freely
Choose your regions in the quickstart below (one call to PUT /account/regions) or on the dashboard. Changeable once every 24 hours.
GB on your key once (step 2), then add ®ion=gb to every request. That’s the whole story. Prices come back as integers in the region’s minor units — pair each with its matching formatted… string for display, and make the region part of your cache key.- Omit
region=and it defaults toUS. An unknown code is400 INVALID_REGION; a code outside your plan is403 REGION_NOT_IN_PLANwith structurederror.details(your enabled set + the exact call to add one). - The enabled set lives on the key.
GET /account/regions(free) reports it with asourceofplan_default,customorall. PUT /account/regionsvalidates strictly (a typo is rejected, not silently dropped), enforces your plan cap, and allows one change per 24h — re-submitting the identical list succeeds without spending that change.- Same game, different region = different currency, value, sale timing and availability. Cache on
(ppid, region), neverppidalone.
Quickstart — from key to first JSON in 5 minutes
Just received your API key by email? Follow these steps in order — especially step 2 (regions), the thing most new integrations trip over. No key yet? Open the keyless /demo in your browser to see the data shape, and grab a key via the plans page.
/api/v2/docs#key=… do this automatically.Step 1 — check that your key works
GET /account is free (it never consumes a request) and shows everything about your key: plan, monthly quota, expiry and — crucially — which store regions it can query.
curl -H "X-API-Key: $PP_KEY" "/api/v2/account"
{
"success": true,
"data": {
"plan": "indie",
"monthly_limit": 20000,
"remaining_this_month": 20000,
"regions": {
"allowed_regions": ["DE", "FR", "GB", "JP", "US"], // <-- what you may query
"region_limit": 5,
"source": "plan_default",
"can_change_now": true
}
}
}
Getting 401 instead? Make sure the header is exactly X-API-Key and the key was copied without spaces or line breaks.
Step 2 — set the regions you actually need (the #1 gotcha)
Every data request is scoped to one store region via the region= parameter — and if you omit it, it defaults to US. New keys start with your plan’s default region set (for Indie: US, GB, DE, FR, JP). Requesting a region that is not enabled for your key returns 403 REGION_NOT_IN_PLAN — that is configuration, not a broken key. Pick your own set (up to your plan’s limit) with one call:
# Example: an Indie key (5 regions) that needs Ukraine + Turkey
curl -X PUT -H "X-API-Key: $PP_KEY" -H "Content-Type: application/json" \
-d '{"regions":["US","GB","UA","TR","DE"]}' \
"/api/v2/account/regions"
The change is immediate. Valid codes come from GET /regions; regions can be changed once every 24 hours (re-submitting the same list is a free no-op), so pick the set you need before you start. The same control exists on your developer dashboard.
Step 3 — your first data request
# Search a game by name in the Turkish store
curl -H "X-API-Key: $PP_KEY" \
"/api/v2/games/search?q=Resident+Evil+4®ion=tr"
# Everything on sale in the Ukrainian store, biggest discounts first
curl -H "X-API-Key: $PP_KEY" \
"/api/v2/deals?region=ua&sort=discount&order=desc&limit=20"
Remember: always pass region= explicitly — it makes your intent unambiguous and your caching keys clean.
Step 4 — the same call in your language
// JavaScript (Node 18+ / browser)
const res = await fetch("/api/v2/deals?region=ua&limit=20", {
headers: { "X-API-Key": "$PP_KEY" }
});
const json = await res.json();
if (!json.success) throw new Error(json.error.code + ": " + json.error.message);
console.log(json.data.length, "deals; first:", json.data[0].ProductName);
# Python 3 (requests)
import requests
r = requests.get(
"/api/v2/deals",
params={"region": "ua", "limit": 20},
headers={"X-API-Key": "$PP_KEY"},
timeout=15,
)
j = r.json()
if not j["success"]:
raise RuntimeError(f"{j['error']['code']}: {j['error']['message']}")
for game in j["data"]:
print(game["ProductName"], game["formattedSalePrice"])
<?php // PHP 8 (cURL)
$ch = curl_init("/api/v2/deals?region=ua&limit=20");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: $PP_KEY'],
CURLOPT_TIMEOUT => 15,
]);
$j = json_decode(curl_exec($ch), true);
if (!($j['success'] ?? false)) {
throw new RuntimeException($j['error']['code'] . ': ' . $j['error']['message']);
}
foreach ($j['data'] as $game) {
echo $game['ProductName'], ' — ', $game['formattedSalePrice'], "\n";
}
Step 5 — watch your quota, handle errors
Every response carries X-RateLimit-Remaining (and meta.rate_limit); GET /status and GET /account/usage are free ways to check usage. Always branch on error.code — the most common first-day issues:
| You see | What it means & the fix |
|---|---|
| 401 MISSING_KEY | The X-API-Key header didn’t arrive — check the header name and that your HTTP client actually sends it. |
| 401 INVALID_KEY | Key not recognised — usually a copy/paste issue (whitespace, truncation). Copy it again from the key email. |
| 403 REGION_NOT_IN_PLAN | The region isn’t enabled for your key. Fix it yourself in 10 seconds — see step 2. |
| 400 INVALID_REGION | Unknown region code — use a 2-letter code from GET /regions. |
| 429 RATE_LIMIT_EXCEEDED | Monthly quota or the 120 req/min/IP burst limit — honour Retry-After and cache responses. |
Full error reference is below. Stuck? Email [email protected] with the request URL and the JSON error you got — we answer fast.
Authentication
Every request (except /demo) needs an API key, sent in the X-API-Key request header. A ?key= query parameter is also accepted for backward compatibility with v1.
curl -H "X-API-Key: $PP_KEY" "/api/v2/games?region=us&limit=5"
Keys are issued by email — tell us about your project at [email protected]. Keep your key secret; treat it like a password.
Fully self-service by key — no website account needed. Everything about your key is manageable through the API itself: GET /account (plan, quota, expiry, regions), GET /account/usage (daily request history + per-endpoint breakdown) and PUT /account/regions (choose which store regions your key serves). All three are free — they never consume a request. Prefer a UI? The same controls live on your developer dashboard.
# Choose your regions — by key alone, no account needed
curl -X PUT -H "X-API-Key: $PP_KEY" -H "Content-Type: application/json" \
-d '{"regions":["US","GB","DE"]}' "/api/v2/account/regions"
Plans & limits
Quotas are counted per calendar month and reset on the 1st (UTC). Region access and price history depend on your plan.
| Plan | Price | Requests / month | Regions | Price history |
|---|---|---|---|---|
| Free | $0 | 1,000 | 2 | — |
| Indie | $19/mo | 20,000 | 5 | 3 months |
| Pro | $49/mo | 100,000 | 20 | 3 months |
| Business | $149/mo | 500,000 | All 47 | 12 months |
Price history (/games/{ppid}/price-history) is a paid feature: on the Free plan it returns 403 PLAN_UPGRADE_REQUIRED. The history window is plan-tied — Indie and Pro see the last 3 months, Business the last 12 months; need a deeper or full-archive export? Talk to us. If you request a region outside your plan you get 403 REGION_NOT_IN_PLAN — configure your allowed regions or upgrade.
Sale archive follows the same window: /sales/{id} serves any active or recently ended sale, but a sale that ended beyond your plan’s history window returns 403 SALE_ARCHIVE_WINDOW.
Terms of use. Use the API in your apps, dashboards, analytics and alerts. Don’t build a directly competing price-tracking service, resell or redistribute the raw data, bulk-archive beyond your plan’s limits, or scrape the website to get around them. Where your plan requires attribution, show “Powered by XBXprices” with a link. Violating keys may be throttled or revoked.
Rate limiting
Each response carries your live usage in headers:
| Header | Meaning |
|---|---|
| X-RateLimit-Limit | Your monthly request quota. |
| X-RateLimit-Used | Requests used this month. |
| X-RateLimit-Remaining | Requests left this month. |
| X-RateLimit-Reset | Unix epoch (UTC) when the quota rolls over — also in the body as meta.rate_limit.reset_at (ISO 8601). |
| X-Response-Time-Ms | Server-side processing time in milliseconds (also meta.response_ms). |
| Retry-After | On 429: seconds until the limit frees up. |
When the monthly quota is exhausted you get 429 RATE_LIMITED (with Retry-After = seconds to the 1st of next month). There is also a short-term burst limit of 120 requests per minute per IP to protect the service; exceeding it returns 429 with Retry-After to the next minute. Please cache responses in your own database rather than re-requesting the same data.
Response format
Every response is a JSON object with the same envelope:
{
"success": true,
"data": { ... } | [ ... ] | null,
"meta": {
"region": "US",
"pagination": { "page": 1, "limit": 20, "total": 134 },
"rate_limit": { "limit": 50000, "used": 12, "remaining": 49988, "reset_at": "2026-07-01T00:00:00Z" },
"response_ms": 14
},
"error": null
}
On failure, success is false, data is null, and error is { "code": "STRING_CODE", "message": "…", "status": 4xx }. Errors your integration is expected to handle (e.g. REGION_NOT_IN_PLAN) additionally carry a machine-readable error.details object — for the region gate it includes requested_region, enabled_regions and the exact self-service call to enable it, so you can branch on fields instead of parsing the message. All responses also send X-API-Version: 2 and permissive CORS headers (Access-Control-Allow-Origin: *, methods GET, PUT, POST, OPTIONS).
Errors
| HTTP | error.code | When |
|---|---|---|
| 400 | INVALID_PARAM / INVALID_REGION / INVALID_SORT | A query parameter is missing or invalid. |
| 401 | MISSING_KEY / INVALID_KEY / KEY_EXPIRED | No key supplied, the key is not recognised, or a time-limited key has expired. |
| 403 | PLAN_UPGRADE_REQUIRED / REGION_NOT_IN_PLAN | Your plan does not include this feature or region. REGION_NOT_IN_PLAN includes error.details with your enabled regions and the PUT /account/regions call that enables the requested one. |
| 404 | GAME_NOT_FOUND / SALE_NOT_FOUND / ROUTE_NOT_FOUND | The resource or route does not exist. |
| 405 | METHOD_NOT_ALLOWED | Data endpoints are GET-only; PUT/POST exist only under /account. |
| 429 | RATE_LIMIT_EXCEEDED / REGION_CHANGE_COOLDOWN | Monthly quota or per-minute burst limit exceeded; or a region change inside the 24h cooldown. |
| 401 | KEY_EXPIRED | The key passed its expiry date (time-limited keys only; most keys never expire). Request a new one. |
| 403 | FREE_KEY_LIMIT | Several Free keys are in use from one network — Free is one key per developer. Upgrade, or contact us if it’s a shared office IP. |
| 403 | KEY_SUSPENDED | The key was administratively suspended (abuse). Rejected on every endpoint — contact us. |
| 403 | SALE_ARCHIVE_WINDOW | /sales/{id} for a sale that ended beyond your plan’s history window (3 months Indie/Pro, 12 Business). |
| 500 | INTERNAL_ERROR | Something went wrong on our side. |
Common query parameters
| Parameter | Applies to | Description |
|---|---|---|
| region | most endpoints | 2-letter region code (e.g. us, gb, de, jp). Defaults to your account region. See /regions. |
| platform | /games, /deals | Filter by series, one, vr, psvr (original PS VR), psvr2 (PS VR2) or move. |
| min_discount | /games, /deals | Only games discounted at least this percent (e.g. 50). |
| sort | /games, /deals | Field to sort by (default popularity). sale_price ranks by the Xbox Game Pass price; valid keys differ per endpoint (see each below). |
| order | list endpoints | asc or desc. |
| limit | list endpoints | Records per page, clamped to 1–your plan max (25 Free → 200 Business). Out-of-range is clamped; non-numeric is a 400. |
| page | list endpoints | Page number (1-based). |
| with_total | list endpoints | On by default. Pass 0 to skip the COUNT(*) — faster when you only need the next page. |
| group_mode | /games | Return unified game “cards” (group editions of the same game). |
| include_related | /games/{ppid} | 1 to include related editions/DLC. |
| q | /games/search | Search text (game name). Resolved to ≤200 closest matches. |
| fields | game endpoints | Comma-separated whitelist — only narrows the object (unknown names are ignored). E.g. fields=PPID,ProductName,SalePrice,formattedSalePrice. |
The game object
The game/product endpoints (/games, /games/{ppid}, /games/search, /deals, /demo…) all return the same object. Notable fields, grouped:
| Group | Fields |
|---|---|
| Identity | PPID (int), XboxAccountID, ProductName, region, Img, MicrosoftStoreURL, XBXpricesURL |
| Platform | IsXboxOne, IsXboxSeriesXS, IsVR, VRType (0 none / 1 PS VR / 2 PS VR2), IsMove, IsDLC, IsDemoOrSoundtrack, PS4Size, PS5Size |
| Pricing | BasePrice, SalePrice, GamePassPrice, DiscPerc, LowestEverPrice, LowestEverGamePassPrice, GamePassNeeded, GamePassPremium, GamePassUltimate — plus a formatted… string for every price |
| Achievements | Bronze, Silver, Gold, Platinum, Difficulty, HoursLow, HoursHigh, TrophyListURL, OpenCriticID |
| Other | Rating, RatingDesc, NumLibrary, NumWishlist, genre flags |
Prices are integers in the region’s minor units (scaled by decimalPlaces from /regions) — for display, use the matching formatted… string. LowestEverPrice/LowestEverGamePassPrice are null when no record exists. HoursLow/HoursHigh are in hours (-1 = unknown). Internal columns are never returned; use fields= to narrow the object further.
Endpoints
Quick reference — tap a path to jump to its full details.
| Endpoint | Returns |
|---|---|
| GET /games | Filterable, paginated list of games with prices, discounts and achievement data. |
| GET /games/search | Search games by name. |
| GET /games/batch | Fetch many games at once by id — refresh a whole wishlist, cart or price-watch list in a single call instead of one request per game. Costs one request and returns up to your plan’s per-list record cap, exactly like one page of /games. |
| GET /games/{ppid} | A single game by its XBXprices ID (the PPID integer). |
| GET /games/by-psnid/{psnid} | A single game by its Xbox bigId (e.g. PPSA07412). |
| GET /games/{ppid}/price-history paid | Price history for a game. paid plans only — Free returns 403 PLAN_UPGRADE_REQUIRED. Depth depends on your plan: Indie and Pro see the last 3 months, Business the last 12 months. |
| GET /deals | Recently-discounted games. Accepts the full /games filter set (genre, price range, achievement filters, publisher, psplus, q…) layered on a tunable “dropped recently” window. |
| GET /sales | Active sale campaigns (e.g. “Big in Japan”), not individual games. Returns all active sales (no pagination). |
| GET /sales/{id} | All games inside one sale campaign. Sales that ended beyond your plan’s history window return 403 SALE_ARCHIVE_WINDOW. |
| GET /regions | Every supported Microsoft Store region. Cached 24h. |
| GET /genres | Every genre token accepted by ?genre= on /games and /deals, with human labels. Discover valid values instead of hard-coding them. Free (no quota); cached 24h. |
| GET /platforms | Every platform token accepted by ?platform= on /games and /deals, with human labels. Free (no quota); cached 24h. |
| GET /status | Your key’s plan, quota and usage. Cheap way to check how many requests you have left. |
| GET /account | Your key’s full self-service view — everything the developer dashboard shows, by API key alone (no website account needed). Free: does not consume a request. |
| GET /account/usage | Your key’s request history — the same counters that feed the dashboard chart. Free: does not consume a request. |
| PUT /account/regions | Choose the store regions your key serves — entirely via the API, no website account needed. Up to your plan’s region limit, at most once every 24 hours (shared with the dashboard’s cooldown). POST is accepted as an alias; GET returns the current configuration without changing it. Re-submitting the identical list is a no-op and does not burn the daily change. |
| GET /demo | Keyless sample — open it straight in your browser. IP rate-limited. |
| GET / | API index — name, version and the list of available endpoints. |
Tap any path above for its full parameters and response — or switch to the full reference to read every endpoint in detail.
Filterable, paginated list of games with prices, discounts and achievement data.
| Parameter | Description |
|---|---|
| region | 2-letter region code (default: your account region). |
| platform | series, one, vr (either VR generation), psvr (original PS VR only), psvr2 (PS VR2 only) or move. |
| genre | Genre token (e.g. rpg, action, fps, horror, racing…). |
| on_sale | 1 for currently-discounted games only. |
| min_discount | Only games discounted ≥ this percent. |
| price_min / price_max | Price range (region minor units). |
| released_after / released_before | Release-date range (YYYY-MM-DD), e.g. only titles released this year. |
| difficulty / difficulty_max | Achievement difficulty range (1–10). |
| hours_max | Maximum hours-to-completion. |
| has_platinum | 1 = only games with full Gamerscore. |
| exclude_dlc / exclude_demo | DLC and demos are hidden by default. Pass exclude_dlc=0 / exclude_demo=0 to include them. |
| publisher / publisher_like | Filter by publisher (exact / partial match). |
| psplus | Xbox Game Pass tier filter (e.g. extra, premium). |
| sort | popularity (default), price, sale_price (ranks by the Xbox Game Pass price), discount, name, release or lowest_ever. |
| order | asc / desc. |
| limit / page | Pagination (page is 1-based). |
| with_total | 0 to skip the total count (on by default). |
| group_mode | 1 to collapse editions/DLC into one card per game. |
| fields | Comma-separated list to return only those fields. |
Returns: An array of game objects. With group_mode=1 it instead returns an array of cards — a representative game object plus edition_group_id, edition_count and an editions[] array (each a slim object with an is_representative flag). meta.pagination { page, limit, total }.
Search games by name.
| Parameter | Description |
|---|---|
| q | Search text (required). Resolved to at most the 200 closest name matches — built for “find this game”, not bulk enumeration. |
| region | Region code. |
| include_dlc | 1 to also match DLC / add-ons (e.g. Resident Evil 4 - Separate Ways). Off by default so a game name resolves to the game. |
| fields | Field projection. |
Returns: An array of game objects matching the query. Special editions are matched directly (q=Resident Evil 4 Gold Edition); to list every edition/DLC of a game you already found, use /games/{ppid}?include_related=1. meta { query, count, include_dlc }.
Fetch many games at once by id — refresh a whole wishlist, cart or price-watch list in a single call instead of one request per game. Costs one request and returns up to your plan’s per-list record cap, exactly like one page of /games.
| Parameter | Description |
|---|---|
| ppids | Comma-separated XBXprices ids, e.g. ppids=7704,8123,9001. |
| psnids | Comma-separated Xbox bigIds, e.g. psnids=PPSA07412,EP9000-CUSA00000_00. Combine with ppids freely. |
| region | Region code. |
| fields | Field projection. |
Returns: An array of game objects in the order you asked for them (ppids first, then psnids). The total number of ids must not exceed your plan’s per-request record cap (400 INVALID_PARAM otherwise — split into more calls). meta { requested, returned, missing[] (ids not found in this region), max_batch }.
A single game by its XBXprices ID (the PPID integer).
| Parameter | Description |
|---|---|
| region | Region code. |
| include_related | 1 to also include related editions/DLC. |
| fields | Field projection. |
Returns: One game object. With include_related=1 it also carries a related[] array of slim objects.
A single game by its Xbox bigId (e.g. PPSA07412).
| Parameter | Description |
|---|---|
| region | Region code. |
| include_related | 1 to also include related editions/DLC. |
| fields | Field projection. |
Returns: One game object (same shape as /games/{ppid}).
Price history for a game. paid plans only — Free returns 403 PLAN_UPGRADE_REQUIRED. Depth depends on your plan: Indie and Pro see the last 3 months, Business the last 12 months.
| Parameter | Description |
|---|---|
| region | Region code. |
Returns: An object { ppid, region, history[] }. Each history point: timestamp, BasePrice, SalePrice, GamePassPrice (+ formatted… strings), isLowestEverPrice and isLowestEverGamePassPrice (booleans). meta.count = number of history points; meta.history_window_months = your plan’s window.
Recently-discounted games. Accepts the full /games filter set (genre, price range, achievement filters, publisher, psplus, q…) layered on a tunable “dropped recently” window.
| Parameter | Description |
|---|---|
| region | Region code. |
| hours | Width of the “recently discounted” window in hours (default 48, range 1–168). Use hours=24 for “dropped today”. |
| platform | series, one, vr, psvr, psvr2 or move (any-of). |
| genre | Genre token(s), e.g. genre=rpg,horror — see /genres. |
| min_discount | Minimum discount percent. |
| price_min / price_max | Price range (region minor units). |
| has_platinum / difficulty_max / hours_max / publisher / psplus | Same achievement / publisher / Xbox Game Pass filters as /games. |
| sort | recent (default), discount, price, sale_price, name or lowest_ever. |
| order | asc / desc. |
| limit / page | Pagination. |
| fields | Field projection. |
Returns: An array of game objects discounted within the window (each has a non-zero DiscPerc). Pass with_total=0 to skip the count. meta.pagination + meta.window_hours.
Active sale campaigns (e.g. “Big in Japan”), not individual games. Returns all active sales (no pagination).
| Parameter | Description |
|---|---|
| region | Region code. |
Returns: An array of sale objects: id, name, startsAt, endsAt, numGames, imageURL, url (link to that sale’s endpoint). meta.count.
All games inside one sale campaign. Sales that ended beyond your plan’s history window return 403 SALE_ARCHIVE_WINDOW.
| Parameter | Description |
|---|---|
| region | Region code. |
Returns: An object: id, name, region, startsAt, endsAt, imageURL, plus game_discounts[] and dlc_discounts[] — each a compact game object (PPID, XboxAccountID, ProductName, Img, Platinum, prices + formatted…). meta { game_count, dlc_count }.
Every supported Microsoft Store region. Cached 24h.
Returns: An array of region objects: region (code), language, currencySymbol, decimalPlaces, and inYourPlan (whether your plan can query it). meta.count.
Every genre token accepted by ?genre= on /games and /deals, with human labels. Discover valid values instead of hard-coding them. Free (no quota); cached 24h.
Returns: An array of { token, label } objects, e.g. { "token": "rpg", "label": "RPG" }. meta { count, usage }.
Every platform token accepted by ?platform= on /games and /deals, with human labels. Free (no quota); cached 24h.
Returns: An array of { token, label } objects, e.g. { "token": "series", "label": "Xbox Series X|S" }. meta { count, usage }.
Your key’s plan, quota and usage. Cheap way to check how many requests you have left.
Returns: An object: plan, plan_name, monthly_limit, used_this_month, remaining_this_month, quota_period (“month”), reset_at_utc, allowed_regions (“all” or an array), commercial_use, attribution_required.
Your key’s full self-service view — everything the developer dashboard shows, by API key alone (no website account needed). Free: does not consume a request.
Returns: An object: key_prefix, plan, plan_name, monthly_limit, used_this_month, remaining_this_month, used_today, reset_at_utc, expires_at_utc (null = never), max_records_per_list_request, price_history_months, commercial_use, attribution_required, and a regions object (allowed_regions, region_limit, source, can_change_now, next_change_allowed_at_utc).
Your key’s request history — the same counters that feed the dashboard chart. Free: does not consume a request.
| Parameter | Description |
|---|---|
| days | Window in days, 1–90 (default 30; counters are retained 90 days). |
Returns: An object: window_days, total_requests, busiest_day {date, requests}, daily[] (zero-filled {date, requests} series, oldest first — chart-ready) and endpoints[] (top routes by volume, e.g. v2/games/{id}).
Choose the store regions your key serves — entirely via the API, no website account needed. Up to your plan’s region limit, at most once every 24 hours (shared with the dashboard’s cooldown). POST is accepted as an alias; GET returns the current configuration without changing it. Re-submitting the identical list is a no-op and does not burn the daily change.
| Parameter | Description |
|---|---|
| regions | JSON body {"regions":["US","GB"]} (or a "US,GB" string); a ?regions=US,GB query parameter also works. Codes must come from /regions; unknown codes return 400 INVALID_PARAM. |
Returns: The new region configuration (same regions object as /account) plus changed (boolean). During the cooldown it returns 429 REGION_CHANGE_COOLDOWN with a Retry-After header.
Keyless sample — open it straight in your browser. IP rate-limited.
Returns: A fixed sample of 10 game objects, same shape as /games (no key, no pagination). The same titles every call — it shows the data shape, not a live feed. meta { note, count }.
API index — name, version and the list of available endpoints.
Returns: An object: name, version, status, base_url, authentication, documentation, endpoints[].
Examples
# Trending deals in the UK, biggest discounts first
curl -H "X-API-Key: $PP_KEY" \
"/api/v2/deals?region=gb&sort=discount&order=desc&limit=20"
# One game by Xbox bigId, with related editions
curl -H "X-API-Key: $PP_KEY" \
"/api/v2/games/by-psnid/PPSA07412?region=us&include_related=1"
# Price history (paid plans; 3 months back, Business 12)
curl -H "X-API-Key: $PP_KEY" \
"/api/v2/games/7704/price-history?region=us"
# No key needed — try this one in your browser:
curl "/api/v2/demo"
The keyless /demo endpoint returns a real sample response so you can see the shape of the data before you get a key.
Engineering notes & best practices
Everything below is exact, production-grade behaviour. The quickstart is enough to read prices; this is the precise contract for integrations that must stay correct under load.
Two independent limits: monthly quota & burst
- Monthly quota — your plan’s request budget, counted per calendar month and reset at 00:00 UTC on the 1st. v1 (
/api.php) and v2 share one counter. Exhausting it →429 RATE_LIMIT_EXCEEDED,Retry-After= seconds to the 1st. One successful call = one unit, however many records it returns. - Burst limit — a separate 120 requests/minute/IP across every endpoint (including unauthenticated and 404 traffic). Over it →
429,Retry-After= seconds to the next minute. Independent of your monthly budget. - Free meta endpoints —
/status,/account,/account/usage,GET /regionsandGET/PUT /account/regionsnever spend monthly quota (they still count toward the burst limit). - Both surface live in headers:
X-RateLimit-Limit/Used/RemainingandX-RateLimit-Reset(Unix epoch). Trust the header over a counter of your own.
The real unit is records, not calls
List endpoints return up to your plan’s page size (25 Free → 200 Business), so your true data ceiling is monthly_limit × max_page. Pull wide pages with precise filters instead of many tiny calls — same quota, far fewer round-trips.
Pagination
pageis 1-based;limitis clamped to[1, max_page](default 20 or the plan cap, whichever is smaller). Out-of-range numbers clamp silently; a non-numeric value is400 INVALID_PARAM.- Deep paging is capped: an effective offset
(page-1) × limitabove 100,000 returns400. Narrow with filters (region, platform, price, discount) rather than paging that far. with_total=0skips theCOUNT(*)— noticeably faster on large result sets when you only need the next page. With it on,meta.paginationcarriestotal,total_pagesandhas_more.- Ordering is stable: every sort breaks ties by
PPIDdescending, so a row never jumps between pages.
Filtering — exact semantics
- DLC and demos are hidden by default. Pass
exclude_dlc=0and/orexclude_demo=0to include them. platformandgenreare any-of (OR):platform=series,onematches titles on either.sort=sale_priceranks by the Xbox Game Pass price;priceby base price. Default sortpopularity, default directiondesc(ascforname).price_min/price_maxtake a human price in the region’s currency (e.g.19.99), matched against the base price./games/searchresolves your text to at most the 200 closest name matches before filtering — for “find this game”, not enumeration.
Types & null handling
- Prices are integers in the region’s minor units (scaled by
decimalPlacesfrom/regions). Don’t divide by 100 blindly — JP has 0 decimal places. For display, use the matchingformatted…string. LowestEverPrice/LowestEverGamePassPricearenullwhen unknown — no sentinel integer to special-case.HoursLow/HoursHighare floats in hours (e.g.7.5);-1= unknown.Difficultyis 1–10,-1unknown.- Flag/count fields (
IsXboxSeriesXS,Platinum,Genre…) are integers, not JSON booleans.
Field projection
fields= can only narrow an object — it drops everything not listed, and unknown names are ignored (never an error). It shrinks payloads and your parsing surface; it cannot add or rename fields. Use the exact public names from the game object.
Caching
- List responses (
/games,/deals) sendCache-Control: private, max-age=300— reuse for 5 minutes, but private (per-key): shared/CDN caches must not store them./regionsis cached 24h; the keyless/demoispublic. - Persist results in your own database and serve from there; call the API to refresh, not on every page view. Prices move over hours, not seconds.
- Responses
VaryonX-API-Key— never key a shared cache on the URL alone.
Robust error handling
- Branch on
error.code(stable), never onerror.message(may be reworded). 403 REGION_NOT_IN_PLANcarries structurederror.details:requested_region,enabled_regions[],planand the exacthow_to_enablecall — branch on those, don’t parse prose.- Honour
Retry-Afteron every429and back off. - GET is safe to retry (idempotent).
PUT /account/regionswith an unchanged list is a no-op that does not spend your once-per-24h change — safe to call defensively.
Terms of use
| Permitted | Not permitted |
|---|---|
| Building apps, sites and tools on top of the data; caching results in your own database; commercial use on a paid plan. | Re-selling or redistributing the raw dataset as-is; scraping around the rate limits; bulk-mirroring the entire catalogue. |
On the Free and Indie plans, please show a “Powered by XBXprices” attribution wherever the data appears. Pro and Business plans have no attribution requirement. We may throttle or revoke keys that abuse the service. Commercial use requires a paid plan.
Questions, a higher limit, or a custom plan? Email [email protected].

