eShop Game Price 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 NTPrices eShop Game Price API v2 is a REST API returning clean JSON for Switch & Switch 2 prices, price history and live sales/discounts across 70+ regions. The base URL is:
https://ntprices.com/api/v2
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" "https://ntprices.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://ntprices.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://ntprices.com/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" \
"https://ntprices.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://ntprices.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://ntprices.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://ntprices.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 — 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" "https://ntprices.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 — 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://ntprices.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.
| 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 50 | 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 NTPrices” 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 | No key supplied, or the key is not recognised. |
| 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. |
| 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 ps4 (base Switch), ps5 (Switch 2), vr or move. |
| min_discount | /games, /deals | Only games discounted at least this percent (e.g. 50). |
| sort | /games, /deals | Field to sort by — the valid keys differ per endpoint (see each endpoint below). |
| order | list endpoints | asc or desc. |
| limit | list endpoints | Results per page (capped server-side). |
| page | list endpoints | Page number (1-based). |
| with_total | list endpoints | 1 to include the total count in meta.pagination.total. |
| 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). |
| fields | game endpoints | Comma-separated whitelist — return only these fields (e.g. fields=PPID,ProductName,SalePrice). |
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), NSUID (eShop NSUID), ProductName, region, Img, NTPricesURL |
| Platform | IsSwitch (base Switch), IsSwitch2 (Switch 2), IsVR, IsDLC, IsDemoOrSoundtrack, SwitchSize, Switch2Size |
| Pricing | BasePrice, SalePrice, DiscPerc, LowestEverPrice — plus a formatted… string for every price |
| Reviews | OpenCriticID — use with the OpenCritic API for review scores |
| 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 is null when no record exists. Internal columns are never returned; use fields= to narrow the object further.
Removed: the legacy trophy fields Bronze, Silver, Gold, Platinum, Difficulty, HoursLow, HoursHigh and TrophyListURL — plus the has_platinum, difficulty, difficulty_max and hours_max query filters — are no longer returned or accepted. Trophies are a PlayStation concept with no Nintendo Switch equivalent; these never carried meaningful data for eShop titles. Requests using those filters simply ignore them.
Renamed (breaking): the PSNID field and the by-psnid endpoint / ?psnid= param were renamed to NSUID / by-nsuid / ?nsuid= (PlayStation → Nintendo eShop — the value was always the 14-digit eShop NSUID). The legacy ?psnid= param and /games/by-psnid/{psnid} path are kept as deprecated aliases.
Endpoints
Quick reference — tap a path to jump to its full details.
| Endpoint | Returns |
|---|---|
| GET /games | Filterable, paginated list of games with prices and discounts. |
| GET /games/search | Search games by name. |
| GET /games/{ppid} | A single game by its NTPrices ID (the PPID integer). |
| GET /games/by-nsuid/{nsuid} | A single game by its eShop product ID / NSUID (e.g. 70010000012345). The legacy /games/by-psnid/{psnid} path still works as a deprecated alias. |
| 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 | Everything currently discounted, sorted by your choice. |
| 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 eShop region. 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. |
Filterable, paginated list of games with prices and discounts.
| Parameter | Description |
|---|---|
| region | 2-letter region code (default: your account region). |
| platform | switch (base Switch), switch2 (Switch 2) or vr. |
| 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). |
| exclude_dlc / exclude_demo | 1 to omit DLC / demos. |
| publisher / publisher_like | Filter by publisher (exact / partial match). |
| sort | popularity, price, sale_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). |
| 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 }.
A single game by its NTPrices 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 eShop product ID / NSUID (e.g. 70010000012345). The legacy /games/by-psnid/{psnid} path still works as a deprecated alias.
| 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 (+ formatted… strings) and isLowestEverPrice (boolean). meta.count = number of history points; meta.history_window_months = your plan’s window.
Everything currently discounted, sorted by your choice.
| Parameter | Description |
|---|---|
| region | Region code. |
| platform | switch / switch2. |
| min_discount | Minimum discount percent. |
| sort | recent (default), discount, price or name. |
| order | asc / desc. |
| limit / page | Pagination. |
| fields | Field projection. |
Returns: An array of game objects that are on sale right now (each has a non-zero DiscPerc). meta.pagination.
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, NSUID, ProductName, Img, prices + formatted…). meta { game_count, dlc_count }.
Every supported eShop region. Cached 24h.
Returns: An array of region objects: region (code), language, currencySymbol, decimalPlaces, and inYourPlan (whether your plan can query it). meta.count.
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" \
"https://ntprices.com/api/v2/deals?region=gb&sort=discount&order=desc&limit=20"
# One game by eShop product ID / NSUID, with related editions
curl -H "X-API-Key: $PP_KEY" \
"https://ntprices.com/api/v2/games/by-nsuid/70010000012345?region=us&include_related=1"
# Price history (paid plans; 3 months back, Business 12)
curl -H "X-API-Key: $PP_KEY" \
"https://ntprices.com/api/v2/games/7704/price-history?region=us"
# No key needed — try this one in your browser:
curl "https://ntprices.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.
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 NTPrices” 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].
