Plat Prices
All Regions Blog

PlayStation 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 PlatPrices PlayStation Store API v2 is a REST API returning clean JSON for PS4 & PS5 prices, price history, live sales/discounts and trophy data across 70+ regions. The base URL is:

https://platprices.com/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. PlayStation has no single global store: every country runs its own PlayStation 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. “Dark Souls III” 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, using 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 gets 2, Business gets 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" "https://platprices.com/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"]}' \
     "https://platprices.com/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" \
  "https://platprices.com/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" \
  "https://platprices.com/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("https://platprices.com/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(
    "https://platprices.com/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("https://platprices.com/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. Here are 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. (Sending the key in the URL is not supported on v2, because keys in URLs leak into server and proxy logs.)

curl -H "X-API-Key: $PP_KEY" "https://platprices.com/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, with 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"]}' "https://platprices.com/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 None
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 PlatPrices” 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. It is 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 ps4, ps5, 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 PS Plus price; valid keys differ per endpoint (see each below).
orderlist endpointsasc or desc.
limitlist endpointsRecords per page, clamped to between 1 and your plan max (25 on Free, 200 on 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(*). It is 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. It 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), PSNID, ProductName, region, Img, PSStoreURL, PlatPricesURL
PlatformIsPS4, IsPS5, IsVR, VRType (0 none / 1 PS VR / 2 PS VR2), IsMove, IsDLC, IsDemoOrSoundtrack, PS4Size, PS5Size
LanguagesVoiceLang, SubtitleLang, VoiceLangByPlatform, SubtitleLangByPlatform
PricingBasePrice, SalePrice, PlusPrice, DiscPerc, LowestEverPrice, LowestEverPlusPrice, LowestRecentPrice, LaunchPrice, PriceCurrency, PSPlusNeeded, PSPExtra, PSPPremium, plus a formatted… string for every price
TrophiesBronze, Silver, Gold, Platinum, Difficulty, HoursLow, HoursHigh, TrophyListURL, OpenCriticID
Catalogue stateIsTrial (free PS Plus Game Trial), IsPreorder (release date still in the future), IsOnSale (discounted right now), Delisted / PubDelisted (per-game / inherited from publisher) and the combined IsDelisted, all 0/1 integers. See the note on delisting below: we now detect one more way a product can leave the store, so expect more products to report IsDelisted: 1 than before
OtherRating, RatingDesc, NumLibrary, NumWishlist, genre flags
Store metadataNpTitleId, ConceptID, StarRating, StarRatingCount, ShortDesc, EditionName, EditionType, StoreClass, StoreClassLocal, PrivacyPolicyURL, IsAgeRestricted

Prices are integers in the region’s minor units (scaled by decimalPlaces from /regions). For display, use the matching formatted… string. LowestEverPrice/LowestEverPlusPrice 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. VoiceLang and SubtitleLang are flat JSON arrays of strings and are [] when we have none: every element is a plain non-empty string, never a nested array, an object or a number. Elements are normally language codes (en, pt_BR, zh_Hant), but a small number of products were labelled by hand by our staff and carry readable names instead (English), so treat an element as an opaque string rather than assuming it parses as a code. VoiceLangByPlatform and SubtitleLangByPlatform are objects keyed by platform (PS4, PS5, PS3, VITA) and are {} when we have none; each value follows the same flat-array-of-strings rule. Every store-metadata field is null until we have observed it. StarRating is a number from 0 to 5, and IsAgeRestricted is 0, 1 or null.

What v1 does and does not have. The legacy v1 endpoint (/api.php) returns none of the store-metadata fields, none of the per-platform language objects, and none of the three store-price fields below, because v1 is frozen and its response shape does not change. It does return VoiceLang and SubtitleLang, and always has: the difference is the type. In v1 they are JSON-encoded strings you have to decode yourself; in v2 they are real JSON arrays.

The store’s own price fields

Three pricing fields come straight from the PlayStation Store rather than from our price history. They are v2 only, they are per region, and each is null until we have actually seen it on that storefront.

FieldMeaning
LowestRecentPrice
formattedLowestRecentPrice
Sony’s own lowest price in the last 30 days for this product in this region, the figure the EU Omnibus directive requires them to publish. Integer minor units plus the formatted string. This is Sony’s number, not ours, and its 30 day window is not the same thing as LowestEverPrice.
LaunchPrice
formattedLaunchPrice
The launch price the store reports for the product, when it reports one. Rarely populated: Sony sends this field empty for most products.
PriceCurrencyThe ISO 4217 code the storefront quoted those prices in, e.g. USD, CZK, JPY. Use it rather than guessing from the currency symbol: $ alone is ambiguous across US, CA, AU, HK, SG and MX.

Unlike LowestEverPrice, the two formatted… companions here are always present and are null when we have no price, so you can read both keys without checking whether they exist.

Delisting: one more way a product is detected as gone

The PlayStation Store answers with HTTP 200 and a page containing no product at all once a product is permanently withdrawn. We used to read that as “page loaded, product still live”. We now recognise it and count it the same as a 404, which feeds the usual delist evidence counter.

What that means for you: nothing in the response shape changes. Delisted, PubDelisted and IsDelisted keep their meaning and their 0/1 type. But products that were gone from the store while we still reported them as live will now correctly report IsDelisted: 1, so if you branch on that flag expect a one-off step up in how many products match. A product is still only marked delisted after repeated evidence across several regions, and a delisting a human has ruled on by hand is never overridden.

Endpoints

Quick reference. Tap a path to jump to its full details.

EndpointReturns
GET /games Filterable, paginated list of games with prices, discounts and trophy data.
GET /games/search Search games by name.
GET /games/batch Fetch many games at once by id, so you can 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 PlatPrices ID (the PPID integer).
GET /games/by-psnid/{psnid} A single game by its PSN product ID (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, trophy 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 PlayStation 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, from 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, with 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 https://platprices.com/api/v2/games

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

ParameterDescription
region2-letter region code (default: your account region).
platformps4, ps5, 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_maxTrophy difficulty range (1 to 10).
hours_maxMaximum hours-to-platinum.
has_platinum1 = only games with a platinum trophy.
exclude_dlc / exclude_demoDLC and demos are hidden by default. Pass exclude_dlc=0 / exclude_demo=0 to include them.
only_dlc1 = add-ons only, the DLC deals view. Cannot be combined with exclude_dlc=1, which returns 400 INVALID_PARAM; with exclude_dlc=0 it simply wins.
is_bundle1 = bundles only, meaning products whose game groups two or more titles.
publisher / publisher_likeFilter by publisher (exact / partial match).
psplusPS Plus tier filter: essential (this month's free games), extra (the on-demand catalogue, Ubisoft+ Classics included) or premium. Matches what the PS Plus pages list.
exclude_psplusHide one or more tiers instead of selecting one, e.g. exclude_psplus=extra,premium for purchases only. Same tokens as psplus, any-of.
is_trial1 = only games with a free PS Plus Game Trial.
preorder1 = only unreleased pre-order titles (release date still in the future).
delisted / exclude_delisteddelisted=1 returns only delisted titles; exclude_delisted=1 hides them. Both are shown by default.
sortpopularity (default), price, sale_price (ranks by the PS Plus price), discount, name, release, lowest_ever or recently_discounted.
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. Each card is 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 https://platprices.com/api/v2/games/batch

Fetch many games at once by id, so you can 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 PlatPrices ids, e.g. ppids=7704,8123,9001.
psnidsComma-separated PSN product ids, 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, so split into more calls). meta { requested, returned, missing[] (ids not found in this region), max_batch }.

GET https://platprices.com/api/v2/games/{ppid}

A single game by its PlatPrices 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 https://platprices.com/api/v2/games/by-psnid/{psnid}

A single game by its PSN product ID (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 https://platprices.com/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, PlusPrice (+ formatted… strings), isLowestEverPrice and isLowestEverPlusPrice (booleans). meta.count = number of history points; meta.history_window_months = your plan’s window.

GET https://platprices.com/api/v2/deals

Recently-discounted games. Accepts the full /games filter set (genre, price range, trophy 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 to 168). Use hours=24 for “dropped today”.
platformps4, ps5, 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 / psplus / exclude_psplusSame trophy / publisher / PS Plus filters as /games.
only_dlc / is_bundleonly_dlc=1 for discounted add-ons only, is_bundle=1 for discounted bundles only. Same meaning as on /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 https://platprices.com/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 https://platprices.com/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, PSNID, ProductName, Img, Platinum, prices + formatted…). meta { game_count, dlc_count }.

GET https://platprices.com/api/v2/regions

Every supported PlayStation 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 https://platprices.com/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 https://platprices.com/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": "ps5", "label": "PlayStation 5" }. meta { count, usage }.

GET https://platprices.com/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 https://platprices.com/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 https://platprices.com/api/v2/account/usage

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

ParameterDescription
daysWindow in days, 1 to 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 and chart-ready) and endpoints[] (top routes by volume, e.g. v2/games/{id}).

PUT https://platprices.com/api/v2/account/regions

Choose the store regions your key serves, entirely via the API, with 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 https://platprices.com/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 https://platprices.com/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" \
  "https://platprices.com/api/v2/deals?region=gb&sort=discount&order=desc&limit=20"

# One game by PSN product ID, with related editions
curl -H "X-API-Key: $PP_KEY" \
  "https://platprices.com/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" \
  "https://platprices.com/api/v2/games/7704/price-history?region=us"

# No key needed, try this one in your browser:
curl "https://platprices.com/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 returns 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 returns 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 on Free, 200 on 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(*) and is 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.
  • DLC has three states, not two. Nothing sent = base games only; exclude_dlc=0 = base games and add-ons together; only_dlc=1 = add-ons alone. Sending only_dlc=1 together with exclude_dlc=1 is a contradiction and returns 400 INVALID_PARAM rather than an empty list.
  • is_bundle=1 keeps only bundles, products whose game groups two or more titles. It is the one filter that reads a column outside Products and Regions, and it does not change any other count.
  • platform and genre are any-of (OR): platform=ps4,ps5 matches titles on either.
  • sort=sale_price ranks by the PS Plus 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. It is 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 / LowestEverPlusPrice are null when unknown, with no sentinel integer to special-case. Their formatted… companions are absent, not null, in that case.
  • LowestRecentPrice / LaunchPrice are also null when unknown, but their formatted… companions are always present and null alongside them. PriceCurrency is null or a 3-letter code, never an empty string.
  • VoiceLang / SubtitleLang are arrays and are [], never null. The array is flat and every element is a non-empty string: no nested arrays, no objects, no numbers, no nulls. Most elements are language codes; a hand-labelled product returns readable names (English) instead, so do not assume an element parses as a code. VoiceLangByPlatform / SubtitleLangByPlatform are objects and are {}, never null or [], and each value obeys the same flat-array-of-strings rule.
  • Every store-metadata field (NpTitleId, ConceptID, StarRating…) is null until observed. IsAgeRestricted is tri-state: 1, 0 or null, so test it with an explicit null check, not for truthiness.
  • HoursLow / HoursHigh are floats in hours (e.g. 7.5); -1 = unknown. Difficulty is 1 to 10, -1 unknown.
  • Flag/count fields (IsPS5, 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 note the private (per-key) flag: 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, so 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, so it is 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 PlatPrices” 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.