Trust
Changelog
What shipped, on what day, and what we took back. Corrections are listed with the same weight as features: an API you are about to depend on is worth more if you can see where it was wrong.
Last entry · 33 entries · Atom feed
Card centering from a photo, with person-confirmed lines
Four routes with vision:read: POST /v1/centering/measure (25 credits), POST /v1/centering/confirm (5), POST /v1/centering/grade (1) and GET /v1/centering/standards (1). Growth and higher; Developer is excluded. After email confirmation, the Free trial includes 5 measure and 5 confirm calls, with separate counters from each other and Vision. Grade and standards use trial credits without separate call counters.
On real photos, measure normally returns HTTP 200 needs-review with ceilings: null: L/R and T/B ratios, uncertainty, detected corners and border lines for your app to display. A person checks or adjusts the lines, then your app calls confirm with confirmed_by: person. Confirm returns the centering-only ceilings for PSA, BGS, CGC and SGC with basis: client-confirmed-lines. This is a ceiling from centering alone, not a prediction of the final grade.
TypeScript and Python expose client.centering.measure, confirm, grade and standards (also async in Python); Go exposes c.Centering.Measure, Confirm, Grade and Standards. MCP adds ptcg_measure_centering, ptcg_grade_centering and ptcg_get_centering_standards. There is no MCP confirm tool: an assistant cannot attest that a person reviewed the lines. The client application collects that review.
Plan guidance and warnings before an allowance is spent
Commercial errors can include details.upgrade with a recommended plan, its monthly catalogue price and a subscribe link. Existing subscriptions use the billing portal, and email verification stays the first step when it unlocks free capacity. No prices, limits, error codes or HTTP statuses change.
GET /v1/me adds upgrade_suggestion and usage_warnings. Trial guidance starts at 50 credits, 50 cards or seven days before expiry; paid guidance starts at 80% of a finite credit or card allowance. Successful metered responses expose X-Cards-Warning through CORS at 80% of the card count before the response.
Lists priced per 50 rows, a distinct-card allowance, signed image URLs
Card and sealed-product lists (GET /v1/cards, GET /v1/sets/{code}/cards, GET /v1/sealed) now cost 1 credit per 50 rows requested, rounded up: the default page of 50 still costs 1, a page of 250 costs 5. include=index on a list costs the same as the bare list; include=prices stays at 4 per 25. Set, series, artist and change lists stay at 1 per page. An include never makes a request cheaper than the same request without it, which also corrects a batch of more than 25 ids with include=index, previously charged 1 instead of 2.
A distinct-card allowance now applies below Growth: 1,000 different cards for the whole trial, 30,000 a month on Developer, no limit from Growth up. Only the card routes count. The page that crosses the allowance is served in full, cards already retrieved keep answering, and a request that would add new cards answers 403 CARD_ALLOWANCE_EXCEEDED, uncharged, with details.cards_used, details.card_allowance, details.new_cards and the owner handoff in details.next_step. Every metered answer on a plan with an allowance carries X-Cards-Limit and X-Cards-Used; GET /v1/me reports quota.card_allowance and quota.cards_used, and GET /v1/billing/plans lists card_allowance per plan. A full catalogue import is a Growth use case; the quickstart, migration guide and pricing pages say so.
Image URLs are now signed: images come from img.pokemontcgapi.com/media/... with an expiry (e) and a signature (s), stay valid for at least a week, and answer 403 IMAGE_URL_INVALID without a valid signature. Store the card id and re-read the URL from the API rather than storing or building image URLs. Because the signature rotates weekly, the ETag of a response carrying image URLs changes once a week.
The SDKs carry the two allowance headers in the last response: @pokemontcgapi/sdk 0.1.7 (cardsLimit, cardsUsed), pokemontcgapi 0.1.2 on PyPI (cards_limit, cards_used) and the Go SDK v0.1.1 (CardsLimit, CardsUsed), null where the plan has no allowance. The same releases add include=index and include=prices to the vision call.
Cardmarket cadence and change-feed page limit clarified
The Cardmarket source publishes a daily point for each covered card, carrying the latest known value. Its card figures are re-read daily for Western cards and about every 48 hours for Japanese and Chinese cards. The minimum 24-hour publication delay is unchanged. The price methodology, attribution, API reference and assistant-readable documentation now distinguish those facts.
The GET /v1/changes reference now states its actual default of 250 rows and maximum of 1,000 rows per page.
Grade bands without a named grader are graded rows
PriceCharting publishes grade bands such as "grade 9" that apply to any grader: the row carries a score and no company. The API recognised a graded row by its company alone, so plans without graded prices received those medians as if they were raw prices, with grading set to null and the score dropped. A graded row is now any row with a company or a score. Those rows are withheld from plans without graded prices on /v1/cards/{id}/prices, /v1/prices/current, include=prices and the price history, and on the plans that include them they come back as grading: {company: null, score}. The composite index and price stats never used them.
grading.company is therefore nullable. @pokemontcgapi/sdk 0.1.6 and pokemontcgapi 0.1.1 on PyPI type it as nullable; the Go SDK keeps it a string, empty for a grade band. @pokemontcgapi/mcp 0.1.8 prints such a row as "any grader 9.5". Recognise a graded row by grading being non-null, never by the company.
@pokemontcgapi/sdk 0.1.6 also ships two changes already in the Python and Go clients: lastResponse.errorCode reads the X-Error-Code header, and the types of sets.cards and artists.list no longer offer set and select, which the API rejects with a 400 before charging credits.
Python and Go SDKs
Two new clients, ports of the TypeScript SDK: pokemontcgapi on PyPI (sync and async over httpx, Python 3.10 or newer) and github.com/pokemontcgapi/sdk-go (standard library only, a context on every call). Each wraps every data route with the same resources, the same error taxonomy, retries with jitter that honour Retry-After and never retry a quota exhaustion, pagination that follows links.next verbatim, an opt-in ETag cache and the next_step hand-off on commercial refusals.
The /sdk page shows the three clients side by side with an endpoint-to-method table per language; llms.txt lists all three. Account and billing routes remain outside the SDKs.
Set searches recognise verified market names
GET /v1/sets keeps every nonempty ordinary search unchanged. An empty first page retries a verified exact market alias, then all distinct words in any order within a stored set name or alias. Region and series filters, sort order, pagination and the one-credit catalogue cost remain the same.
SET_ALIAS_MATCHED and SET_WORDS_MATCHED identify a successful retry. SET_ALIAS_ELSEWHERE points from a nonempty ordinary result to a filtered market-name match outside that page without adding rows. The cursor retains fallback mode; lang still changes display only, not which names q searches.
Stable order for quotes with the same sorting keys
Quotes that share the same sorting keys now have a stable order: a declared condition comes before an undeclared one, then the more recently captured quote comes first. Previously their relative order was unspecified and could change between identical requests.
Quota balance on refusals and a machine-readable error header
X-Quota-Remaining now reports the actual period balance on rate and concurrency refusals instead of the full plan limit. Refunded responses, including overage and meter outage cases, report the balance without the attempted charge. Limits, costs, refunds and counters are unchanged.
Every Go API error response with error.code now carries X-Error-Code with the same value as the JSON body. Browser clients can read it through CORS; successful responses do not carry it. The operational /v1/health probe retains its separate status body.
Card-number searches handle leading zeros and suggest Pokédex numbers
GET /v1/cards and GET /v1/sets/{code}/cards keep exact collector-number results unchanged. Only an empty first page retries simple positive number terms after NFKC normalization and removal of leading zeros. Printed N/M values check the set’s printed_total when known; cursors retain the selected search mode.
Successful retries carry meta.hints with NUMBER_NORMALIZED. A still-empty number search can carry TRY_POKEDEX_NUMBER with a ready-to-copy nationalPokedexNumbers query, a capped match count, and context for older Japanese cards. Hints are absent on ordinary results, and meta.warnings remains a string array.
Blog: dated articles at /blog with an Atom feed at /blog.xml and Markdown twins for agents
The site has a blog at /blog. Every article carries its publication date and, when revised, the date of the revision; the index lists the newest first. An Atom feed at /blog.xml announces new articles with stable tag ids, the same way /changelog.xml does for release notes.
Each article also exists as Markdown at its own address plus /md, generated from the same content as the HTML page, and the articles are listed in llms.txt and appended to llms-full.txt, so an assistant reading the documentation finds them without parsing HTML. The first article is a migration checklist for card apps that must move off a sunsetting catalogue API before 1 March 2027.
SDK 0.1.5 and MCP 0.1.7 hand commercial refusals to the account owner
When a request is refused for a commercial reason (QUOTA_EXCEEDED, PLAN_REQUIRED, UPGRADE_REQUIRED or TRIAL_EXPIRED), the API answers with details.next_step: the action, a sentence written for the account owner, and the URL where they complete it. @pokemontcgapi/sdk 0.1.5 exposes it on every error as err.nextStep, err.handoff, err.actionUrl and err.checkoutUrl, with the NextStep type exported. The accessors return undefined when the block is absent or malformed, and the error classes, status and details are unchanged, so existing catch blocks keep working.
@pokemontcgapi/mcp 0.1.7 puts the handoff sentence on the first line of the tool result, followed by the API message and the complete details, and all eight tool descriptions tell the model to show that sentence and its URL to the user verbatim and not to retry. With eight tools carrying that rule, tools/list is about 11 KB.
Both packages now ship the signup instructions that keep one Idempotency-Key with the request body: the help printed when PTCG_API_KEY is missing, and the packaged READMEs, explain how the same request returns the original response, if stored, within 24 hours.
Signup examples keep the original Idempotency-Key for a lost response
Signup examples now keep the Idempotency-Key with the request body so the same request, from the same network, can return the original response, if stored, secret included, within 24 hours. A key rotated or revoked since then is not revived.
These are documentation and source changes. Published npm packages do not update automatically: the installed help and packaged READMEs carry these instructions from @pokemontcgapi/sdk 0.1.5 and @pokemontcgapi/mcp 0.1.7, released the same day.
SDK 0.1.4 wraps every data route; MCP 0.1.6 filters regions in the API
@pokemontcgapi/sdk 0.1.4 adds a method for every data route that did not have one: client.prices.card, current, history, stats, movers and sources; client.sealed.list, get and prices; client.series.list; and client.changes. After each call client.lastResponse exposes what the request cost in credits, the quota left, and the contents of X-Plan-Withheld. A 403 PLAN_REQUIRED now throws PlanRequiredError and a 403 TRIAL_EXPIRED throws TrialExpiredError, both subclasses of PermissionDeniedError, so existing catch blocks keep working.
@pokemontcgapi/mcp 0.1.6 passes the region argument to the API as a set.region filter instead of scanning pages on the client, which makes a Japanese or Chinese search one request. The card prices tool now reports what the plan withheld, so a model does not read a trial response as the whole market.
The 30th Celebration set code is 30c, and ids written with the set slug still resolve
The 30th Celebration set changed code upstream on 2026-09-17, from its slug to 30c. Its cards are now 30c-001, 30c-002 and so on. A card id written with the set slug in place of the set code keeps resolving: GET /v1/cards/30th-celebration-001 answers with the card whose id is 30c-001, on the single-card route and in GET /v1/cards/batch. The response always carries the canonical id.
The translation runs only on ids the normal lookup did not resolve. A prefix that is itself a set code stays canonical, so a canonical id that does not exist is still a 404.
Import guidance: walk the flat card list, not set by set
The quickstart, the pagination guide, llms.txt, the SDK page and both package READMEs now recommend GET /v1/cards?limit=250&orderBy=id followed by links.next for a first import. Pages fill across set boundaries, so the whole catalogue takes fewer requests, and fewer credits, than looping over every set. q=set.region:JP or q=set.region:CN narrows the walk to one print region; include=translations adds names at the plain catalogue cost. GET /v1/sets/{code}/cards remains the route for one particular set.
Batch ids bind to their set, and unresolved ids come back in missing[]
An id whose prefix is a real set code now resolves only inside that set. Before, a legacy alias could match first and return a card from a different set: sv8-116 could answer with a card that is not in sv8. This applies to GET /v1/cards/{id} and to GET /v1/cards/batch. An alias such as base1-4 still resolves to bs-4, because base1 is not a canonical set code.
GET /v1/cards/batch reports every id it could not resolve, once, in a top-level missing array of { "id": "..." }. Where an existing card carries that id as a legacy alias in a different set, the entry also has suggested_id. It is a suggestion and never a substitution. requested and found count ids sent and cards returned; missing is absent when everything resolved. Unresolved ids are billed like resolved ones.
The changes feed stops at the first gap, so a mirror never skips a row
GET /v1/changes used to serve every committed row below the highest servable id. Two concurrent writers could commit out of order, so a row with a lower id could become visible after the cursor had already moved past it, and a mirror following the feed would never see it. The feed now stops at the first row that is still uncommitted and serves only the contiguous range below it. The held-back rows arrive on the next read. No cursor or response field changed.
A key from the terminal, SDK 0.1.3 and MCP 0.1.5, and a listing in the official MCP registry
POST /v1/accounts/free with an email address and an Idempotency-Key header returns a trial key in the signup response at data.key.secret. The quickstart, llms.txt and llms-full.txt now print the call, so a key no longer requires a browser. The trial starts at 80 credits with a 40 credit daily cap; confirming the address raises it to 800 and 400, for 30 days from signup.
@pokemontcgapi/sdk 0.1.3 and @pokemontcgapi/mcp 0.1.5 are on npm, with the batch missing and suggested_id fields typed and reported. The MCP server is listed in the official MCP registry at registry.modelcontextprotocol.io as com.pokemontcgapi/mcp. When PTCG_API_KEY is missing, the first error now names the variable to set in the client configuration and prints the call that creates a key.
Email notices for trial usage and expiry
Verified trial accounts can receive a recorded usage summary, a notice when trial credits are used up, or a reminder before the trial expires. Figures and dates come from the account, and the credit limit notice uses the current monthly price list.
These messages come from Riccardo at the RareBit team. Reply to ask for technical help or reply "no" to stop trial emails. Verification and account recovery emails remain available.
Clear trial limits and a way back from expired verification links
The free API key page now states that the full catalogue and current prices are available on every plan, while photo recognition starts with Growth. Trial prices cover ungraded quotes in English.
The registration confirmation shows your current credit limit and daily cap next to your key, plus the limits unlocked by email verification. Check your inbox for the verification link.
Missing or expired verification links now lead to sign in, with instructions to choose "send the link again" in your profile. The API error for an invalid verification link gives the same recovery path.
Private offers: a discount attached to your account is applied automatically at checkout
Eligible accounts can see their private offer on the account page and choose a matching plan from pricing. The discount is already applied at checkout, with no code to enter. Active offers are also included in GET /v1/me as offers, with the eligible plan, billing interval, coupon and expiry; the field is omitted when no offers are open.
Yearly subscriptions renew their credits every month
Yearly plans grant the monthly credits every month for twelve months. The first credit window starts at the invoice period start, and subsequent windows renew monthly within the paid year. Previously, one monthly allowance lasted for the entire year. Monthly subscriptions are unchanged.
Set totals count cards in the catalogue, excluding variants
The total on set details and lists now counts cards present in the catalogue, including secret rares but excluding variants such as reverse holos; sets without cards return null. Upstream totals counted reverse holos separately in several Japanese sets, making complete card lists look incomplete: m2a declared 486 for 250 cards. printed_total is unchanged.
The trial ends 30 days after signup, and its daily cap is 400
The trial is still 800 credits granted once, and from today it also has an end: 30 days from signup. After that date every credited route answers 403 TRIAL_EXPIRED — not 429, because no Retry-After reopens a trial — with the date in the message and in details.trial_expired_at. The zero-cost routes keep answering, so an expired key can still read GET /v1/me and open POST /v1/billing/checkout; subscribing clears the expiry on the same key, with no rotation. Accounts that were already on the trial received 30 days from today, not from the day they signed up, so nobody lost access at the moment of the release.
The end date is published from the first minute rather than discovered at the first 403: every response on a trial key carries X-Trial-Expires-At in RFC 3339 (exposed to browsers over CORS), the signup and register responses carry quota.trial_expires_at and say the length in quota.note, GET /v1/me carries quota.trial_expires_at and quota.trial_expired, and the verification email states the 30 days. Paid plans carry none of these.
The daily cap on the trial goes from 200 to 400 credits. The total does not change — it is the same 800 — but the whole catalogue, about 210 credits, now fits inside one day instead of needing two, which is what the requests for a top-up were about. An unverified account keeps a tenth of both figures, 80 credits and 40 a day, until the address is confirmed.
The composite index leaves the bare list row: include=index
GET /v1/cards, GET /v1/sets/{code}/cards, GET /v1/cards/batch and GET /v1/sealed no longer carry index_eur and last_price_at on their rows unless you ask with include=index, which costs 1 credit per 50 rows requested, rounded up: a page of 250 is 5 credits. A select that names either field without the include gets rows without them and meta.withheld: ["index"], so the published SDK and MCP server keep working; an orderBy on them without the include is 400 INVALID_PARAMETER with the fix in the message. include=prices still carries the index and still costs 4 per 25. A single card or sealed product carries its index whatever you include, at 1 credit as before. Until today every row of every list carried the index at the price of the row, so the whole catalogue with its market value cost 210 credits and fit inside the trial; measured on 10 September.
The website no longer publishes the catalogue. The set, card, illustrator and series pages showed, without a key, the values and the full price tables that the API sells — a set page listed every card with its euro value, a card page listed graded quotes — so they are gone, and their addresses redirect permanently to the documentation of the endpoint that answers the same question. The docs carry one real request and response per operation; the coverage page keeps the counts.
GET /v1/cards/{id}/prices/stats summarises the index series of the card's own print region when locale is not given — en for Western printings, ja, zh or ko for the others — and says which in meta.locale. It used to default to en for every card, so a card printed only in Japanese answered all nulls at 2 credits. GET /v1/sealed/{id}/prices carries the product's index instead of null. GET /v1/prices/movers answers from a one-hour cache, which takes it from three seconds to a few milliseconds.
A route your plan does not include — the movers feed and card recognition below Growth — answers 403 PLAN_REQUIRED before any credit is reserved; it used to answer 429 first if you were at your daily cap. X-Credits-Cost reads 0 on every refunded answer instead of the cost that was attempted. RateLimit-Limit is now the bucket capacity and a new RateLimit-Policy header states the pace, so RateLimit-Remaining can no longer exceed RateLimit-Limit. If the usage meter is unreachable, the trial answers 503 METER_UNAVAILABLE with a Retry-After rather than being served uncounted; paid plans are still served, with X-RateLimit-Degraded: 1. GET /v1/artists/{slug} carries links.cards, the search for that illustrator's cards. Two messages were wrong: the movers 403 named a plan that does not exist, and the limit error offered "snapshot exports" that do not exist either.
include=prices is priced and plan-filtered like the price routes
GET /v1/cards, GET /v1/sets/{code}/cards and GET /v1/cards/batch with include=prices now cost 4 credits per 25 cards requested, rounded up — the same rate as GET /v1/prices/current — and GET /v1/cards/{id}?include=prices costs 2, the same as GET /v1/cards/{id}/prices. Without include=prices nothing changes: a page of 250 cards is still one credit. Until today the list cost one credit whatever it carried, so a full snapshot of every price row in the catalogue cost 210 credits and fit inside the trial.
The plan filter that the price routes have always applied now applies to include=prices as well: below Growth, graded quotes and non-English locales are not in the rows. Lists and batches say so in meta.withheld (withheld on the batch envelope); a single card says so in the X-Plan-Withheld header. Responses that carry prices are Cache-Control: private, max-age=300 from today, because their rows depend on the key — and so are the price routes themselves, which answered public until now while already filtering by plan.
Creating an account, signing in and recovering a password from the website share nothing with other visitors any more. The site called the API through its public address, so every visitor reached the sign-up limiter with the server's own IP and the ten-accounts-a-week ceiling applied to the whole site at once; it was reached on 10 September. The site now talks to the API over the internal network and passes the visitor's address, and the counter was reset. A sign-up that fails — an address already registered, a database that does not answer — no longer uses up one of the three daily attempts an address gets.
Cards carry their game text
attacks, abilities, weaknesses, resistances, subtypes, retreat_cost, converted_retreat_cost, rules, flavor_text, evolves_from and evolves_to carry rows for the first time. On 27 August we published that these keys existed and were empty, and positioned the catalogue accordingly; that is no longer the shape of the data. The Base Set Charizard answers with Energy Burn and Fire Spin.
The text is English, so it lands on the 20,725 Western printings and not on the Japanese and Chinese ones, which are the larger half of the catalogue. Counted whole rather than sampled, on 3 September: attacks on 17,207 cards — 33% of the catalogue, 83% of the Western part — subtypes on 20,082, types on 22,363, national_pokedex_numbers on 21,738, weaknesses on 16,124, flavor_text on 10,284, abilities on 4,042, rules on 2,951. Every field publishes its own rate on the card object page, because a share of the whole catalogue and a share of the cards a field can apply to are different claims.
A null attacks now means we do not hold the text for that printing, never that the card has no attack. legalities and level are still empty for every card, and deck legality remains a question this API cannot answer.
The text comes from the TCGdex open database, MIT licensed, matched onto cards our own catalogue already carried; the attribution page carries their copyright notice. No price figure comes from them. 20,330 cards changed in one pass, so a mirror following GET /v1/changes saw a single large batch that day rather than a drift.
New plan ceilings, and the market data moves to Growth
The monthly ceilings come down to the shape of the market: Developer answers 50,000 credits a month, Growth 200,000, Professional 500,000. The trial is unchanged at 800 credits granted once. Rate limits, concurrency and key counts are untouched — what changed is the monthly total, which on the old scale was large enough that no plan could be told apart from another.
The line between the plans is now drawn in one place rather than spread thinly. Every plan keeps the whole catalogue: every card, every set, every image, names in six languages, search, the changes feed and today’s price. What starts at Growth is the market depth — graded quotes by company and score, the complete price series, the movers feed, and recognising a card from a photograph.
POST /v1/vision/identify is the endpoint affected most, and it now answers 403 with the code PLAN_REQUIRED below Growth, with details.min_plan naming the plan it needs. The check runs before the upload is read, so a client on the wrong plan finds out in one round trip instead of after sending ten megabytes. GET /v1/prices/movers moves from Developer to Growth in the same release.
If you are on Developer today: graded rows, the movers feed and price history beyond 30 days stop appearing in your responses. Nothing errors — the rows are simply not there — so if your product requires them, the upgrade is a single change of plan and takes effect within a minute. GET /v1/billing/plans and GET /v1/me both carry a new vision boolean so you can check entitlement in code rather than by trying a call.
Sealed products, the price routes and the change feed are live
GET /v1/sealed, GET /v1/sealed/{id} and GET /v1/sealed/{id}/prices serve 2,088 sealed products — booster boxes, elite trainer boxes, tins, blisters, collections and eighteen other kinds — with names in six locales, images, the set they belong to, and price observations that reach back to November 2024. The id of a sealed product is its sku. GET /v1/status now reports sealed: 2,088 where it reported 0; the 237 products of other card games that share the source catalogue are filtered out.
The price routes documented as planned on 2026-08-27 answer today: GET /v1/cards/{id}/prices, /prices/history, /prices/stats, GET /v1/prices/current for up to 50 cards, GET /v1/prices/movers from the Developer plan up, and GET /v1/prices/sources with the allow-list in the open. The composite index series starts on 2026-09-02, so stats and movers fill in from that day; marketplace observations run from 2026-07-28.
GET /v1/changes is live: an append-only feed written by database triggers, with monotonic ids, meta.watermark and meta.oldest_available. GET /v1/series lists the seventeen blocks. GET /v1/reference replaces the four vocabulary lists that were documented and never routed by this service.
Cardmarket is served: the CARDMARKET source — lowest price, 1/7/30-day averages and trend, in six printing languages, with history back to November 2024 — joins the allow-list with the same 24-hour delay as the other marketplace sources, on cards and on sealed products alike. Price rows carry the amount under the key amount, and name their source as CARDMARKET, TCGPLAYER, PRICECHARTING, COMMUNITY or PTCG_INDEX. The TCGplayer rows answer to the shorter code TCGPLAYER from today, and carry the provenance string "TCGplayer"; the longer code they used until this release is still accepted as a ?source= filter, so an existing query keeps working. The documentation used an older key and older source names; every sample on the site was regenerated from live responses.
The catalogue moved to a single Postgres cluster shared with its source database, and the API now serves from that cluster directly. Email verification for trial accounts is live: verifying raises the trial quota from the unverified allowance to 800 credits and makes the key recoverable from the address.
The site now claims only what the API answers
Nine endpoint families were documented here as available and are not routed by the running service: the two card price sub-resources, the change feed, bulk export, the reference lists, the series index, the price collections, account usage and image recognition. They are marked planned everywhere they appear. They stay on the plan; none of them answers a request today.
The second response shape published under a /v2 prefix has been withdrawn from the documentation. The service answers on one prefix, /v1. Describing a second envelope was describing something that does not exist, which is worse than describing nothing.
Catalogue figures were re-measured against GET /v1/status before publishing: 615 sets, 52,337 cards, 0 sealed products. Every count on the site now names the endpoint it was read from and the day it was read.
Availability and latency percentages were removed rather than restated. We run no external probe that produces them, and a figure with no measurement behind it is worth less to you than no figure at all. They come back when a probe exists and publishes its own history.
The catalogue is now described by what it holds. Game text — attacks, abilities, subtypes, weaknesses, resistances, retreat cost, flavour text — exists as keys on the card object and is empty across the catalogue: measured over 500 cards drawn from five Western sets it came back 0% populated, while types and national Pokedex numbers appeared on roughly half of the Scarlet & Violet cards in that sample and on none of the older ones. The site no longer implies otherwise. What is positioned instead is what is actually there: the catalogue, six locales, three print regions, images, artist credits, and prices that carry their own provenance.
The catalogue moved to a dedicated host, and grew to 615 sets
The API moved onto dedicated hardware and now reads its catalogue from a replica kept in step by a nightly sync. GET /v1/status reports both ingest jobs — the price refresh and the catalogue sync — with the timestamp of their last success and its age in hours, so the freshness of what you are reading is one request away instead of a claim on a marketing page.
The catalogue went from the 203 sets published at launch to 615 sets and 52,337 cards. By print region that is 176 Western sets, 379 Japanese and 60 Simplified Chinese.
The span of the catalogue is thirty years of print. The oldest set carried is Expansion Pack, released 1996-10-20 in Japan; the newest already in the database is 30th Celebration, dated 2026-09-16.
First public release
/v1 opens: cards, sets, artists, rarities, types and supertypes, over a single envelope of {data, meta, links}. Collections page with an opaque keyset cursor, and every sort ends on id as a tiebreaker, so paging never repeats a row and never drops one.
Failures are one shape — {error: {code, message, details, request_id}} — with a stable code per condition, so a client can branch on the code instead of matching the message. Every response carries a strong ETag and an x-request-id.
Card records carry names in six locales (en, ja, fr, de, es, it) with set code, series, rarity, hp, artist credit and images. Ids are stable, and each card also keeps a legacy_id, the older-style identifier for the same card: GET /v1/cards/base1-4 resolves to the card whose id is bs-4 and whose legacy_id is base1-4, so an existing lookup table keeps working.
Prices arrive on a card through ?include=prices. Every row states its own source, variant, basis, price, currency, locale, condition, printing, grading, capture date, sample size and provenance string, and the card carries a composite index_eur alongside last_price_at.
The set index on this site is generated from the live catalogue — 203 sets at that point — and the price methodology and attribution pages went out the same day as the endpoints, because a price you cannot trace is a price you cannot build a business on.
How this page is kept
Entries record changes we can point at: a commit, or a response you can fetch yourself. Nothing is announced here before it answers a request, and a figure published in an entry names the endpoint it was read from. The same list is served as an Atom feed at /changelog.xml, so a release does not have to be discovered by revisiting a page.