Card recognition
Point a camera at a card. Get the printing.
Not the card name — the printing. Which set, which number, which language, which finish, and what it is worth in both markets. Over a socket while the card is still under the lens, or as a single photo upload. Milliseconds either way, and you are never billed for a card we could not place.
- 38 ms
- Median match, server-side
- 8
- Games, every printing
- $0.0025
- Per scan at volume
- $0
- Charged for a miss
Live scanning
One connection, a whole intake session
The slow part of scanning a thousand cards has never been the matching. It is the thousand round trips: a connection opened, a TLS handshake paid for, a request queued and a spinner shown, once per card, while an operator waits with the next card in their hand.
So live mode is a socket. You open it once at the start of a session and push frames as fast as the camera produces them. Matches come back on the same connection, tagged with the frame they belong to, and land while the card is still under the lens. Frames that do not resolve are answered too, with the candidates and the reason, so your interface can say flatten it instead of freezing.
In practice that is the difference between four hundred cards an hour and eight hundred, with the same person doing the same work.
- 38 ms
- p50
- 74 ms
- p95
- 140 ms
- p99
Measured server-side, from the frame landing to the match leaving, at 1080p single-card frames. Your uplink is on top of that, which is why the socket exists: it pays TLS and connection setup once instead of on every card.
wss://api.tcggraph.com/v1/scanidle- 0
- Frames
- 0
- Matched
- 0
- Unbilled
A replayed session transcript using cards from the public demo catalog. Nothing is being inferred in your browser — it is here to show the shape and the cadence of what arrives on the socket, including the frame that does not resolve and is not billed.
const socket = new WebSocket("wss://api.tcggraph.com/v1/scan", [
"tcggraph.v1",
`bearer.${process.env.TCGGRAPH_KEY}`,
]);
socket.onopen = () =>
socket.send(
JSON.stringify({
type: "config",
games: ["pokemon", "magic-the-gathering"],
minConfidence: 0.92,
prices: ["cardmarket", "tcgplayer"],
}),
);
socket.onmessage = (event) => {
const frame = JSON.parse(event.data);
if (frame.type === "match") addToInventory(frame.matches[0]);
if (frame.type === "unresolved") showHint(frame.reason);
};
// Binary frames need no envelope. Push them straight off the camera.
setInterval(async () => socket.send(await captureJpeg()), 100);Three ways in
Stream it, post it, or send nine cards at once
The same match object comes back from all of them, so a batch importer and a live scanner share one parser.
Live scanning
WebSocketOpen one connection and send camera frames as fast as your device produces them. Matches come back on the same socket, tagged with the frame they belong to, so a card is identified while it is still under the lens and the operator never waits on a request. Frames that do not resolve are answered too — with the candidates and a reason — so your UI can say hold it flatter instead of freezing.
Best for Intake desks, buylist kiosks, phone cameras, anything with a continuous view of cards moving past.
Single photo
HTTPSA plain multipart upload, a public URL, or base64 in a JSON body. One request, one response, no polling and no job ids to chase. The response is the same shape the socket emits, so code written against one works against the other, and a batch importer and a live scanner can share a single parser.
Best for Photo uploads, server-side backfills, listing tools, anything where the image already exists.
Many cards, one frame
EitherOne image can contain many cards. Each is located, matched and returned with its own box and its own confidence, so a nine-pocket binder page becomes nine catalog records from one photo. You are billed per card matched rather than per image, and cards the model will not commit to are not billed at all.
Best for Binder and box digitisation, showcase shelves, collection onboarding.
The response
A match is the whole card, priced, in one round trip
Identification and pricing are one operation here. Everywhere else they are two metered calls, which means finding out what a scanned card is worth costs about double the sticker price.
curl -X POST "https://api.tcggraph.com/v1/scan" \
-H "Authorization: Bearer $TCGGRAPH_KEY" \
-F image=@front.jpg \
-F games=pokemon \
-F minConfidence=0.92{
"requestId": "scan_01K5Z8P4XQJ7YB3M",
"latencyMs": 41,
"matches": [
{
"confidence": 0.9962,
"billed": true,
"box": {
"x": 0.171,
"y": 0.064,
"w": 0.658,
"h": 0.872
},
"printing": {
"id": "pkm_sv3pt5_199",
"language": "en",
"finish": "holofoil",
"edition": "unlimited",
"collectorNumber": "199/165"
},
"card": {
"id": "pkm_sv3pt5_199",
"game": "pokemon",
"name": "Charizard ex",
"set": {
"code": "sv3pt5",
"name": "151"
},
"rarity": "Special Illustration Rare",
"prices": [
{
"source": "tcgplayer",
"region": "NA",
"currency": "USD",
"market": 370.48
},
{
"source": "cardmarket",
"region": "EU",
"currency": "EUR",
"market": 312.67,
"trend": 312.67,
"sellers": 86
}
]
},
"alternatives": []
}
],
"scans": {
"billed": 1,
"remaining": 5842
}
}{
"requestId": "scan_01K5Z8P51N4WQD0T",
"latencyMs": 29,
"matches": [],
"unresolved": [
{
"box": { "x": 0.19, "y": 0.07, "w": 0.63, "h": 0.86 },
"reason": "glare across set symbol",
"alternatives": [
{ "printingId": "pkm_sv3pt5_199", "confidence": 0.71 },
{ "printingId": "pkm_sv3pt5_223", "confidence": 0.68 }
]
}
],
"scans": { "billed": 0, "remaining": 5842 }
}matches[].confidenceFloat- 0 to 1. Above your threshold the match is returned and billed; below it, the frame comes back as a candidate list and costs nothing.
matches[].billedBoolean- Whether this match consumed a scan. Sum it if you want to reconcile your own counter against ours.
matches[].boxObject- Normalised x, y, width and height of the card inside the frame, for drawing an overlay without a second pass.
matches[].cardCard- The whole catalog record — the identical object /v1/cards returns, including images, legalities and game-specific fields.
matches[].card.prices[Price]- Cardmarket in EUR and TCGplayer in USD, on the match, in the same response. No second request and no second charge.
matches[].printingObject- Which physical printing was matched: language, finish, edition and collector number, not just the card name.
matches[].alternatives[Object]- Ranked runners-up with their confidences. Useful for a confirm step, and the only thing returned when nothing clears the threshold.
latencyMsInt- Server-side time from frame received to match emitted. Excludes network transit, so you can tell our latency from your connection's.
scansObject- Billed count for this call and the balance left on the period.
Why ours
Six things that are different, and one that matters most
The first one on this list is the reason people move. You should not pay for our uncertainty, or for your own bad lighting.
A miss costs nothing
You are billed for a card we will commit to, not for an image you sent. A blurred frame, a thumb over the set symbol, a card we genuinely cannot place — all free, and all answered with the candidates so your operator can fix the shot. Most providers charge on submission, which means you pay for your own bad lighting.
The price is already in the response
A match returns the full catalog record with Cardmarket in EUR and TCGplayer in USD attached. Elsewhere identification and pricing are separate metered operations, so the card you scanned costs you twice before you know what it is worth. Here it is one charge and one round trip.
It is a socket, not a queue
Cards are identified while they are still under the lens, on a connection that stays open across a whole intake session. No per-image handshake, no job id to poll, no spinner between cards. An operator working a stack never waits, which is the difference between four hundred cards an hour and eight hundred.
Printings, not just card names
Knowing a card is Charizard is worth very little. The match tells you which Charizard: which set, which collector number, which language, which finish, which edition. That is the field that decides whether the card is worth three dollars or three hundred, and it is the one most scanners quietly guess at.
Scans have their own meter
A scan never spends your data credits and a catalog lookup never spends your scans. A heavy intake morning cannot starve the repricer that runs at midnight, and you can read the two numbers separately when you are working out what your product actually costs to run.
Same object as the rest of the API
The card inside a match is byte-for-byte the object /v1/cards returns. Your existing parser, types and database columns already handle it, so adding a scanner is a new input to code you have written rather than a second integration to maintain.
Scan pricing
Priced per matched card, metered on its own
Recognition does not draw on your data credits and your data plan does not draw on your scans, so a heavy intake morning cannot starve the job that reprices your stock at midnight.
Recognition Starter
A collection app, a single intake desk, a listing tool finding its feet.
$29/mo
6,000
matched cards per month · $0.0048 each
- Overage
- $0.008 per scan
- Beats pay-as-you-go from
- 2,417 scans
- Unmatched frames
- Never billed
- Live socket and photo upload
- All 8 games, every printing
- Cardmarket EUR and TCGplayer USD on the match
- Multi-card frames
- 2 concurrent sockets
- Misses are never billed
Recognition Growth
PopularA shop running intake most days, or an app with real users scanning.
$89/mo
25,000
matched cards per month · $0.0036 each
- Overage
- $0.005 per scan
- Beats pay-as-you-go from
- 7,417 scans
- Unmatched frames
- Never billed
- Everything in Starter
- 10 concurrent sockets
- Condition hints on the match
- Slab label reading
- Session transcripts, 30 days
- Webhook on every session close
Recognition Scale
Bulk operations, marketplaces, and anyone pointing more than one camera at cards.
$249/mo
100,000
matched cards per month · $0.0025 each
- Overage
- $0.0035 per scan
- Beats pay-as-you-go from
- 20,750 scans
- Unmatched frames
- Never billed
- Everything in Growth
- 50 concurrent sockets
- Priority inference queue
- Regional endpoint pinning
- Private catalog matching
- 99.95% uptime SLA
Or scan without a plan
$0.012per matched card
No plan, no minimum, no monthly fee. Scans are drawn from the same prepaid balance your data plan sits on, at a flat rate, and stop when the balance runs out. Someone digitising one collection over a weekend should not have to sign up for a month, and at this rate the cheapest per-scan option elsewhere still costs about ten times more.
- No monthly fee and no minimum spend
- Drawn from the balance you already top up
- Still not billed for a miss
- Switch to a plan the moment it is cheaper — the calculator on this page tells you when
Overage costs more per scan than the plan it extends, and it stops once it reaches your plan price — so the worst a month can cost is double. If you pay overage twice running, the next plan up is cheaper. You can set a hard stop instead if you would rather fail than spend.
Work out your bill
Tell it your volume and it will argue against itself
Including the cases where no plan at all is the cheapest thing you can do. We would rather you paid us the right amount than signed up for the wrong tier and left.
5,000
Cheapest here
$29.00
Recognition Starter · $0.0058 per card
No plan
5,000 scans at $0.012
$60.00
Recognition StarterCheapest
6,000 included, 1,000 spare
$29.00
Recognition Growth
25,000 included, 20,000 spare
$89.00
Recognition Scale
100,000 included, 95,000 spare
$249
Pay-as-you-go is a flat $0.012 per matched card with no monthly fee. Plan rows include overage where the volume runs past the allowance, capped at the plan price. We show whichever is actually cheapest, including when that is not a plan.
The market
What everyone else charges for the same scan
Per-scan figures are each provider's plan price divided by the scans that plan includes, which is the number that lands on your invoice.
| Provider | Entry | Included | Per scan | Prices | Bills misses | Live stream |
|---|---|---|---|---|---|---|
| TCGGraph | $29 / mo | 6,000 scans | $0.0048 | Both markets | No | WebSocket |
| TCGGraph, no plan | None | Pay as you go | $0.012 | Both markets | No | WebSocket |
| Ximilar | €59 / mo | 100,000 credits | ≈ €0.0059 | No, +10 credits | Yes | No |
| NeoSatoshi | $29 / mo | 2,500 scans | ≈ $0.0116 | Cardmarket only | No | No |
| TCGAPIs | £99 / mo | 1,000 scans | ≈ £0.099 | Bundled | Not stated | No |
| CardGrader.AI | $49 pack | 400 credits | $0.1225 | Separate module | Yes | No |
Published list prices from each provider's own pricing page, checked in September 2026, converted to a per-scan figure by dividing the plan price by the scans it includes. Ximilar bills 10 credits to identify a trading card and 10 more to price it, so a scan that tells you what a card is worth is about €0.0118 there rather than €0.0059. Two providers charge whether or not the card is identified. None of them offer a persistent connection for continuous scanning.
Who buys it
What people actually point cameras at cards for
Bulk intake
Card shops, buylist desks, bulk buyers
A camera over the counter and one operator feeding cards. Each card is priced and in inventory before the next one lands, so a shoebox becomes a stock list in an afternoon instead of a week. The socket is the whole point here: at four hundred cards an hour, a round trip per card is most of your day.
Collection apps
Consumer apps, deck trackers, portfolio tools
Point the phone at a card and it is in the collection, with the right printing and today's price on both sides of the Atlantic. Onboarding a new user is the hardest part of a collection app, and typing set codes is where they quit.
Listing automation
Marketplace sellers, listing tools, repricers
A photo becomes a draft listing: title, set, number, language, finish, and a market price to anchor against. Because the match carries the catalog id, the listing links to a real product rather than a free-text guess that nobody can search for.
Binder digitisation
Collectors, estate sales, insurers
One photo of a nine-pocket page returns nine cards with boxes, so a binder is a spreadsheet in a few minutes. Billed per card matched, which makes it cheaper per card than photographing them one at a time.
Trade and event tooling
Tournament organisers, judges, trade nights
Check a physical deck against a submitted list, or confirm the cards in a trade are the printings that were agreed. Legality comes back on the card object, so a format check does not need a second lookup.
Grading prep
Submission services, graders, consignment
Identify the exact printing before a card goes in the queue, so the submission form is right the first time. On Growth and up the match carries condition hints and reads slab labels, which is enough to sort a raw pile from an already-graded one.
Per game
Every game is hard in a different way
Pokémon prints one illustration as three products. Yu-Gi-Oh! prints one card as nine rarities. Magic prints one card eighty times. A scanner that treats them alike returns the wrong printing at the wrong price, which is worse than returning nothing.
Pokémon Trading Card Game
The same artwork ships as three different products, and only one of them is worth anything.
Magic: The Gathering
Thirty years of reprints means one card name maps to eighty different products.
One Piece Card Game
The alternate art is worth ten times the base card and shares its number.
Yu-Gi-Oh! Trading Card Game
The card is not the product. The rarity is.
Disney Lorcana
Enchanted variants hide behind ordinary card numbers.
Star Wars: Unlimited
Hyperspace and showcase variants, plus leaders that are printed on both sides.
Digimon Card Game
Alternate art parallels with the same card number, across four languages.
Grand Archive
A small catalog, and almost all of its value in foil and alternate treatments.
FAQ
Questions about scanning
Turn a shoebox into a stock list
Recognition Starter is $29 a month for six thousand matched cards, or $0.012 a card with no plan at all. Either way, the frames we cannot place cost you nothing.
- No card on file
- No annual lock-in
- Misses never billed