Skip to content
pokemontcgapi.com
Documentation

Identify a card from a photo

Send a photograph, get back the cards it could be, ranked, with a distance you can threshold yourself.

POST/v1/vision/identifyThe most expensive call in the price list, and the only one that is not a row being read: it is the index of every catalogue image answering at once.growth plan

Cache-Control: private, no-store

Objects returned

A photograph goes in and a ranked list comes out. Never a single answer: reprints and regional twins share their artwork, so an exact call from the image alone is not available to us and would not be available to anyone. What the endpoint returns instead is what it actually knows — the candidates, how far each one is, and whether the top of the list is far enough clear of the rest to act on without asking a human.

Included from the Growth plan

This is the one route with a plan behind it as well as a scope. Below Growth it answers 403 with the code PLAN_REQUIRED and details.min_plan, before the upload is read — so a client on the wrong plan finds out in a round trip rather than after sending ten megabytes. It is also the most expensive call in the price list at 25 credits, because it is the index of every catalogue image answering rather than a row being read.

How it works

Every catalogue image is reduced to a 64-byte perceptual signature — a DCT signature of its low frequencies plus a gradient signature — and your photograph is reduced the same way. The comparison is a Hamming distance across the whole index, which is a linear scan and costs under a millisecond.

The photograph does not need to be a tight crop. Before hashing, the card is looked for as a quadrilateral in the frame and each candidate quadrilateral is straightened into the canonical portrait crop; those hypotheses compete with a set of concentric centre crops, and the one that actually matches the catalogue wins on distance. A quadrilateral that grabbed the table instead of the card lands about three times farther away and simply loses.

What the numbers mean

distance is 0 to 512 and is the number you should threshold on: real matches land well under 150 even on a noisy phone photo, and nothing above 170 is returned at all. confidence is the same information rescaled to 0–1 for a progress bar, and carries no extra evidence.

Reading the decision

The three outcomes
decisionmeanswhat to do
matchOne candidate, close, and clear of the next by a wide margin.Safe to act on.
ambiguousTwo or more candidates within a few bits of each other — almost always the same illustration printed twice.Show the candidates and let a person pick, or re-send with set or region.
no_matchNothing within range.Check meta.regions_detected: zero means the card was never isolated from the background, which is a framing problem rather than a catalogue one.

data.id is populated only on match. On ambiguous it is null on purpose: a client that reads it without looking at decision should break immediately and visibly rather than quietly hand over the wrong printing.

Body fields

Body fields
NameTypeDefaultDescription
image requiredfile | stringThe photograph. Three ways to send it: a multipart field named image, the raw bytes as the whole body with an image/* content type, or base64 (a data: URL is accepted) in a JSON field of this name. JPEG, PNG, WebP and GIF; 10 MB and 96 px per side are the bounds.
top_kinteger3How many candidates to return, 1 to 10. In query string when the body is the image itself.
setstringRestrict to one set code. This is the parameter that resolves a reprint: from artwork alone the original and its reprint are the same card, and if you know which pack it came out of, say so.
regionstringRestrict to a print region — WEST, JP or CN. Same purpose: a Japanese card and its Western twin share the illustration.

Headers

Headers
NameTypeDefaultDescription
X-Api-Key requiredstringYour API key. Authorization: Bearer <key> is accepted as an alias.

Example request

curl -s -X POST "https://api.pokemontcgapi.com/v1/vision/identify" \
  -H "X-Api-Key: $PTCG_API_KEY" \
  -F "[email protected]" \
  -F "top_k=3"

Example response

200 OK · Cache-Control: private, no-store

response.json
{
  "data": {
    "decision": "ambiguous",
    "id": null,
    "candidates": [
      {
        "id": "bs-4",
        "name": "Charizard",
        "number": "4",
        "set": { "code": "bs", "name": "Base Set", "print_region": "WEST" },
        "rarity": "Rare Holo",
        "image_url": "https://img.pokemontcgapi.com/cards/bs/4-normal.webp",
        "distance": 31,
        "confidence": 0.818
      },
      {
        "id": "b2-4",
        "name": "Charizard",
        "number": "4",
        "set": { "code": "b2", "name": "Base Set 2", "print_region": "WEST" },
        "rarity": "Rare Holo",
        "image_url": "https://img.pokemontcgapi.com/cards/b2/4-normal.webp",
        "distance": 38,
        "confidence": 0.776
      }
    ]
  },
  "meta": {
    "count": 2,
    "cards_indexed": 51983,
    "index_built_at": "2026-08-27T04:10:22Z",
    "signature_version": 1,
    "regions_detected": 2,
    "hypotheses_tried": 8,
    "elapsed_ms": 74
  }
}

Errors

Every error body carries error.code, error.message and error.request_id. Switch on the code, never on the message.

Errors returned by this endpoint
StatusCodeWhen
401MISSING_API_KEYNo X-Api-Key header and no bearer token on a route that requires one.
INSUFFICIENT_SCOPEINSUFFICIENT_SCOPE
403PLAN_REQUIREDThe route exists and the key is valid, but the plan does not include this feature. Today that is /v1/prices/movers on the trial.
400INVALID_PARAMETERA query parameter has the wrong type or an unsupported value — a non-integer limit, an unknown lang, an unknown region, a sort key that is not sortable.
415UNSUPPORTED_MEDIA_TYPEThe Content-Type is not one this route accepts.
413PAYLOAD_TOO_LARGEThe request body is larger than the server accepts.
QUOTA_EXCEEDEDQUOTA_EXCEEDED
503FEATURE_NOT_CONFIGUREDThe endpoint exists but its optional backend is not configured in this deployment.

The full taxonomy, with what to do about each code, is on the errors page.

Two candidates, one illustration

The example above is the normal case rather than an edge case. Base Set Charizard and its Base Set 2 reprint are the same illustration on the same layout; the seven bits between them are printing noise, not a difference the camera can see. Any scanner that picks one of the two silently is wrong about half the time, and it is wrong on precisely the cards people care most about.

The two ways out are both in your hands: pass set or region when your workflow knows them — someone inventorying a pack they just opened does — or present both and take one tap. What the API will not do is guess and call it certainty.

It answers 503 until the index is built

The signature index is built by an offline job that hashes every catalogue image. On a deployment where it has not run, this endpoint answers 503 FEATURE_NOT_CONFIGURED rather than no_match — because "we did not look" and "it is not in the catalogue" are opposite answers, and a client would cache the second one as a fact.

view this page as markdown