What a card scanner API does
A card scanner API takes a photo of a trading card and returns the catalog products it most likely shows. Your app can then look up the set, card number, artwork and price without the user typing anything.
The TCGAPIs card recognition endpoint works this way. You send one image and the game the card belongs to. It returns a ranked list of candidate product IDs, each with the card name, set, card number, rarity and catalog image. It does not return prices; a price is a separate request using the product ID you accept.
This guide walks through the full flow: choose the game, upload the photo, decide whether to accept the top match, then fetch the price.
Before you start
- Create a free account. It includes 100 shared credits; one successful recognition uses one credit, and a price lookup uses another.
- Keep your API key on your server. Do not ship it inside a mobile app or browser bundle; have the app upload the photo to your own backend, which then calls the API.
- You can try recognition without an account in the public demo, which allows 10 recognitions per IP address a day.
Step 1: find the value for the game
Every request should name the game. It selects the right recognition model, and from 1 December 2026 a request without it is rejected. The accepted values come from a public endpoint:
const response = await fetch('https://api.tcgapis.com/api/v1/recognize/games');
const body = await response.json();
if (!response.ok || !body.success) throw new Error(body.error || 'Could not load games');
for (const game of body.games) {
console.log(game.name, game.categoryId, game.aliases, game.cards);
}Each entry has a name, a categoryId, a list of aliases and the number of cards the model knows. You can send the name, the category ID or an alias. The endpoint listed 55 games when this article was written; read the list at run time instead of hard-coding it.
Step 2: upload the photo
Send the image as multipart/form-data in a field called image. JPEG, PNG, GIF and WebP are accepted, up to 10 MB. This example runs on Node 18 or later:
import { readFile } from 'node:fs/promises';
const photo = await readFile('./card.jpg');
const form = new FormData();
form.append('image', new Blob([photo], { type: 'image/jpeg' }), 'card.jpg');
form.append('game', 'Pokemon');
form.append('limit', '5'); // optional: default 10, maximum 50
const response = await fetch('https://api.tcgapis.com/api/v1/recognize', {
method: 'POST',
headers: { 'x-api-key': process.env.TCGAPIS_KEY },
body: form,
});
const body = await response.json();
if (!response.ok || !body.success) throw new Error(body.error || 'Recognition failed');Do not set the Content-Type header yourself. fetch adds it with the correct multipart boundary.
Step 3: read the candidates
A successful response has a confident flag and a results array, best match first:
console.log(body.confident); // true when the top match is clear
for (const candidate of body.results) {
const card = candidate.product;
console.log(candidate.score, candidate.productId, card.name, card.expansionName, card.number, card.rarity);
}Two rules keep a scanner honest:
- Accept the top match automatically only when
confidentis true. When it is false, show the first few candidates with their images and let the user pick, or ask for a new photo. - Treat
scoreas a ranking, not a percentage. A score of 90 does not mean a 90% chance of being right.
Printings are where scanners go wrong most often. Two cards can share the same artwork and differ only in set symbol, card number or foil treatment. If your app cares about the exact printing, show the set and card number and let the user confirm.
Step 4: fetch the price for the accepted card
Use the productId you accepted with the pricing endpoint:
const headers = { 'x-api-key': process.env.TCGAPIS_KEY };
const productId = body.results[0].productId; // only after the checks above
const priceResponse = await fetch('https://api.tcgapis.com/api/v2/prices/' + productId, { headers });
const price = await priceResponse.json();
if (!priceResponse.ok || !price.success) throw new Error(price.error || 'Price lookup failed');
console.log(price.data.prices); // keyed by printing, such as Normal or Foil
console.log(price.data.last_updated ?? null);These are TCGPlayer prices in US dollars. A missing or null field means the value is unavailable, not zero. For European prices in euros, see the Cardmarket price API; a recognised product needs a verified Cardmarket cross-reference before that lookup.
Getting better matches
- Photograph one card, face up, filling most of the frame.
- Use even light. Glare across a foil card hides the detail the model needs.
- Take the card out of a thick or coloured sleeve if you can.
- Hold the camera square to the card; steep angles distort the artwork.
- Send the correct game. Pokemon and Pokemon Japan, for example, are separate entries in the games list.
Test with the photos your own users will take before you rely on automatic acceptance. We have not published a measured accuracy or speed benchmark, so measure both on your own workload.
Errors to handle
| Status | Meaning | What to do |
|---|---|---|
| 400 | No image, an unsupported file, or an unknown game | Fix the request; an unknown game lists the accepted names |
| 401 | Missing or invalid API key | Check the x-api-key header |
| 403 | Signup credits used up, or the plan does not include recognition | Check your balance or plan |
| 429 | Allowance reached | Wait for the next period or upgrade |
| 503 | The recognition service is busy or unavailable | Retry after a short wait |
Plans
Recognition is included on every paid plan, with a monthly recognition allowance that is separate from general API calls; Unlimited has no recognition cap. Product prices need Business or above once the signup credits are used. Current allowances are on the pricing page, and the full request and response reference is in the API documentation.
Get Your API Key
Explore catalog records across 60+ trading card games, with price, listing and sales-history data where available.
Start Building