# Migrating a Pokémon TCG app before 1 March 2027: a checklist

> A field-by-field checklist for moving a card app off a sunsetting catalogue API: IDs, pagination, prices, images, incremental sync and budget.

- **Published**: 2026-09-23
- **Updated**: 2026-09-23
- **Tags**: migration

Source: https://pokemontcgapi.com/blog/migration-checklist-before-march-2027

A great many hobby projects and a fair number of production apps read their Pokémon card data from one catalogue API that stops serving on 1 March 2027 and no longer accepts new registrations. The dated announcement is quoted in full, with a link to the source, at the top of our [migration guide](https://pokemontcgapi.com/docs/migrate-from-pokemontcg-io); read it there rather than taking our word for it. That guide is the field-by-field reference. This post is the checklist: the same work, in the order that avoids doing any of it twice.

A word on where you would be moving to. This API is built in Europe, and it shows in the data: Cardmarket quotes in EUR sit on every card next to TCGplayer quotes in USD, and the Japanese and Simplified Chinese print lines are catalogued next to the Western one, with their own set codes and card numbers. The live counts per print region are on the [coverage page](https://pokemontcgapi.com/coverage), read from the API when you open it. pokemontcgapi.com is an independent service and is not affiliated with, endorsed by or a reseller of Cardmarket or TCGplayer; each marketplace appears here as the stated source of a price row, the way a newspaper names the exchange a quote came from.

## 1. Inventory what you actually read

Before touching a single URL, grep your code for the fields you read from the old response: ids, names, numbers, images, prices, legalities, set metadata. Most apps use fewer than ten. Write them down in a file next to the adapter. That list is the migration; everything below is a row on it, and any field not on it is work you do not have to do.

Be honest about the fields you read but never display. A price block that is parsed and stored but not shown anywhere is still a dependency, and it is the kind that breaks silently when the shape changes underneath it.

## 2. Resolve IDs, do not rewrite them

Our card object has an `id` and a nullable `legacy_id`. The single-card lookup and the batch lookup both accept a historical alias when its prefix is not itself a canonical set code, so a stored `base1-4` resolves to the card whose canonical id is `bs-4`. An alias is a recorded relationship, not a string-rewriting rule: do not generate new ids by replacing a prefix, because there is no guaranteed mapping for every id in an existing dataset.

```bash
# Set PTCG_API_KEY to your own key before running these examples.
curl --fail-with-body \
  'https://api.pokemontcgapi.com/v1/cards/base1-4' \
  --header "X-Api-Key: ${PTCG_API_KEY}"
```

The lookup costs 1 credit and answers with the canonical `id` in the body. Keep the original provider id beside the canonical one in your own table; it lets you audit any match later without changing what the original record meant. Put every record the lookup did not resolve into a review queue, then compare set, collector number, print region and image before linking it by hand. A name alone is not enough: the same Pikachu has been printed dozens of times.

## 3. Replace page numbers with cursors

Collections return a `{data, meta, links}` envelope and page with `limit` plus an opaque `cursor`. Follow the complete `links.next` URL until it is absent; `meta.has_more` tells you whether another page exists. A saved page number does not translate into a cursor, so any bookmarked position from the old API is simply discarded and the walk starts again.

For the first full import, do not loop over sets. Start at `GET /v1/cards?limit=250&orderBy=id` and follow `links.next`: pages fill across set boundaries, so the whole catalogue takes fewer requests than a loop that opens a new walk for every set. Add `include=translations` if you need names in other languages; it keeps the plain catalogue cost. The [pagination guide](https://pokemontcgapi.com/docs/pagination) has the protocol in full.

## 4. Decide what a price means before you show it

This is the step people skip, and the one that produces the most support tickets. Every quote here carries its `source`, its `basis` (an asking price, a published guide value, or a figure we derived), its `currency`, `condition`, `printing` and `as_of` date. EUR quotes come from Cardmarket and USD quotes from TCGplayer; both can be present on the same card. The composite `index_eur` is a separate, nullable field with its own [methodology](https://pokemontcgapi.com/price-methodology), and it is not a renamed lowest offer or last sale.

```bash
curl --fail-with-body \
  'https://api.pokemontcgapi.com/v1/cards/base1-4/prices' \
  --header "X-Api-Key: ${PTCG_API_KEY}"
```

The call costs 2 credits and separates `data.index` from `data.quotes`. Pick the dimensions your app requires, and pick them once: an EUR asking price and a USD guide value are two different facts, and averaging across currencies or across bases produces a number that means nothing. Show the currency and the `as_of` date next to the value. An empty quote array or a null index means no value for that response, not a value of zero.

## 5. Keep images and set metadata explicit

Relations are omitted unless you ask for them. `include=images,set` on a card request adds an `images[]` array, where each entry has a `url`, a `face`, a `size` and a `locale`; there is no fixed small/large pair, so choose the entry by size rather than by position. The embedded set carries `code`, `slug`, a nullable `legacy_id`, and the `logo_url` and `symbol_url` fields, which are nullable too.

- Pick the image size in code, by the `size` field, and fall back to the next size down when the one you want is absent.
- Keep a placeholder for a set without a logo or symbol. Some sets in the Japanese and Chinese lines never had one.
- Resolve a set by `code`; a set alias such as `base1` resolves to the same record, and the returned `code` is the canonical one.

## 6. Sync incrementally after the first import

`GET /v1/changes` is an append-only log of catalogue changes with monotonic ids, and each page costs 1 credit, an empty page included. The trap is the order of operations: capture the watermark BEFORE the import, not after, or every change that lands while the import is running is lost.

1. Read the feed once with `since=0&limit=1` and store `meta.watermark`. That is the newest change id at the moment your import begins.
2. Run the full import from step 3.
3. Read the feed from the stored watermark, apply each page, and fetch the affected records. Commit `meta.next_since` as your new position only after those fetches succeed; a position committed before the fetch is a change you will never see again.
4. Poll on a schedule. A position older than what the feed retains answers 410 `SYNC_CURSOR_TOO_OLD`, and the honest answer to that is a fresh import with a new watermark.

## 7. Budget in credits, not requests

Every route has a cost in credits, and the cost of a list depends on what you include, not on how many rows come back. The routes in this checklist cost the following; `ceil` means round up, and list pricing uses the requested `limit`.

| Route | Request shape | Credits |
| --- | --- | --- |
| `GET /v1/cards/{id}` | Plain catalogue, images and set included | 1 |
| `GET /v1/cards` | Plain catalogue, any `limit` up to 250 | 1 |
| `GET /v1/cards` | `include=index` | `ceil(limit / 50)` |
| `GET /v1/cards` | `include=prices` | `4 × ceil(limit / 25)` |
| `GET /v1/cards/{id}/prices` | Current quotes | 2 |
| `GET /v1/sets`, `GET /v1/sets/{code}` | Catalogue | 1 |
| `GET /v1/sealed`, `GET /v1/sealed/{id}` | Plain catalogue | 1 |
| `GET /v1/changes` | A page of events | 1 |

Two consequences. A full catalogue import at 250 cards a page costs one credit per page, so the page count is the budget, and adding `include=index` to every page multiplies it by five. The plans and their monthly credits are on the [pricing page](https://pokemontcgapi.com/pricing); the numbers there are the same ones the API applies.

## 8. Run both side by side for a week

Do not flip traffic on the day the adapter compiles. Take a sample of two hundred cards that your users actually open, read them from both APIs, and diff names, numbers and images. For prices, compare the meaning rather than the value: a EUR asking price will not equal a USD market figure, and it should not.

While the two run in parallel, log the `X-Credits-Cost` header on every response and watch the `RateLimit-*` family; together they tell you what the migration will cost in production before it costs anything. Then flip, and keep the old identifiers and the old observations until the new mapping has survived a week of real traffic.

## Timeline

A [trial key](https://pokemontcgapi.com/free-api-key) needs no card and is granted once: 800 credits over 30 days, enough for the inventory, the id resolution and a test import. Leave at least a month of parallel running before the deadline, which puts the start of the work no later than January 2027 for a small app and earlier for anything with a sync worker.

> **Where to go next**
>
> The [quickstart](https://pokemontcgapi.com/docs/quickstart) gets a key into a first request in a few minutes, and the [migration guide](https://pokemontcgapi.com/docs/migrate-from-pokemontcg-io) has the field-by-field table this checklist summarises.
