Skip to content
Prepaid, no subscription trap. Top up a balance and we draw the monthly fee from it.
tcggraph

Card recognition

Point a camera at a card. Get the printing.

Not the card name — the printing. Which set, which number, which language, the finishes it exists in, and what it is worth in both markets. Over a socket while the card is still under the lens — in your hand, if that is where it is — or as a single photo upload. About half a second at the median on the socket, and you are never billed for a card we could not place.

500 ms
Median match, live socket
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.

500 ms
p50
2.0 s
p95
2.9 s
p99

The percentiles are the live socket's matched frames in production over 30 days to September 2026, from a frame arriving to its answer leaving, metering included. A single photo over HTTPS takes 1 to 1.5 seconds when it lands on an instance that has the index loaded, and 7 to 10 seconds when it lands on one that has not, because 174 MB of index and tiles, two models and the printing table load first; photos minutes apart often land on a fresh instance. The socket starts that load the moment it opens, before your camera has produced a frame, so a session pays it once. Your uplink is on top of every figure here.

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.

    A whole scanner
    // Mint a single-use ticket, so no key reaches client code.
    const { ticket } = await fetch("https://api.tcggraph.com/v1/scan/tickets", {
      method: "POST",
      headers: { authorization: `Bearer ${process.env.TCGGRAPH_KEY}` },
    }).then((r) => r.json());
    
    const socket = new WebSocket(`wss://api.tcggraph.com/v1/scan?ticket=${ticket}`);
    
    // Binary frames need no envelope: a JPEG straight off the camera,
    // 1280 px across at quality 70. One frame in flight at a time —
    // the next goes when the answer to the last arrives.
    const sendFrame = async () => socket.send(await captureJpeg());
    
    socket.onopen = () => {
      socket.send(
        JSON.stringify({
          type: "config",
          games: ["pokemon", "magic-the-gathering"],
          minConfidence: 0.92,
        }),
      );
      sendFrame();
    };
    
    socket.onmessage = (event) => {
      const frame = JSON.parse(event.data);
      if (frame.type === "match") addToInventory(frame.matches[0]);
      if (frame.type === "unresolved") showHint(frame.unresolved[0]?.reason);
      if (frame.type === "match" || frame.type === "unresolved" || frame.type === "error") sendFrame();
    };

    Two ways in

    Stream it, or post it

    The same match object comes back from both, so a batch importer and a live scanner share one parser.

    Live scanning

    WebSocket

    Open 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

    HTTPS

    A 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.

    One card per frame. A frame is a card. Photograph a binder page pocket by pocket, or crop the page yourself before sending — the cost is identical either way, because scans are counted per matched card rather than per request. Locating several cards in one image is coming; shipping it early would mean billing you for cuts through artwork.

    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.

    Single photo
    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
    POST /v1/scan — a match
    {
      "requestId": "scan_01K5Z8P4XQJ7YB3M",
      "latencyMs": 41,
      "matches": [
        {
          "confidence": 0.9962,
          "billed": true,
          "box": {
            "x": 0.171,
            "y": 0.064,
            "w": 0.658,
            "h": 0.872
          },
          "corners": [
            {
              "x": 0.183,
              "y": 0.064
            },
            {
              "x": 0.829,
              "y": 0.071
            },
            {
              "x": 0.817,
              "y": 0.936
            },
            {
              "x": 0.171,
              "y": 0.928
            }
          ],
          "distance": 21,
          "printingResolved": true,
          "finishResolved": true,
          "card": {
            "id": "pkm_ex7_99",
            "game": "pokemon",
            "name": "Rocket's Mewtwo ex",
            "set": {
              "code": "ex7",
              "name": "Team Rocket Returns"
            },
            "rarity": "Rare Holo EX",
            "prices": [
              {
                "source": "tcgplayer",
                "region": "NA",
                "currency": "USD",
                "market": 550
              },
              {
                "source": "cardmarket",
                "region": "EU",
                "currency": "EUR",
                "market": 5201.01,
                "trend": 5201.01,
                "avg7": 4398.42
              }
            ]
          },
          "alternatives": []
        }
      ],
      "scans": {
        "billed": 1,
        "remaining": 5842
      }
    }
    POST /v1/scan — an unresolved frame, billed 0
    {
      "requestId": "scan_01K5Z8P51N4WQD0T",
      "latencyMs": 29,
      "matches": [],
      "unresolved": [
        {
          "reason": "glare",
          "detail": "A highlight has blown out 31% of one region of the card. Tilt the card away from the light.",
          "box": { "x": 0.19, "y": 0.07, "w": 0.63, "h": 0.86 },
          "corners": [
            { "x": 0.21, "y": 0.07 },
            { "x": 0.82, "y": 0.09 },
            { "x": 0.80, "y": 0.93 },
            { "x": 0.19, "y": 0.90 }
          ],
          "candidates": 2,
          "bestConfidence": 0.71,
          "bestDistance": 51
        }
      ],
      "scans": { "billed": 0, "remaining": 5842 }
    }
    What you can rely on
    matches[].confidenceFloat
    0 to 1. Above your threshold the match is returned and billed; below it the frame is unresolved, which tells you where the card was and why it failed, 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[].corners[Point] | null
    The card's four corners in the frame as fractions of its width and height, clockwise from the corner matched as the card's top-left. This is the outline that was flattened and hashed, so drawn over the frame it shows a tilted or rotated card exactly — the box is the same outline squared off. Null when the frame was matched without finding an outline, such as a full-bleed scan.
    matches[].distanceInt
    Bits between the frame's hash and the matched printing's, 0 to 256 — the measurement confidence is derived from. On real phone frames the right card lands at a median of 24 bits and an unrelated card never under 46, so a caller tuning minConfidence can see what they are tuning against.
    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[].printingResolvedBoolean
    True when the set, collector number and language on card are the matched ones — the artwork belongs to one record, or the collector number, set code and language printed on the card were read and settled it. False when several records share the artwork and the printed line did not separate them: the card is identified, and the records it could be are in alternatives. Says nothing about the finish; see finishResolved.
    matches[].finishResolvedBoolean
    True only when card.printings has exactly one entry. Normal, reverse holo, foil and the patterns held on one record are entries in its printings, and the photo is not used to choose between them, so a card that exists in two finishes comes back false however clean the frame. Ask the operator, or use what you know about the stock.
    matches[].alternatives[Object]
    When printingResolved is false, the other records sharing the artwork, closest to the photo first. At most 15: a card reprinted more often than that with the same art (a Commander land, a Yu-Gi-Oh common) can have its own record outside the list, so a confirm step should also offer GET /v1/cards?game=…&name=…&language=…, which lists every printing. When it is true, ranked runners-up. Each carries cardId, game, language, confidence, name, printedName, set, collectorNumber, rarity, images, pricesFrom and printings (every finish with its key, label, picture and prices), so a confirm step can be drawn without another request. Carried on a billed match only: an unresolved frame reports how many candidates remained, not which.
    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 where the card was and what went wrong, so your operator can fix the shot rather than guess. 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, and an honest flag when it cannot be one

    Knowing a card is Charizard is worth very little — which Charizard decides whether it is worth three dollars or three hundred. So a match names the printing — set, collector number, language — and lists every finish it exists in, each with its price. Where the printing cannot be settled it says so instead of guessing: about one printing in six shares its artwork with another, and when the printed line does not separate them the card comes back identified, the printing marked unresolved, and every candidate attached with its set, number and finishes. The finish is never guessed from the photo. That is the field most scanners quietly guess at, and a wrong guess sits in your inventory at the wrong price until somebody notices.

    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
    • 6,000 matched cards a month
    • No daily limit — scan the month in an afternoon if you need to
    • Live socket and photo upload, same matcher on both
    • All 9 games, every printing we hold
    • Cardmarket EUR and TCGplayer USD on the match
    • Misses and refusals are never billed
    Choose Starter

    Recognition Growth

    Popular

    A 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
    • 25,000 matched cards a month
    • $0.0036 a scan — a quarter off Starter's rate
    • Overage at $0.005 rather than $0.008
    • Everything in Starter
    Choose Growth

    Recognition Scale

    Bulk operations, marketplaces, and apps whose users scan all day.

    $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
    • 100,000 matched cards a month
    • $0.0025 a scan — the lowest rate we publish
    • Overage at $0.0035
    • Everything in Growth
    Choose Scale

    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
    Create an account

    There is no daily limit. Past the allowance, scans keep matching as overage, drawn from your prepaid balance at a higher rate per scan than the plan it extends. 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

    250500,000
    • 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, at each plan’s overage rate. 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.

    ProviderEntryIncludedPer scanPricesBills missesLive stream
    TCGGraph$29 / mo6,000 scans$0.0048Both marketsNoWebSocket
    TCGGraph, no planNonePay as you go$0.012Both marketsNoWebSocket
    Ximilar€59 / mo100,000 credits≈ €0.0059No, +10 creditsYesNo
    CardSight AI$14.95 / mo5,000 calls≈ $0.0030Separate callYesEnterprise only
    NeoSatoshi$29 / mo2,500 scans≈ $0.0116Cardmarket onlyNoAnnounced
    TCGAPIs£99 / mo1,000 scans≈ £0.099BundledNot statedNo
    CardGrader.AI$49 pack400 credits$0.1225Separate moduleYesNo

    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. CardSight AI meters every API call, and its price lookup is a second call. Three providers charge whether or not the card is identified. One of them streams video: CardSight AI, for enterprise contracts at custom pricing, over gRPC. NeoSatoshi has announced a WebSocket mode. The socket here is on every plan and on pay-as-you-go, and a reconnect is never billed.

    Where this stands

    The only card recognition API with a live socket on every plan

    Checked against each provider's published documentation in September 2026. Every claim below is paired with what the field does, so you can verify the difference instead of taking the adjective.

    A live socket on every plan
    TCGGraph
    wss://api.tcggraph.com/v1/scan is part of the product from pay-as-you-go up: open it when a user points a camera at a card, push frames, and matches come back on the same connection in the same JSON the photo endpoint returns. The playground on the dashboard runs on it.
    Elsewhere
    Every other card recognition API we could find identifies one uploaded image per request. The exception is CardSight AI, which streams video over gRPC for enterprise clients at custom pricing and does not include it in its Free or Pro tiers. NeoSatoshi has announced a WebSocket mode.
    One card, one scan — not one frame, one charge
    TCGGraph
    The session tracks the card in front of the lens. The same printing is billed once every 20 seconds however many frames show it, so a card that sits still at ten frames a second is one scan rather than two hundred, and a frame that cannot be placed is answered and costs nothing.
    Elsewhere
    Per image or per API call. Ximilar, CardSight AI and CardGrader.AI meter the request whether or not a card was identified; a live feed sent to a per-image API is billed on every frame.
    The session remembers the printing
    TCGGraph
    Once one frame has read the collector number off the card, following frames of the same card report that printing even when the line is no longer legible, and while a tie stands the same leader is reported rather than a different one on every frame. A track rides out a hand passing over the lens or a frame that was refused, and lets go 2.5 seconds after the card was last seen so the next card on the table is never mistaken for the last one.
    Elsewhere
    A request is answered on its own, so carrying a printing between frames — and keeping a live overlay from flickering between two reprints — is left to your client. CardSight AI's streaming session returns detections as they occur; its documentation does not describe fusing them across frames.
    Two markets in the match
    TCGGraph
    A match carries the full catalog record with Cardmarket in EUR and TCGplayer in USD, from the same source and at the same freshness as the data API, on every plan and in one round trip.
    Elsewhere
    Ximilar prices from eBay listings for ten more credits. TCGAPIs returns product IDs and prices through separate endpoints, with Cardmarket on its higher plans. CardSight AI prices through a second call, from its own US sales data, with Magic market data announced rather than live. NeoSatoshi and CardStand return Cardmarket only.
    A refusal you can act on
    TCGGraph
    An unresolved frame names the reason — blurred, glare, cropped, occluded, ambiguous, not in the catalog — with the measurement behind it, how far the nearest printing was in bits, how many were still in contention, and where the card was in the frame. Your interface can say what to change.
    Elsewhere
    Usually a confidence score below a threshold, or an empty list. CardStand names a reason; the others documented above do not.
    Cards held in a hand
    TCGGraph
    The detector predicts the corners a thumb covers, the card is flattened from those, and where the covered patch hides too much of the artwork a tile search names the card from the parts that show. When even that cannot settle it, the answer is occluded — a refusal with the region — rather than a guess at a different card.
    Elsewhere
    Ximilar states its models are trained for partly obscured cards. Most others ask for a clear, well-lit photo of the card front with minimal background clutter, and what happens to a covered card is not documented.

    As far as their published documentation shows, checked in September 2026, this is the only card recognition API that ships a live WebSocket on every plan including pay-as-you-go, bills per identified card rather than per frame or per request, carries the printing across the frames of a session, and returns Cardmarket and TCGplayer prices with the match. The claims above are the ones we can point at; anything broader would be an adjective.

    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, the finishes it exists in with a market price for each, and one tap to pick the finish. 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

    Point a phone at a binder and pull the pocket under it, card by card, and a shelf of binders becomes a spreadsheet in an afternoon. The socket is what makes that bearable: no handshake per card, so the pace is however fast somebody can turn a page.

    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.

    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