Card Clerk · Scan API · v1

Card Clerk Scan API

Send one scanned trading card image and get back the card and its printing, in the same call. Built for scanner output: one card per image, cropped to the card.

Quick start

We send your API key separately. Keep it secret, and send it only in the Authorization header, over HTTPS.

curl https://vision.card-clerk.com/v1/scans \
  -H "Authorization: Bearer $CARD_CLERK_KEY" \
  -F "img=@card-0001.jpg" \
  -F "game=mtg" \
  -F "ref=card-0001" \
  -F "batch=box-7"

The same call in Python, with requests:

import os
import requests

try:
    with open("card-0001.jpg", "rb") as image:
        resp = requests.post(
            "https://vision.card-clerk.com/v1/scans",
            headers={"Authorization": f"Bearer {os.environ['CARD_CLERK_KEY']}"},
            files={"img": ("card-0001.jpg", image, "image/jpeg")},
            data={"game": "mtg", "ref": "card-0001", "batch": "box-7"},
            timeout=180,
        )
except requests.ConnectionError:
    resp = None  # refused while the API restarts: retry shortly
if resp is None or not resp.headers.get("content-type", "").startswith("application/json"):
    print("the API is restarting: retry shortly")  # refused, or a 502 from the server in front
elif resp.ok:
    answer = resp.json()
    print(answer["status"], answer["card"], answer["review_reasons"])
else:  # every error has an `error`; a refusal before the scan has no `status`
    answer = resp.json()
    print(resp.status_code, answer.get("status"), answer.get("error"))

The request

POST /v1/scans takes a multipart/form-data form with one image and the text fields below. Other text fields are ignored; a form may hold up to 16 text fields of up to 8 KB each.

FieldRequiredWhat to send
imgYesThe image file: one card, cropped to the card.
gameYesmtg (Magic: The Gathering), pokemon, fab (Flesh and Blood), onepiece or swu (Star Wars: Unlimited).
refNoYour own id for this image, up to 256 characters. We return it unchanged, so you can match answers to images.
batchNoYour own id for a group of images, such as a box or a session. Up to 256 characters.

Images

The answer

Every answer from the API is JSON. A scan the API could read comes back 200 with a status of matched, no_match or card_back. This example shows a Magic promo answered as its standard printing, flagged for review:

{
  "ref": "card-0001",
  "batch": "box-7",
  "scan_id": "6f1c2b9e-…",
  "status": "matched",
  "card": {
    "game": "mtg",
    "print_id": "…",
    "set_code": "stx",
    "set_name": "Strixhaven: School of Mages",
    "collector_number": "119",
    "variant_code": "",
    "name": "Accomplished Alchemist",
    "face": null
  },
  "card_confidence": 0.998,
  "printing_confidence": 0.978,
  "needs_review": true,
  "review_reasons": ["low_printing_confidence"],
  "candidates": [],
  "answered_base": true,
  "rotation_applied": 90,
  "alternatives": [
    {"print_id": "…", "set_code": "pstx", "collector_number": "119p", "variant_code": "",
     "relationship_type": "promo_alternative", "is_base": false, "label": "pstx-119p"},
    {"print_id": "…", "set_code": "pstx", "collector_number": "119s", "variant_code": "",
     "relationship_type": "promo_alternative", "is_base": false, "label": "pstx-119s · Alternate Art s"}
  ]
}
FieldMeaning
statusWhat happened. See Statuses.
scan_idOur id for this scan. Quote it when you report a problem.
cardThe printing we identified, or null. print_id is Card Clerk's id for that printing; it is null when we recognise a card we hold no record for (no_library_record), or could not read its record (library_unavailable). variant_code tells the printings at one set and number apart; its form differs by game (Magic and Pokémon leave it empty for the standard printing). face names the face of a double-faced card, when we can tell. For Flesh and Blood, One Piece, Pokémon and Star Wars: Unlimited, the image does not tell the finish: the printing is the card in one of its finishes, so set foil or not yourself.
card_confidenceMagic only, 0 to 1: how sure we are of the card (its artwork). null for other games.
printing_confidenceMagic only, 0 to 1: how sure we are of the printing our model chose.
needs_reviewtrue when a person should check the answer. review_reasons says why.
candidatesWhen we could not choose one printing: every printing that fits, the likeliest first. Each has the same fields as card.
alternativesRelated printings a person might mean instead: promos, reprints and other print runs, and for Magic other finishes. Offer these when the answer needs review.
answered_basetrue when we answered the standard printing of a Magic promo that shares its artwork. The promo our model saw is then the first alternative.
rotation_appliedDegrees clockwise we turned the image before the match.
ref, batchYours, returned unchanged.

When an answer needs review

An answer with needs_review: true is still our best answer, but a person should confirm it. The reasons:

ReasonMeaning
several_candidatesWe could not choose between cards. candidates lists them.
low_card_confidenceMagic: under 95% sure of the card.
low_printing_confidenceMagic: sure of the card, not of the printing, such as a prerelease stamp against the plain card. Also every answer with answered_base: true. alternatives lists the related printings.
print_not_resolvedThe set and number fit more than one printing. candidates lists them, the likeliest first.
edition_not_resolvedThe card has a 1st Edition and an Unlimited printing, and the image cannot tell them apart.
no_library_recordWe recognised the card but hold no record for that printing, so print_id is null.
library_unavailableOur card database did not answer in time. The answer can lack the card's record (print_id and its names null), alternatives may be short, and a Magic promo is not checked against its standard printing. Send the image again later for the full answer.

Statuses and errors

Every error answer from POST /v1/scans is JSON with an error sentence. A wrong path or method gets the web server's own 404 or 405, whose JSON has a detail instead. Once the API has your request, every answer also carries status, your ref (null where the form could not be read) and the scan_id of its record. The refusals before that (401, 411, an early 413, 429, and busy when 64 scans are in hand) are not recorded and carry no scan_id.

HTTPStatusMeaningWhat to do
200matchedWe identified the card.done Check needs_review.
200no_matchNo card matched, after the 180° retry.check Rescan, or review by hand.
200card_backThe image is a card back (Magic).check Flip and rescan.
400invalid_imageNot an image we can read, or outside the size and shape limits.fix Do not resend as is.
400unsupported_gameThe game is not one of the five.fix Correct game.
400invalid_fieldref or batch over 256 characters.fix Shorten it.
400invalid_formThe body is not the form above: no img file or no game, a second file, more than 16 text fields, or a field over 8 KB.fix Correct the form.
401—No key, or a key we do not know.fix Check the header.
411—No Content-Length. Chunked uploads are not taken.fix Send the length.
413too_largeThe image is over 20 MB or 25 megapixels. A request over 21 MB is refused 413 before it is read, without a status.fix Send a smaller image.
429—Over your key's requests per minute.wait Retry after the minute turns.
503busyToo many scans in hand right now.wait Retry after the Retry-After seconds.
503vision_unavailableCard identification is down for a moment.wait Retry later, backing off.
502—The API is restarting, for a moment, and the server in front of it answers. The body is not JSON. While it restarts, a connection can also be refused outright.wait Retry shortly.
500errorSomething failed on our side.wait Retry once; if it repeats, send us the scan_id.

Limits and pacing

Batches: send the images of a batch as separate requests, with the same batch value. Retry busy, vision_unavailable, 429 and 502 answers, and a refused connection; do not retry the other 4xx ones without changing the request.

Health check

GET /v1/ping needs no key and answers {"ok": true} while the API is up.

Your data

We keep a record of each request we take in: its time, your ref and batch, its status and the printing we answered. A request refused before we read it (a missing or unknown key, over your requests per minute, a request over 21 MB, or busy with 64 scans in hand) leaves no record. We also keep the image of a scan we read; we do not keep one we refuse as too large or for its fields, and under heavy load an image can be dropped while its record is kept. We use them to bill and to look into problems you report.