Getting started
The Binderim Data API is a read-only JSON API over HTTPS. Every endpoint lives under one base address:
https://dev.binderim.com/v1
- Sign in with your Binderim account (top right).
- Accept the API terms and create an API key on your dashboard. The key is shown once; copy it then.
- Send the key with every request and read the
datapart of the answer.
curl
curl -H "X-Api-Key: $BINDERIM_API_KEY" "https://dev.binderim.com/v1/games"
JavaScript
const res = await fetch("https://dev.binderim.com/v1/games", {
headers: { "X-Api-Key": process.env.BINDERIM_API_KEY },
});
const body = await res.json();
Python
import os, requests
res = requests.get(
"https://dev.binderim.com/v1/games",
headers={"X-Api-Key": os.environ["BINDERIM_API_KEY"]},
)
body = res.json()
The API never changes data: every endpoint reads. Keep your key on your server; browsers would expose it.
Authentication
Send your key in the X-Api-Key header, or as Authorization: Bearer <key>. Keys are never accepted in the URL, because URLs end up in logs; a request with the key in the URL gets 401.
You can create one key per account, replace it (the old one stops working at once) or revoke it on your dashboard. Only GET /v1 and GET /v1/health work without a key.
Plans
Limits and features come from your plan. The plan of your key is returned as meta.plan in every answer.
| Plan | Requests per day | Requests per second | Cards per response | Cards per day | Price sources | Currency conversion | Graded prices and AI | Bulk download |
|---|---|---|---|---|---|---|---|---|
| Free | 100 | 1 | 10 | 1,000 | General | No | No | No |
| Pro | 1,000 | 3 | 50 | 50,000 | General and other sources | Yes | No | No |
| Business | 5,000 | 10 | 100 | 500,000 | General, other sources and Binderim sales | Yes | Yes | Weekly files |
"Cards per day" is the headline quota: requests per day times the cards one request can return. Commercial use needs the Business plan; see the terms.
Rate limits
Every plan has a daily quota (counted per UTC day, resetting at 00:00 UTC) and a per-second limit. A request that reaches an endpoint counts as one request, even when it ends in a 4xx answer such as card_not_found. Requests rejected before that (401, 503) do not count. Every answer to a request with a valid key tells you where you stand:
X-RateLimit-Limit: your daily quota (requests)X-RateLimit-Remaining: requests left todayX-RateLimit-Reset: when the quota resets (Unix time, seconds)
Over a limit you receive 429 with rate_limited (per second) or daily_quota_exceeded, and a Retry-After header in seconds. Successful answers can be cached for 60 seconds (Cache-Control: private, max-age=60).
The one exception is GET /v1/bulk (Bulk download): it does not count against the daily quota and works when the quota is used up, but the per-second limit applies, and its answers are never cached (Cache-Control: no-store).
Errors
Errors have the same shape everywhere. Use the code in your program; the message is for people and may change.
{ "error": { "code": "invalid_api_key", "message": "Missing, invalid or revoked API key. ..." } }
| Code | HTTP | Meaning |
|---|---|---|
invalid_api_key | 401 | The API key is missing, wrong or revoked, or it was sent in the URL. |
plan_upgrade_required | 403 | Your plan does not include this feature, price source or currency conversion. |
invalid_id | 400 | The card id is not a valid id (lowercase letters, digits and hyphens). |
invalid_parameter | 400 | A query parameter is not valid, for example page=0. |
page_size_too_large | 400 | page_size is larger than your plan allows per response. |
batch_too_large | 400 | The batch has more distinct ids than your plan allows per request. |
invalid_body | 400 | The request body is not the expected JSON object. |
unknown_source | 400 | The price source name is not known. |
unknown_currency | 400 | The currency is not supported. Use USD, EUR, GBP or TRY. |
game_not_found | 404 | No game with that id. See GET /v1/games. |
set_not_found | 404 | No set with that id. |
card_not_found | 404 | No card with that id. |
not_found | 404 | Unknown endpoint. |
method_not_allowed | 405 | The endpoint exists but not for this HTTP method. The Allow header lists the allowed ones. |
card_deleted | 410 | The card was removed from the catalog. |
payload_too_large | 413 | The request body is larger than 64 KB. |
rate_limited | 429 | You sent more requests per second than your plan allows. Wait for Retry-After seconds. |
daily_quota_exceeded | 429 | Your daily quota is used up. It resets at 00:00 UTC. |
internal_error | 500 | Something went wrong on our side. Retry later. |
warming_up | 503 | The service is starting up (or the AI data is still loading). Retry after Retry-After seconds. This does not use your quota. |
bulk_not_ready | 503 | The weekly bulk files are not ready yet (the first release is still being built) or are temporarily unavailable. Retry after Retry-After seconds. This does not use your quota. |
server_busy | 503 | Too many requests are being handled at once. Retry shortly. This does not use your quota. |
Response format
Successful answers wrap the result in data and describe it in meta:
{
"data": [ ... ],
"meta": {
"plan": "free",
"catalog_as_of": "2026-10-22T06:00:00Z",
"prices_as_of": "2026-10-22T08:00:00Z",
"attribution": "Powered by Binderim (https://binderim.com)"
}
}
- Field names are
snake_case. Times are UTC, ISO 8601. - A field with no value is left out; it is never
null. meta.attributionappears on the Free plan only (see Attribution).catalog_as_ofandprices_as_oftell you how fresh the data is. The catalog is rebuilt every 3 hours and prices every hour.
Endpoints
All endpoints answer GET (and HEAD) except the batch endpoint, which answers POST. Game ids are short names such as pokemon; set ids are numbers; card ids are described in Card ids and merges.
GET /v1/health and GET /v1
No key, no quota. /v1/health answers 200 when the service is ready and 503 while it starts up; use it for uptime checks. GET /v1 returns the API name and version.
{ "status": "ok", "ready": true, "ai_ready": true, "catalog_built_utc": "2026-10-22T06:00:00Z", "prices_built_utc": "2026-10-22T08:00:00Z", "bulk_generated_utc": "2026-10-19T02:00:12Z" }
bulk_generated_utc is when the latest weekly bulk release was generated; it is left out until the first release exists and does not change status.
curl
curl "https://dev.binderim.com/v1/health"
JavaScript
const res = await fetch("https://dev.binderim.com/v1/health");
const body = await res.json();
Python
import os, requests
res = requests.get(
"https://dev.binderim.com/v1/health",
)
body = res.json()
GET /v1/games
The games in the catalog, with their number of sets and cards.
{ "data": [ { "id": "pokemon", "name": "Pokémon", "set_count": 932, "card_count": 76304 } ], "meta": { ... } }
curl
curl -H "X-Api-Key: $BINDERIM_API_KEY" "https://dev.binderim.com/v1/games"
JavaScript
const res = await fetch("https://dev.binderim.com/v1/games", {
headers: { "X-Api-Key": process.env.BINDERIM_API_KEY },
});
const body = await res.json();
Python
import os, requests
res = requests.get(
"https://dev.binderim.com/v1/games",
headers={"X-Api-Key": os.environ["BINDERIM_API_KEY"]},
)
body = res.json()
GET /v1/games/{game}/sets
All sets of a game: id, code, name, series, release_date, card_count, and logo / symbol image addresses when we have them. Not paged. Unknown game: 404 game_not_found.
curl
curl -H "X-Api-Key: $BINDERIM_API_KEY" "https://dev.binderim.com/v1/games/pokemon/sets"
JavaScript
const res = await fetch("https://dev.binderim.com/v1/games/pokemon/sets", {
headers: { "X-Api-Key": process.env.BINDERIM_API_KEY },
});
const body = await res.json();
Python
import os, requests
res = requests.get(
"https://dev.binderim.com/v1/games/pokemon/sets",
headers={"X-Api-Key": os.environ["BINDERIM_API_KEY"]},
)
body = res.json()
GET /v1/sets/{id}/cards
The cards of a set, in card-number order. Query: page (from 1) and page_size (default and maximum are your plan's cards per response). meta carries page, page_size and total. Errors: set_not_found, invalid_parameter, page_size_too_large.
curl
curl -H "X-Api-Key: $BINDERIM_API_KEY" "https://dev.binderim.com/v1/sets/2841/cards?page=1&page_size=10"
JavaScript
const res = await fetch("https://dev.binderim.com/v1/sets/2841/cards?page=1&page_size=10", {
headers: { "X-Api-Key": process.env.BINDERIM_API_KEY },
});
const body = await res.json();
Python
import os, requests
res = requests.get(
"https://dev.binderim.com/v1/sets/2841/cards?page=1&page_size=10",
headers={"X-Api-Key": os.environ["BINDERIM_API_KEY"]},
)
body = res.json()
GET /v1/cards/{publicId}
One card: id, game, set, name, number, language, rarity, images and external ids. On the Business plan the answer also has grades and ai. Optional query: currency (adds converted amounts to grades; Pro and Business).
{
"data": {
"id": "pkm-sv08-123-en", "game": "pokemon",
"set": { "id": "2841", "code": "sv08", "name": "Surging Sparks" },
"name": "Pikachu", "number": "123", "language": "en", "rarity": "Common",
"images": { "large": "https://img.binderim.com/images/...", "small": "https://img.binderim.com/images/..." },
"external_ids": { "tcgplayer": "123456", "pricecharting": { "normal": "1111", "reverse-holo": "2222" } }
},
"meta": { ... }
}
curl
curl -H "X-Api-Key: $BINDERIM_API_KEY" "https://dev.binderim.com/v1/cards/pkm-sv08-123-en"
JavaScript
const res = await fetch("https://dev.binderim.com/v1/cards/pkm-sv08-123-en", {
headers: { "X-Api-Key": process.env.BINDERIM_API_KEY },
});
const body = await res.json();
Python
import os, requests
res = requests.get(
"https://dev.binderim.com/v1/cards/pkm-sv08-123-en",
headers={"X-Api-Key": os.environ["BINDERIM_API_KEY"]},
)
body = res.json()
GET /v1/cards/{publicId}/prices
The latest prices of a card, one entry per source, price type and variant. Optional query: source (one source name) and currency (USD, EUR, GBP or TRY; Pro and Business). Business also gets grades. See Prices and currencies.
{
"data": {
"card_id": "pkm-sv08-123-en",
"prices": [
{ "source": "general", "type": "market", "variant": "normal", "amount": 1.5, "currency": "USD", "as_of": "2026-10-22T03:12:00Z" },
{ "source": "tcgplayer", "type": "market", "variant": "reverse-holo", "amount": 2.1, "currency": "USD", "as_of": "2026-10-22T03:12:00Z",
"converted": { "amount": 1.57, "currency": "GBP", "rate": 0.748, "rate_date": "2026-10-22" } }
]
},
"meta": { ... }
}
curl
curl -H "X-Api-Key: $BINDERIM_API_KEY" "https://dev.binderim.com/v1/cards/pkm-sv08-123-en/prices?currency=GBP&source=tcgplayer"
JavaScript
const res = await fetch("https://dev.binderim.com/v1/cards/pkm-sv08-123-en/prices?currency=GBP&source=tcgplayer", {
headers: { "X-Api-Key": process.env.BINDERIM_API_KEY },
});
const body = await res.json();
Python
import os, requests
res = requests.get(
"https://dev.binderim.com/v1/cards/pkm-sv08-123-en/prices?currency=GBP&source=tcgplayer",
headers={"X-Api-Key": os.environ["BINDERIM_API_KEY"]},
)
body = res.json()
POST /v1/prices/batch
Prices for many cards in one request. Send { "ids": [...], "currency": "USD", "source": "general" } (currency and source are optional). The body is at most 64 KB and the number of distinct ids is at most your plan's cards per response. Repeated ids are answered once. Every id gets its own result:
ok:id,prices(andgradeson Business)merged: the id you sent was retired;merged_intois the id to use, and the prices are those of that carddeleted,not_found,invalid_id: no prices; the request itself still succeeds
{ "data": [ { "requested_id": "pkm-sv08-123-en", "status": "ok", "id": "pkm-sv08-123-en", "prices": [ ... ] },
{ "requested_id": "pkm-old-001-en", "status": "merged", "merged_into": "pkm-sv08-001-en", "prices": [ ... ] },
{ "requested_id": "pkm-gone-001-en", "status": "deleted" } ], "meta": { ... } }
curl
curl -X POST -H "X-Api-Key: $BINDERIM_API_KEY" -H "Content-Type: application/json" \
-d '{"ids": ["pkm-sv08-123-en", "mtg-snc-1-en"], "currency": "USD"}' \
"https://dev.binderim.com/v1/prices/batch"
JavaScript
const res = await fetch("https://dev.binderim.com/v1/prices/batch", {
method: "POST",
headers: { "X-Api-Key": process.env.BINDERIM_API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({"ids": ["pkm-sv08-123-en", "mtg-snc-1-en"], "currency": "USD"}),
});
const body = await res.json();
Python
import os, requests
res = requests.post(
"https://dev.binderim.com/v1/prices/batch",
headers={"X-Api-Key": os.environ["BINDERIM_API_KEY"]},
json={"ids": ["pkm-sv08-123-en", "mtg-snc-1-en"], "currency": "USD"},
)
body = res.json()
GET /v1/bulk
Business plan only. Time-limited download links for the weekly bulk files; see Bulk download. Optional query: game (one game's files plus retired_ids). Errors: plan_upgrade_required on other plans, game_not_found, bulk_not_ready.
curl
curl -H "X-Api-Key: $BINDERIM_API_KEY" "https://dev.binderim.com/v1/bulk?game=pokemon"
JavaScript
const res = await fetch("https://dev.binderim.com/v1/bulk?game=pokemon", {
headers: { "X-Api-Key": process.env.BINDERIM_API_KEY },
});
const body = await res.json();
Python
import os, requests
res = requests.get(
"https://dev.binderim.com/v1/bulk?game=pokemon",
headers={"X-Api-Key": os.environ["BINDERIM_API_KEY"]},
)
body = res.json()
Prices and currencies
The API returns the latest price only. There is no price history. A price older than its freshness limit is left out instead of being returned stale.
| Source | What it is | Plan |
|---|---|---|
general | The general price Binderim shows for a card, per variant. Type market. | All plans |
tcgplayer, ebay, ygoprodeck, lorcast, optcgapi, cardmarket | Prices from those markets and feeds. Types market, trend or card_low (the lowest price across all printings of the card). Cardmarket prices are not returned for Pokémon and One Piece. | Pro and Business |
binderim-tr, binderim-uk | What cards really sold for on Binderim: sold_median (with sample_size, the sales in the last 90 days) and last_sold. A market without recent data returns no entries. | Business |
- Freshness. Daily sources must be at most 3 days old,
ygoprodeckat most 8 days, Binderim sales at most 24 hours. - Amounts are in the currency of the source (
currencyin each entry).conditionis present for Binderim sales that are tied to a card condition. - Converting. With
currency=GBP(Pro and Business) each entry also getsconverted: the amount, the rate and the date of the exchange rate. If we have no fresh exchange rate,convertedis left out; the original amount is always there. - Choosing a source.
source=tcgplayernarrowspricesto that source.pricechartingis not a source name you can ask for; the raw price isgeneral. It only appears assourceinsidegrades. - Asking for something your plan does not include (a currency, a source) returns
403 plan_upgrade_required.
Card ids and merges
Every card has a readable id that never changes, for example pkm-sv08-123-en: game prefix, set, card number and language (en, ja, zh, ko). Ids are lowercase letters, digits and hyphens, at most 160 characters. Store them; they are safe to keep in your own database.
- When two catalog entries for the same card are merged, the retired id keeps working: you get the card it was merged into, and
merged_fromtells you which id you asked for. In a batch the result hasstatus: mergedandmerged_into. Switch to the new id when you can; there is no redirect, because a second request would use your quota again. - A card removed from the catalog answers
410 card_deleted(status: deletedin a batch). - An id that does not exist answers
404 card_not_found; a malformed id answers400 invalid_id. - Ids are never reused for another card.
Variants and grades
The variant of a price is a lowercase name with hyphens, for example normal, foil or etched. These names are readable on purpose, and a few get a fixed public name:
| Variant | Meaning |
|---|---|
1st-edition | First edition print |
master-ball-reverse | Reverse holo with a Master Ball pattern |
poke-ball-reverse | Reverse holo with a Poké Ball pattern |
reverse-holo | Reverse holo print |
The same names are used as keys in external_ids.pricecharting, which maps each variant to the product number of the price source.
Grades (Business) are prices of graded copies. Each entry in grades has a grade key:
| Grade | Meaning |
|---|---|
psa10 | PSA 10 |
bgs10 | Beckett (BGS) 10 |
cgc10 | CGC 10 |
sgc10 | SGC 10 |
grade7 | Grade 7 from any grading company |
grade8 | Grade 8 from any grading company |
grade9 | Grade 9 from any grading company |
grade95 | Grade 9.5 from any grading company |
grade9 means grade 9 from any grading company; we do not split the numeric grades by company.
AI block
On the Business plan, GET /v1/cards/{publicId} includes an ai block with what our image analysis found on the front of the card: model_version, colors, emotions, mood, style, setting, time_of_day, weather, character_count and description. The beta object (objects, characters, costumes) is experimental and may change.
Values the analysis could not decide are left out. A card without an analysis has no ai field. While the AI data is loading after a restart, single-card requests on Business answer 503 warming_up without using quota.
Bulk download (Business)
The Business plan includes weekly files of the whole catalog. They are meant for the first load and for keeping a full copy; use the endpoints above for changes in between. Downloading the files does not use your daily quota.
Schedule
A new release is built every Monday at about 02:00 UTC (05:00 in Turkey) and holds the data of that moment. A release id looks like 20261026T020012Z: the time the build started. The latest three releases are kept. Until a new release is ready, the previous one stays available; next_release_after in the answer tells you when to expect the next one. If a build fails, the previous release stays and the build is tried again.
Getting the links
GET /v1/bulk lists the files of the latest release, each with a download link. The optional game parameter limits the list to one game's files plus retired_ids. Each link is valid for 15 minutes, and a link may be handed out again for the next few minutes, so every link in an answer has at least 5 minutes left. url_expires_at is when the earliest of them stops working: when it passes, ask again. A download that has already started is not cut. Treat a link like a password and do not share it.
- It does not count against your daily quota and works when the quota is used up. The per-second limit of your plan still applies.
- Other plans get
403 plan_upgrade_required. - While the first release is being built, or if storage is unavailable, you get
503 bulk_not_readywithRetry-After: 3600. Try again later.
{
"data": {
"release": "20261026T020012Z",
"generated_at": "2026-10-26T02:00:12Z",
"catalog_as_of": "2026-10-26T00:00:09Z", "prices_as_of": "2026-10-26T01:00:03Z", "ai_as_of": "2026-10-25T14:00:21Z",
"next_release_after": "2026-11-02T02:00:00Z",
"url_expires_at": "2026-10-26T09:15:00Z",
"manifest_url": "https://...",
"files": [
{ "game": "pokemon", "kind": "cards", "records": 76304, "bytes": 2380000, "sha256": "9f2c...", "url": "https://..." },
{ "kind": "retired_ids", "records": 0, "bytes": 20, "sha256": "ac73...", "url": "https://..." }
]
},
"meta": { ... }
}
Files
For every game there are four files: sets, cards, prices and ai (a game with nothing to put in a file still has it, empty). One more file, retired_ids, covers all games. Every file is gzip-compressed JSON Lines (UTF-8): one JSON object per line. Each line is the same object the API returns:
| File | Each line is | No line for |
|---|---|---|
sets | An item of GET /v1/games/{game}/sets | - |
cards | An item of GET /v1/sets/{id}/cards. grades and ai are not in it: they have their own files. | - |
prices | The data of GET /v1/cards/{publicId}/prices: card_id, prices and grades | A card without any price |
ai | { "card_id", "ai" }, where ai is the block from GET /v1/cards/{publicId} | A card without an AI block |
retired_ids | { "id", "status", "merged_into" }. status is merged or deleted, as in the batch endpoint. | - |
The files follow the same rules as the API does for the Business plan: the same price sources, freshness limits, variant names, image links and AI values.
Currencies. Amounts are in the currency of their source. The exchange rates are in the manifest, as exchange_rates (rates[X] is how much one US dollar is in X). To convert, use amount / rates[from] * rates[to] and round to 2 decimals, which is what the API does. If the manifest has no exchange_rates, the rates are too old to use: do not convert.
Manifest and checksums
manifest.json lists every file of the release with its record count, size and SHA-256 checksum. The checksum is of the compressed file as you download it; check it after saving.
{
"format_version": 1,
"release": "20261026T020012Z",
"generated_at": "2026-10-26T02:00:12Z",
"catalog_as_of": "...", "prices_as_of": "...", "ai_as_of": "...",
"exchange_rates": { "base": "USD", "rate_date": "2026-10-25", "rates": { "EUR": 0.891, "GBP": 0.748, "TRY": 49.6, "USD": 1 } },
"files": [ { "game": "pokemon", "kind": "cards", "path": "pokemon/cards.jsonl.gz", "records": 76304, "bytes": 2380000, "sha256": "9f2c..." } ]
}
Example
The files are sent exactly as stored, without a Content-Encoding header, so your HTTP client does not unpack them: save the bytes, check the checksum, then unpack. Never send your API key to a download link.
# 1. Ask for the links (does not use your daily quota)
curl -H "X-Api-Key: $BINDERIM_API_KEY" "https://dev.binderim.com/v1/bulk?game=pokemon"
# 2. Download a file with its url from the answer, then check and read it
curl -o pokemon-cards.jsonl.gz "<url from the answer>"
sha256sum pokemon-cards.jsonl.gz # compare with sha256 in the answer
gunzip -c pokemon-cards.jsonl.gz | head -n 2
import gzip, hashlib, json, os, requests
release = requests.get(
"https://dev.binderim.com/v1/bulk?game=pokemon",
headers={"X-Api-Key": os.environ["BINDERIM_API_KEY"]},
).json()["data"]
entry = next(f for f in release["files"] if f["kind"] == "cards")
raw = requests.get(entry["url"]).content # the .gz bytes, as stored
assert hashlib.sha256(raw).hexdigest() == entry["sha256"]
for line in gzip.decompress(raw).splitlines():
card = json.loads(line)
print(card["id"], card.get("name"))
Keeping ids up to date
Use retired_ids to update ids you stored: for merged, replace the id with merged_into; for deleted, remove the card. An id that is not in the file is a live card (or one we have never had). Read Card ids and merges for how ids behave.
Attribution
On the Free plan you must show Powered by Binderim with a link to https://binderim.com wherever you show our data. The text to use is in meta.attribution. Pro and Business have no attribution requirement. The terms have the details.
OpenAPI file
The whole API is described in an OpenAPI 3.1 file: /openapi/v1.json. Import it into Postman, Insomnia or a code generator. It lists every endpoint, answer shape and error code, and it is checked against the real answers.
Changelog
- v1: first public version: games, sets, cards, latest prices from several sources, batch prices, permanent card ids with merge handling, converted amounts, graded prices and the AI block for Business, and the OpenAPI file.
- Before launch: four internal price variant codes were replaced by the readable names above, so v1 never shows them.
- v1: bulk download for Business:
GET /v1/bulkand weekly.jsonl.gzfiles,bulk_not_readyerror code andbulk_generated_utcin/v1/health.
Changes that could break a program need a new version (/v2); /v1 stays as it is.