XBXprices
Deals All Regions Blog

Microsoft Store API — v2 Reference

This is the current, recommended API (v2). The legacy v1 endpoint (/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
Reading level:
New here? Beginner shows just the essentials. Flip to Full reference for every endpoint, field & error code.
You’re in Beginner view — just what you need to make your first calls. Start with how regions work, then the 5-minute quickstart. Want every endpoint, header, field and error code? Switch to the full reference →

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.

1

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.

2

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.

3

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.

Worked example — “I just want UK prices.” Enable GB on your key once (step 2), then add &region=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.
Under the hood (advanced).
  • Omit region= and it defaults to US. An unknown code is 400 INVALID_REGION; a code outside your plan is 403 REGION_NOT_IN_PLAN with structured error.details (your enabled set + the exact call to add one).
  • The enabled set lives on the key. GET /account/regions (free) reports it with a source of plan_default, custom or all.
  • PUT /account/regions validates 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), never ppid alone.

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.

The key stays in your browser only (it is never sent to this page’s server). Links from our emails of the form /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_PLANthat 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&region=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 seeWhat it means & the fix
401 MISSING_KEYThe X-API-Key header didn’t arrive — check the header name and that your HTTP client actually sends it.
401 INVALID_KEYKey not recognised — usually a copy/paste issue (whitespace, truncation). Copy it again from the key email.
403 REGION_NOT_IN_PLANThe region isn’t enabled for your key. Fix it yourself in 10 seconds — see step 2.
400 INVALID_REGIONUnknown region code — use a 2-letter code from GET /regions.
429 RATE_LIMIT_EXCEEDEDMonthly 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.

PlanPriceRequests / monthRegionsPrice 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:

HeaderMeaning
X-RateLimit-LimitYour monthly request quota.
X-RateLimit-UsedRequests used this month.
X-RateLimit-RemainingRequests left this month.
X-RateLimit-ResetUnix epoch (UTC) when the quota rolls over — also in the body as meta.rate_limit.reset_at (ISO 8601).
X-Response-Time-MsServer-side processing time in milliseconds (also meta.response_ms).
Retry-AfterOn 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

HTTPerror.codeWhen
400INVALID_PARAM / INVALID_REGION / INVALID_SORTA query parameter is missing or invalid.
401MISSING_KEY / INVALID_KEY / KEY_EXPIREDNo key supplied, the key is not recognised, or a time-limited key has expired.
403PLAN_UPGRADE_REQUIRED / REGION_NOT_IN_PLANYour 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.
404GAME_NOT_FOUND / SALE_NOT_FOUND / ROUTE_NOT_FOUNDThe resource or route does not exist.
405METHOD_NOT_ALLOWEDData endpoints are GET-only; PUT/POST exist only under /account.
429RATE_LIMIT_EXCEEDED / REGION_CHANGE_COOLDOWNMonthly quota or per-minute burst limit exceeded; or a region change inside the 24h cooldown.
401KEY_EXPIREDThe key passed its expiry date (time-limited keys only; most keys never expire). Request a new one.
403FREE_KEY_LIMITSeveral 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.
403KEY_SUSPENDEDThe key was administratively suspended (abuse). Rejected on every endpoint — contact us.
403SALE_ARCHIVE_WINDOW/sales/{id} for a sale that ended beyond your plan’s history window (3 months Indie/Pro, 12 Business).
500INTERNAL_ERRORSomething went wrong on our side.

Common query parameters

ParameterApplies toDescription
regionmost endpoints2-letter region code (e.g. us, gb, de, jp). Defaults to your account region. See /regions.
platform/games, /dealsFilter by series, one, vr, psvr (original PS VR), psvr2 (PS VR2) or move.
min_discount/games, /dealsOnly games discounted at least this percent (e.g. 50).
sort/games, /dealsField to sort by (default popularity). sale_price ranks by the Xbox Game Pass price; valid keys differ per endpoint (see each below).
orderlist endpointsasc or desc.
limitlist endpointsRecords per page, clamped to 1–your plan max (25 Free → 200 Business). Out-of-range is clamped; non-numeric is a 400.
pagelist endpointsPage number (1-based).
with_totallist endpointsOn by default. Pass 0 to skip the COUNT(*) — faster when you only need the next page.
group_mode/gamesReturn unified game “cards” (group editions of the same game).
include_related/games/{ppid}1 to include related editions/DLC.
q/games/searchSearch text (game name). Resolved to ≤200 closest matches.
fieldsgame endpointsComma-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:

GroupFields
IdentityPPID (int), XboxAccountID, ProductName, region, Img, MicrosoftStoreURL, XBXpricesURL
PlatformIsXboxOne, IsXboxSeriesXS, IsVR, VRType (0 none / 1 PS VR / 2 PS VR2), IsMove, IsDLC, IsDemoOrSoundtrack, PS4Size, PS5Size
PricingBasePrice, SalePrice, GamePassPrice, DiscPerc, LowestEverPrice, LowestEverGamePassPrice, GamePassNeeded, GamePassPremium, GamePassUltimate — plus a formatted… string for every price
AchievementsBronze, Silver, Gold, Platinum, Difficulty, HoursLow, HoursHigh, TrophyListURL, OpenCriticID
OtherRating, 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.

EndpointReturns
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 Price history for a game. — 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.

GET /api/v2/games

Filterable, paginated list of games with prices, discounts and achievement data.

ParameterDescription
region2-letter region code (default: your account region).
platformseries, one, vr (either VR generation), psvr (original PS VR only), psvr2 (PS VR2 only) or move.
genreGenre token (e.g. rpg, action, fps, horror, racing…).
on_sale1 for currently-discounted games only.
min_discountOnly games discounted ≥ this percent.
price_min / price_maxPrice range (region minor units).
released_after / released_beforeRelease-date range (YYYY-MM-DD), e.g. only titles released this year.
difficulty / difficulty_maxAchievement difficulty range (1–10).
hours_maxMaximum hours-to-completion.
has_platinum1 = only games with full Gamerscore.
exclude_dlc / exclude_demoDLC and demos are hidden by default. Pass exclude_dlc=0 / exclude_demo=0 to include them.
publisher / publisher_likeFilter by publisher (exact / partial match).
psplusXbox Game Pass tier filter (e.g. extra, premium).
sortpopularity (default), price, sale_price (ranks by the Xbox Game Pass price), discount, name, release or lowest_ever.
orderasc / desc.
limit / pagePagination (page is 1-based).
with_total0 to skip the total count (on by default).
group_mode1 to collapse editions/DLC into one card per game.
fieldsComma-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 }.

GET /api/v2/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.

ParameterDescription
ppidsComma-separated XBXprices ids, e.g. ppids=7704,8123,9001.
psnidsComma-separated Xbox bigIds, e.g. psnids=PPSA07412,EP9000-CUSA00000_00. Combine with ppids freely.
regionRegion code.
fieldsField 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 }.

GET /api/v2/games/{ppid}

A single game by its XBXprices ID (the PPID integer).

ParameterDescription
regionRegion code.
include_related1 to also include related editions/DLC.
fieldsField projection.

Returns: One game object. With include_related=1 it also carries a related[] array of slim objects.

GET /api/v2/games/by-psnid/{psnid}

A single game by its Xbox bigId (e.g. PPSA07412).

ParameterDescription
regionRegion code.
include_related1 to also include related editions/DLC.
fieldsField projection.

Returns: One game object (same shape as /games/{ppid}).

GET /api/v2/games/{ppid}/price-history

Price history for a game. — Free returns 403 PLAN_UPGRADE_REQUIRED. Depth depends on your plan: Indie and Pro see the last 3 months, Business the last 12 months.

ParameterDescription
regionRegion 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.

GET /api/v2/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.

ParameterDescription
regionRegion code.
hoursWidth of the “recently discounted” window in hours (default 48, range 1–168). Use hours=24 for “dropped today”.
platformseries, one, vr, psvr, psvr2 or move (any-of).
genreGenre token(s), e.g. genre=rpg,horror — see /genres.
min_discountMinimum discount percent.
price_min / price_maxPrice range (region minor units).
has_platinum / difficulty_max / hours_max / publisher / psplusSame achievement / publisher / Xbox Game Pass filters as /games.
sortrecent (default), discount, price, sale_price, name or lowest_ever.
orderasc / desc.
limit / pagePagination.
fieldsField 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.

GET /api/v2/sales

Active sale campaigns (e.g. “Big in Japan”), not individual games. Returns all active sales (no pagination).

ParameterDescription
regionRegion code.

Returns: An array of sale objects: id, name, startsAt, endsAt, numGames, imageURL, url (link to that sale’s endpoint). meta.count.

GET /api/v2/sales/{id}

All games inside one sale campaign. Sales that ended beyond your plan’s history window return 403 SALE_ARCHIVE_WINDOW.

ParameterDescription
regionRegion 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 }.

GET /api/v2/regions

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.

GET /api/v2/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.

Returns: An array of { token, label } objects, e.g. { "token": "rpg", "label": "RPG" }. meta { count, usage }.

GET /api/v2/platforms

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 }.

GET /api/v2/status

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.

GET /api/v2/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.

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).

GET /api/v2/account/usage

Your key’s request history — the same counters that feed the dashboard chart. Free: does not consume a request.

ParameterDescription
daysWindow 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}).

PUT /api/v2/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.

ParameterDescription
regionsJSON 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.

GET /api/v2/demo

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 }.

GET /api/v2/

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 /regions and GET/PUT /account/regions never spend monthly quota (they still count toward the burst limit).
  • Both surface live in headers: X-RateLimit-Limit/Used/Remaining and X-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

  • page is 1-based; limit is clamped to [1, max_page] (default 20 or the plan cap, whichever is smaller). Out-of-range numbers clamp silently; a non-numeric value is 400 INVALID_PARAM.
  • Deep paging is capped: an effective offset (page-1) × limit above 100,000 returns 400. Narrow with filters (region, platform, price, discount) rather than paging that far.
  • with_total=0 skips the COUNT(*) — noticeably faster on large result sets when you only need the next page. With it on, meta.pagination carries total, total_pages and has_more.
  • Ordering is stable: every sort breaks ties by PPID descending, so a row never jumps between pages.

Filtering — exact semantics

  • DLC and demos are hidden by default. Pass exclude_dlc=0 and/or exclude_demo=0 to include them.
  • platform and genre are any-of (OR): platform=series,one matches titles on either.
  • sort=sale_price ranks by the Xbox Game Pass price; price by base price. Default sort popularity, default direction desc (asc for name).
  • price_min/price_max take a human price in the region’s currency (e.g. 19.99), matched against the base price.
  • /games/search resolves 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 decimalPlaces from /regions). Don’t divide by 100 blindly — JP has 0 decimal places. For display, use the matching formatted… string.
  • LowestEverPrice / LowestEverGamePassPrice are null when unknown — no sentinel integer to special-case.
  • HoursLow / HoursHigh are floats in hours (e.g. 7.5); -1 = unknown. Difficulty is 1–10, -1 unknown.
  • 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) send Cache-Control: private, max-age=300 — reuse for 5 minutes, but private (per-key): shared/CDN caches must not store them. /regions is cached 24h; the keyless /demo is public.
  • 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 Vary on X-API-Key — never key a shared cache on the URL alone.

Robust error handling

  • Branch on error.code (stable), never on error.message (may be reworded).
  • 403 REGION_NOT_IN_PLAN carries structured error.details: requested_region, enabled_regions[], plan and the exact how_to_enable call — branch on those, don’t parse prose.
  • Honour Retry-After on every 429 and back off.
  • GET is safe to retry (idempotent). PUT /account/regions with an unchanged list is a no-op that does not spend your once-per-24h change — safe to call defensively.

Terms of use

PermittedNot 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].

↑ Top
We value your privacy. We use strictly-necessary cookies to run the site (sign-in, security). With your permission we also use analytics and advertising cookies to measure traffic and fund the site. You can accept all, reject non-essential, or choose per category. We use cookies for analytics & ads to measure traffic and fund the site; choose per category via Manage. See our Cookie & Privacy Policy.