Skip to content
Data API

Documentation

From your first request to your product: endpoints, prices, errors and the OpenAPI file.

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
  1. Sign in with your Binderim account (top right).
  2. Accept the API terms and create an API key on your dashboard. The key is shown once; copy it then.
  3. Send the key with every request and read the data part 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.

PlanRequests per dayRequests per secondCards per responseCards per dayPrice sourcesCurrency conversionGraded prices and AIBulk 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 today
  • X-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. ..." } }
CodeHTTPMeaning
invalid_api_key401The API key is missing, wrong or revoked, or it was sent in the URL.
plan_upgrade_required403Your plan does not include this feature, price source or currency conversion.
invalid_id400The card id is not a valid id (lowercase letters, digits and hyphens).
invalid_parameter400A query parameter is not valid, for example page=0.
page_size_too_large400page_size is larger than your plan allows per response.
batch_too_large400The batch has more distinct ids than your plan allows per request.
invalid_body400The request body is not the expected JSON object.
unknown_source400The price source name is not known.
unknown_currency400The currency is not supported. Use USD, EUR, GBP or TRY.
game_not_found404No game with that id. See GET /v1/games.
set_not_found404No set with that id.
card_not_found404No card with that id.
not_found404Unknown endpoint.
method_not_allowed405The endpoint exists but not for this HTTP method. The Allow header lists the allowed ones.
card_deleted410The card was removed from the catalog.
payload_too_large413The request body is larger than 64 KB.
rate_limited429You sent more requests per second than your plan allows. Wait for Retry-After seconds.
daily_quota_exceeded429Your daily quota is used up. It resets at 00:00 UTC.
internal_error500Something went wrong on our side. Retry later.
warming_up503The service is starting up (or the AI data is still loading). Retry after Retry-After seconds. This does not use your quota.
bulk_not_ready503The 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_busy503Too 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.attribution appears on the Free plan only (see Attribution).
  • catalog_as_of and prices_as_of tell 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 (and grades on Business)
  • merged: the id you sent was retired; merged_into is the id to use, and the prices are those of that card
  • deleted, 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.

SourceWhat it isPlan
generalThe general price Binderim shows for a card, per variant. Type market.All plans
tcgplayer, ebay, ygoprodeck, lorcast, optcgapi, cardmarketPrices 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-ukWhat 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, ygoprodeck at most 8 days, Binderim sales at most 24 hours.
  • Amounts are in the currency of the source (currency in each entry). condition is present for Binderim sales that are tied to a card condition.
  • Converting. With currency=GBP (Pro and Business) each entry also gets converted: the amount, the rate and the date of the exchange rate. If we have no fresh exchange rate, converted is left out; the original amount is always there.
  • Choosing a source. source=tcgplayer narrows prices to that source. pricecharting is not a source name you can ask for; the raw price is general. It only appears as source inside grades.
  • 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_from tells you which id you asked for. In a batch the result has status: merged and merged_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: deleted in a batch).
  • An id that does not exist answers 404 card_not_found; a malformed id answers 400 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:

VariantMeaning
1st-editionFirst edition print
master-ball-reverseReverse holo with a Master Ball pattern
poke-ball-reverseReverse holo with a Poké Ball pattern
reverse-holoReverse 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:

GradeMeaning
psa10PSA 10
bgs10Beckett (BGS) 10
cgc10CGC 10
sgc10SGC 10
grade7Grade 7 from any grading company
grade8Grade 8 from any grading company
grade9Grade 9 from any grading company
grade95Grade 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.

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_ready with Retry-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:

FileEach line isNo line for
setsAn item of GET /v1/games/{game}/sets-
cardsAn item of GET /v1/sets/{id}/cards. grades and ai are not in it: they have their own files.-
pricesThe data of GET /v1/cards/{publicId}/prices: card_id, prices and gradesA 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/bulk and weekly .jsonl.gz files, bulk_not_ready error code and bulk_generated_utc in /v1/health.

Changes that could break a program need a new version (/v2); /v1 stays as it is.