Card Recognition API for Pokémon, One Piece, Lorcana, Yu-Gi-Oh!, Magic and Star Wars: Unlimited
One photo in, structured cards out. Send an image and get the Pokémon, One Piece, Disney Lorcana, Yu-Gi-Oh!, Magic or Star Wars: Unlimited cards it shows as clean JSON: name, set, number, rarity, Cardmarket ID and daily updated pricing. Football (soccer) cards come back with the player, the release and the suggested parallel.
The API runs on your existing NeoSatoshi subscription. Detections draw from the same monthly credit balance as the app, so there is no separate API plan to buy. API keys need a paid plan.
Test it now, no signup
Public demo key: nsk_live_9c253fbed995042a98280ad13e5c47d3c4a59c9b4f0f7274 (10 detections per day per network). This block runs as-is in any terminal and returns the JSON for our example photo. AI agents and coding assistants may call the endpoint directly with this key; the machine-readable spec is at /openapi.json.
bash
curl -s https://neosatoshi.com/static/images/example_api_single.jpg -o card.jpg
printf '{"image": "%s"}' "$(base64 < card.jpg | tr -d '\n')" > body.json
curl -X POST https://europe-west1-neosatoshi.cloudfunctions.net/detectApi \
-H "Authorization: Bearer nsk_live_9c253fbed995042a98280ad13e5c47d3c4a59c9b4f0f7274" \
-H "Content-Type: application/json" \
-d @body.jsonOr pass a public link instead of a file with image_url:
bash
curl -X POST https://europe-west1-neosatoshi.cloudfunctions.net/detectApi \
-H "Authorization: Bearer nsk_live_9c253fbed995042a98280ad13e5c47d3c4a59c9b4f0f7274" \
-H "Content-Type: application/json" \
-d '{"image_url": "https://neosatoshi.com/static/images/example_api_lorcana.jpg", "tcg": "lorcana"}'REST, optional streaming
·
Bearer-token auth
·
Multi-card photos for Pokémon
Live examples
Single card
1 card
Click to enlarge
One card in, one fully parsed card object out: identity, set, rarity, Cardmarket id, price and confidence.

Cinderace ex
Scarlet & Violet Black Star Promos
#163
4.36 €
CM 796939Multiple cards in one photo
9 cards
Click to enlarge
Nine cards detected in a single binder photo, each returned as its own object in the cards array, with its own confidence.

Celebi & Bisaflor-GX
Team Up
#1
42.64 €
CM 367097
Eevee
SM Black Star Promos
#SM184
19.53 €
CM 370819
Ludicolo
Phantasmal Flames
#95
1.98 €
CM 857670
Zytomega
Scarlet & Violet Black Star Promos
#212
0.98 €
CM 836658
Gallopa
Destined Rivals
#189
5.46 €
CM 826063
Delibird
Mega Evolution
#152
2.70 €
CM 851223
Eevee
SWSH Black Star Promos
#SWSH095
3.79 €
CM 547026
Eevee
SWSH Black Star Promos
#SWSH127
5.25 €
CM 568799
Eevee
SWSH Black Star Promos
#SWSH190
3.67 €
CM 609464Japanese card
1 card
Click to enlarge
Japanese cards are detected too: the Japanese name is returned in name_ja and the card is matched to its set and Cardmarket id.

Chansey
ラッキー
Mask of Change
#113
17.69 €
30d avg
CM 767547Disney Lorcana card
1 card
Click to enlarge
Send "tcg": "lorcana". Lorcana cards come back with title and subtitle, the English name, ink colour and both non-foil and foil Cardmarket prices.

Genie - Wonderful Trickster
Azurite Sea
#61
1.32 €
CM 799023Try it
Live test box
betaAPI key
Public demo key. Limited to 10 detections per day per network. Create your own key in Settings for your plan's full credit balance.
Quick examples
Run a detection on a known image with just your API key, no upload needed.
Card image (JPEG or PNG)
No image selected
POSTs to https://europe-west1-neosatoshi.cloudfunctions.net/detectApi
Quickstart
Create an API key
Create a key in your dashboard settings. It uses your existing plan's credit balance, with no separate API subscription.
POST an image
Base64-encode a JPEG or PNG and POST it as JSON with your key in the Authorization header.
Read the cards
The response returns an array of detected cards with set, number, rarity, Cardmarket ID and pricing.
Authentication
HTTP header
Authorization: Bearer <API_KEY>For testing, use the public demo key nsk_live_9c253fbed995042a98280ad13e5c47d3c4a59c9b4f0f7274. It allows 10 detections per day per network and needs no account. For production, create your own key in the dashboard settings (any paid plan).
Keep your key server-side. Never embed it in client-side or mobile app code where it can be extracted. Requests without a valid key return 401.
Endpoint reference
https://europe-west1-neosatoshi.cloudfunctions.net/detectApiRequest body is JSON: the field image containing a base64-encoded JPEG or PNG, or image_url with a public https link to one. For Pokémon one image can contain many cards and the response returns all of them; the other games need one card per image.
Request parameters
| Field | Type | Required | Description |
|---|---|---|---|
image | string | Yes, or image_url | Base64-encoded JPEG or PNG of one or more cards, at most 12 MB. |
image_url | string | Instead of image | A public https link to a JPEG or PNG (at most 12 MB, up to 3 redirects). Handy for chat assistants and no-code tools that cannot base64-encode a file. Private or internal addresses are refused. |
tcg | string | No | Card game to detect: pokemon (default), onepiece, lorcana, yugioh, magic, star_wars_unlimited or football (one card front per photo, at most 4 MB). |
stream | boolean | No | true answers with Server-Sent Events instead of one JSON body, the same as the header Accept: text/event-stream. Default false. See Streaming. |
Available variants are not automatic variant recognition
For Pokémon, possible_variants describes known catalog options for that card, not which option is visible in the photo. Offer those options for merchant confirmation; do not automatically select the first entry. A null or empty list is not proof that no special print exists. The list can also include base, First Edition or related reprint products, and an entry’s special_variant may be null. Reverse Holo and First Edition retain their separate detection flags; catalog entries do not set those flags.
Entries can include key, label, cardmarket_id, special_variant and an optional price object (avg30, avg7, lowPrice, trendPrice, idProduct; EUR). Prices may be missing. Preserve distinct entries even when their special_variant values match: they can refer to different Cardmarket products. After choosing an alternative, use that alternative’s list and clear the previous special-variant selection.
One Piece uses a separate printing model. These Pokémon variant fields are not provided for One Piece and do not constitute a complete One Piece printing selector. The existing website WebSocket response is unchanged by this REST API addition.
Example request body
JSON
{
"image": "<base64-encoded JPEG or PNG>"
}Code examples
bash
# Your key, or the public demo key (10 detections per day per network).
API_KEY=${NEOSATOSHI_API_KEY:-nsk_live_9c253fbed995042a98280ad13e5c47d3c4a59c9b4f0f7274}
# Base64-encode the image into a JSON body (works on macOS and Linux, any image size).
printf '{"image": "%s"}' "$(base64 < card.jpg | tr -d '\n')" > body.json
curl -X POST https://europe-west1-neosatoshi.cloudfunctions.net/detectApi \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @body.jsonStreaming (SSE)
Streaming helps with multi-card Pokémon photos such as binder pages, where the first cards arrive while the rest are still being matched, and with progress displays. The other games have one card per photo, so there you get progress events, not a faster result. Measured on a binder page with 9 cards: the first card after about 6 seconds, all of them after about 17. A stream costs the same as a normal call and ends with the same result.
Opt in with "stream": true in the body or the header Accept: text/event-stream. A boolean stream in the body wins over the header, so "stream": false always gets JSON. The stream starts once the image is uploaded; every error before that is the usual JSON response with its status code.
| Event | When | Data |
|---|---|---|
accepted | First event, once the image is uploaded and detection starts. | { request_id, tcg } |
progress | The stage changed: detecting (detection started) or finalizing (all cards are in, the final response is being prepared). Ignore stages you do not know. | { stage, elapsed_ms } |
card | One card, as soon as it is matched. index is its 0-based position in the cards of done. Cards come in the order they finish, not left to right in the photo. | { index, card } with card exactly as in cards[] |
done | Final event on success. Its data is exactly the JSON 200 body (demo instead of credits for the demo key) and is the final result: alternatives can be shorter than in the card event. | { request_id, count, cards, credits } |
error | Final event on failure. status is the HTTP status the call returns without streaming; the rest is the same JSON error body, always with a message. | { status, error, message, credits? } |
Each event is one event: line and one data: line with JSON. A : keep-alive comment comes every 15 seconds.
Example stream (shortened)
text/event-stream
event: accepted
data: {"request_id":"req_abc123","tcg":"pokemon"}
event: progress
data: {"stage":"detecting","elapsed_ms":2140}
event: card
data: {"index":0,"card":{"name":"Charizard ex","language":"en","set":{"id":"sv3pt5","ptcgo_code":"MEW","name":"151","number":"006"}, ...}}
event: card
data: {"index":1,"card":{"name":"Cinderace ex","language":"en","set":{"id":"svp","ptcgo_code":"PR-SV","name":"Scarlet & Violet Black Star Promos","number":"163"}, ...}}
: keep-alive
event: progress
data: {"stage":"finalizing","elapsed_ms":6380}
event: done
data: {"request_id":"req_abc123","count":2,"cards":[...],"credits":{"remaining":4985}}bash
# Your key, or the public demo key (10 detections per day per network).
API_KEY=${NEOSATOSHI_API_KEY:-nsk_live_9c253fbed995042a98280ad13e5c47d3c4a59c9b4f0f7274}
# -N prints every event as it arrives instead of buffering the output.
curl -N -X POST https://europe-west1-neosatoshi.cloudfunctions.net/detectApi \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"image_url": "https://neosatoshi.com/static/images/example_api_multi.jpg"}'Rules for clients
- Check the status and the Content-Type first: errors before the detection starts are
application/json, exactly as without streaming. - A stream sends exactly one final event,
doneorerror, and then ends. If the connection ends without one, the outcome is unknown and cards may already have been charged. - Do not reconnect automatically. A stream cannot be resumed; sending the image again is a new, charged detection.
- Use the cards in
doneas the final result. Treat a repeatedindexas a replacement of the card at that position. - In a browser, use
fetchand read the response body as shown:EventSourcecannot send a POST.
Correcting a card
A detection carries correction with its request_id and a deadline, and every card its card_index. First look at the card's alternatives: each has a card_id. If the right card is not there, search with the detection's request_id and card_index. Then send the chosen card_id to the corrections endpoint. You get back the full card in the shape of a detected card, with prices, Cardmarket product id and images. Fields only a photo can tell (confidence, reverse holo, first edition, graded) are empty. Corrections also teach our recognition.
1. Correction search
bash
curl -X POST https://europe-west1-neosatoshi.cloudfunctions.net/cardsApi/search \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"request_id": "req_e4b31a98-6499-44e6-87e3-c42fda41ec98", "card_index": 0, "q": "charizard"}'200 · JSON
{
"results": [
{ "card_id": "base1-4", "name": "Charizard", "language": "en",
"set": { "id": "base1", "ptcgo_code": "BS", "name": "Base", "number": "4" },
"cardmarket_id": 273699,
"prices": { "cardmarket": { "trend": 441.22, "low": 99, "avg7": 390.77, "avg30": 457.47, "currency": "EUR" } },
"images": { "small": "https://d1m1gwcombja0u.cloudfront.net/...", "large": "https://d1m1gwcombja0u.cloudfront.net/..." },
"confidence": null, "alternatives": [], "graded": null, ... }
],
"count": 3,
"has_more": true,
"billing": { "charged_credits": 0, "source": "free_correction" },
"correction": { "request_id": "req_e4b31a98-...", "card_index": 0, "searches_left": 4 }
}Body: request_id and card_index, then q (a card name, optionally with the number, e.g. "charizard 4/102") or set plus number. Optional: language (default: the detected card's language), tcg (default: the detected card's game) and limit (1 to 5).
2. Correction
bash
curl -X POST https://europe-west1-neosatoshi.cloudfunctions.net/cardsApi/corrections \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"request_id": "req_e4b31a98-6499-44e6-87e3-c42fda41ec98", "card_index": 0, "card_id": "base1-4"}'200 · JSON
{
"request_id": "req_e4b31a98-6499-44e6-87e3-c42fda41ec98",
"card_index": 0,
"card": { "card_id": "base1-4", "name": "Charizard", "card_index": 0, "cardmarket_id": 273699, "prices": { ... }, ... },
"applies_left": 2,
"correction": { "until": "2026-11-03T13:03:23.823Z" }
}| Limit | Value |
|---|---|
| Window | 24 hours after the detection (correction.until), from the API key account that made it. Demo key detections only from the same network. |
| Correction searches | 5 per detected card, 500 per account per day. Free. Fewer than 3 letters, a set alone or a number alone is refused (query_too_broad). |
| Corrections | 3 per detected card, the last one counts. Free. The detection itself stays charged. |
| Results | At most 5 per correction search, has_more but no total. |
| Rate | 30 requests per minute per key for the card search. |
| Games | All six games in their catalog languages. Yu-Gi-Oh!: card_id is the passcode, add set (the printed code, e.g. LOB-EN001) to pick a printing. Magic: card_id is the oracle_id, set + number pick the printing. Star Wars: Unlimited: card_id of a printing or the card_key. One Piece: cardmarket_id picks a parallel. |
| Error | Meaning |
|---|---|
400 query_too_broad | Search for at least 3 letters of the name, or send set and number. |
403 request_not_yours | The detection belongs to another account (or another network for the demo key). |
404 unknown_request_id, card_not_found | Unknown or older than 48 hours, or no card with that card_id in that language. |
410 correction_window_closed | More than 24 hours since the detection. |
429 correction_budget_exhausted, correction_limit_reached | The free searches or corrections of this card are used up. |
Card search
The same endpoint as the correction search, without request_id. Body: tcg (required), then q (a card name, optionally with the number) or set plus number. Optional: language and limit. Results come in the shape of a detected card; fields only a photo can tell (confidence, reverse holo, first edition, graded) are empty.
Search
bash
curl -X POST https://europe-west1-neosatoshi.cloudfunctions.net/cardsApi/search \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-card-3" \
-d '{"tcg": "pokemon", "q": "charizard 4/102", "language": "en", "limit": 3}'200 · JSON
{
"results": [
{ "card_id": "base1-4", "name": "Charizard", "language": "en",
"set": { "id": "base1", "ptcgo_code": "BS", "name": "Base", "number": "4" },
"cardmarket_id": 273699,
"prices": { "cardmarket": { "trend": 441.22, "low": 99, "avg7": 390.77, "avg30": 457.47, "currency": "EUR" } },
"images": { "small": "https://d1m1gwcombja0u.cloudfront.net/...", "large": "https://d1m1gwcombja0u.cloudfront.net/..." },
"confidence": null, "alternatives": [], "graded": null, ... }
],
"count": 1,
"has_more": false,
"billing": { "charged_credits": 0.5, "source": "debited", "prepaid_half": true },
"credits": { "remaining": 4211 }
}One card by id
bash
curl "https://europe-west1-neosatoshi.cloudfunctions.net/cardsApi/cards/pokemon/base1-4?language=en" \
-H "Authorization: Bearer YOUR_API_KEY"200 · JSON
{
"card": { "card_id": "base1-4", "name": "Charizard", "cardmarket_id": 273699, "prices": { ... }, "images": { ... }, ... },
"billing": { "charged_credits": 0.5, "source": "prepaid" }
}Football
bash
curl -X POST https://europe-west1-neosatoshi.cloudfunctions.net/cardsApi/search \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"tcg": "football", "q": "kane chrome 2023", "limit": 5}'
# then, with card_id and ref of a result:
curl "https://europe-west1-neosatoshi.cloudfunctions.net/cardsApi/cards/football/fb-a2a-2a?ref=RESULT_REF" \
-H "Authorization: Bearer YOUR_API_KEY"200 · JSON
{
"card": { "name": "Harry Kane", "tcg": "football", "card_id": "fb-a2a-2a",
"set": { "id": "topps-chrome-bundesliga-2023-24", "name": "2023-24 Topps Chrome Bundesliga", "number": "82" },
"season": "2023-24", "subset": "Base", "teams": [ ... ], "rookie": false, "card_type": "player",
"prices": null, "images": { "small": null, "large": null },
"parallels": [ { "name": "Refractor", "print_run": null }, { "name": "Magenta Refractor", "print_run": 399 }, ... ] },
"billing": { "charged_credits": 0.5, "source": "prepaid" }
}| Rule | Value |
|---|---|
| Price | 0.5 credits per search or lookup that returns a card, from the same balance as detection. No result, an unknown card or an error: free. Credits are whole numbers, so the first paid call takes 1 credit and also pays the next one (billing.source "debited", then "prepaid"). |
| Results | At most 10 full card objects per search (limit 1 to 10), has_more but no total. Fewer than 3 letters, a set alone, a number alone or wildcards are refused (query_too_broad). |
| Repeats | Send an Idempotency-Key header (8 to 100 characters) and a repeat of the same request within 24 hours returns the stored answer, uncharged, with Idempotent-Replayed: true. |
| Rate | 30 requests per minute per key, shared with correction searches. |
| Demo key | 10 searches and 3 lookups per day per network, at most 5 results, never charged. |
| Games | All six games in their catalog languages; language defaults to en. The lookup takes the card_id of a detected or searched card; the same printing rules as for corrections apply (Yu-Gi-Oh! set, Magic and SWU set + number, One Piece cardmarket_id), as query parameters. |
| Football | tcg "football": text search only (the player, optionally with set or season words), no language, no prices yet. Each result carries a ref; the lookup needs it (?ref=..., 24 hours, the account that searched) and adds the parallels of the card's subset with their print runs. Football detections cannot be corrected yet. |
| Plan | Searches per day | Lookups per day |
|---|---|---|
| Start-Up | 500 | 500 |
| Business | 1,500 | 1,500 |
| Enterprise | 5,000 | 5,000 |
| Unlimited | 2,000 | 2,000 |
| Error | Meaning |
|---|---|
402 out_of_credits | No credits left. No results, nothing charged. |
403 feature_not_enabled | Card search is not part of this plan. |
404 card_not_found | The lookup found no card with this card_id in this language. Nothing charged. |
409 idempotency_in_progress | A request with this Idempotency-Key is still running. |
422 idempotency_key_reuse | This Idempotency-Key was used for another request. |
429 rate_limited, daily_cap_reached | Per minute or per day limit reached. Retry-After is set; the daily limit resets at midnight UTC. |
503 billing_unavailable | Billing could not be confirmed. Nothing charged, retry. |
400 ref_required, ref_invalid, ref_expired | Football lookup without the ref of a search result of this account from the last 24 hours. Search again. Nothing charged. |
429 football_daily_limit, 503 football_unavailable | The football catalog's own daily limit, or the catalog is unavailable. Nothing charged. |
Response schema
| Field | Type | Description |
|---|---|---|
request_id | string | Unique id for the request. Include it when contacting support. |
count | number | Number of confidently detected cards in the image. |
cards | array | List of detected cards (see fields below). |
cards[].card_index | number | Position of the card in this detection (0, 1, 2 ...). A correction names the card by request_id and card_index. |
cards[].name | string | Card name in its printed language; the English name for Japanese, Korean and Simplified Chinese cards. |
correction | object | When this detection can be corrected for free: request_id, until (ISO time, 24 hours) and the corrections endpoint. See Correcting a card. |
cards[].card_id | string | null | Our card id, e.g. "base1-4". Stable; use it to correct, look up or store the card. |
cards[].name_ja | string | null | Japanese name, when known; otherwise null. |
cards[].name_ko | string | null | Korean name, when known; otherwise null. |
cards[].name_zh | string | null | Simplified Chinese name, when known; otherwise null. |
cards[].language | string | Detected card language, e.g. "en", "ja" or "zh". |
cards[].reverse_holo | boolean | Detected as a Reverse Holo print (gated by rarity and era). |
cards[].first_edition | boolean | Detected as a 1st Edition print. |
cards[].possible_variants | array | null | Curated per-card variant entries, passed through unchanged. Entries may contain key, label, cardmarket_id, special_variant and price. Null means no variant data is available; it does not prove that no variants exist. |
cards[].special_variant | string | null | Selected/detected special variant only when supplied upstream; otherwise null. Never inferred from possible_variants. The current detection pipeline generally does not populate this field. |
cards[].alternatives[].card_id | string | null | The alternative’s card id: send it to the corrections endpoint to pick this alternative. |
cards[].alternatives[].cardmarket_id | number | null | The alternative’s Cardmarket product id. |
cards[].alternatives[].possible_variants | array | null | The alternative card’s own variant entries, in the same unchanged shape as the main match. |
cards[].is_energy_card | boolean | The card is an Energy card. |
cards[].is_card_back | boolean | The photo shows a card back. When true, identity fields are unreliable. |
cards[].set | object | Set id, ptcgo_code (the short set abbreviation / tournament code, e.g. "MEW" for 151, "ASC" for Ascended Heroes — distinct from the internal id like "sv3pt5"), name, and the card number within the set. |
cards[].rarity | string | Printed rarity. |
cards[].cardmarket_id | number | Cardmarket product id (idProduct), the canonical Cardmarket identifier. Null for a few cards. |
cards[].prices | object | Pricing block. cardmarket.trend / low / avg7 / avg30 in EUR, plus a currency field. |
cards[].images | object | Reference image URLs (small, large), served from our CDN where available (null when a card is not yet hosted). |
cards[].confidence | number | Detection confidence from 0 to 1. |
cards[].image_quality | object | Input photo quality flags: blurred and unusual_dimensions (booleans). |
cards[].alternatives | array | Lower-scored candidate matches, each with name, set, number and score. |
cards[].graded | object | null | The slab on the photo, null for a raw card: grader, grade, cert_number, card_check and label. Read from the label, never estimated. See "Graded slabs" below. |
credits.remaining | number | Credits left on your account after this request. |
demo | object | Only with the public demo key, instead of credits: limit_per_day and remaining_today for your network. |
warnings | array | Only when the image was processed in part: [{code, message}] with code "credits_ran_out" (the key ran out of card scans; only the returned cards were processed and charged) or "incomplete" (the detection failed for part of the image). Send the image again for the rest. |
Example response
200 · JSON
{
"request_id": "req_abc123",
"count": 2,
"cards": [
{
"name": "Charizard ex",
"name_ja": null,
"language": "en",
"set": { "id": "sv3pt5", "ptcgo_code": "MEW", "name": "151", "number": "006" },
"rarity": "Illustration Rare",
"cardmarket_id": 746497,
"reverse_holo": false,
"first_edition": false,
"possible_variants": null,
"special_variant": null,
"is_energy_card": false,
"is_card_back": false,
"prices": {
"cardmarket": { "trend": 3.93, "low": 2.50, "avg7": 4.05, "avg30": 4.21, "currency": "EUR" }
},
"images": {
"small": "https://d1m1gwcombja0u.cloudfront.net/public_images/cards_en/sv3pt5/sv3pt5-006_small.webp",
"large": "https://d1m1gwcombja0u.cloudfront.net/public_images/cards_en/sv3pt5/sv3pt5-006.png"
},
"confidence": 0.97,
"graded": null,
"image_quality": { "blurred": false, "unusual_dimensions": false },
"alternatives": [
{ "name": "Charizard", "set": "Base", "number": "004", "score": 0.81, "possible_variants": null }
]
}
],
"credits": { "remaining": 4985 }
}Graded slabs
Send a photo of a PSA, BGS or CGC slab like any other card, in every game and the languages that game supports. No extra parameter and no extra cost: a slab is one card scan. cards[].graded then holds what the label says, and it is null on a raw card.
cards[].graded
{
"grader": "PSA",
"grade": "10",
"cert_number": "12345678",
"card_check": "agree",
"label": {
"name": "Charizard",
"number": "199",
"code": null,
"set": "MEW",
"year": "2023"
}
}grader: PSA, BGS or CGC, for football cards also SGC. null when the slab was recognised but the company was not.grade: a string on that company's scale, such as "10", "9.5", "10 Black Label", "10 Pristine" or "Authentic". null when it could not be read: the card is still a slab, so ask for the grade rather than treating it as raw.cert_number: as read, null when unread. It is not verified with the grading company.card_check: agree, disagree or unknown, comparing the label with the card fields. On disagree, show the slab to a person. In Magic and Star Wars: Unlimited a card that could not be read can come from the name on the label, so agree is not always a second opinion there.label: name, number, code, set and year as read from the label, each null when absent. The name can be shorter than the card's (on Pokémon PSA and BGS labels often the species alone) and is usually English, also on a localized card, so usecards[].nameandcards[].languagefor the card.pricesremain the prices of the raw card. They are not a price for the slab.- Football cards:
label.nameis the player andlabel.setthe product as printed, pluslabel.parallelandauto_grade, the autograph's own grade on a dual-graded slab. - A slab of another company, for example TAG (or SGC outside football cards), comes back as a raw card or with
gradernull, never with a grade. The API never estimates a grade or condition and does not check authenticity.
About the reference images
The images URLs are reference images for identification, served from our own CDN where available (the cardmarket_id, set and number remain the stable references). Where we don't yet host an image, the URL falls back to a third-party source (e.g. the Pokémon TCG API / pokemontcg.io) and is subject to that source' terms. Card artwork is © The Pokémon Company / Nintendo / Creatures / GAME FREAK.
Errors
| Status | Meaning | How to handle |
|---|---|---|
| 400 | Bad image | The image is missing, not base64, or not a JPEG/PNG. Fix the payload. |
| 401 | Invalid or missing key | Check the Authorization header and your key. |
| 402 | Out of credits | Top up your balance before retrying. |
| 405 | Method not allowed | The endpoint only accepts POST (and OPTIONS for CORS preflight). |
| 422 | No card detected | No card was found. Improve lighting/framing and retry. No credit charged. |
| 429 | Rate limited | Back off and retry with exponential backoff. |
| 500 | Transient error | Temporary issue. Retry the request after a short delay. |
| 503 | Football capacity | Football only. football_daily_limit: the daily football capacity is used up; retry after resets_at (the Retry-After header). Nothing is charged and the other games still work. football_unavailable: retry later. |
| 504 | Detection timed out | The pipeline did not finish in time (often a cold start). Retry; warm requests are fast. Use a client timeout of at least 120s. |
Without streaming the call holds the connection until detection is done. One card usually takes about 4 seconds, about 10 to 12 seconds on the first request after a quiet period; a binder page with 9 cards about 17 seconds. If it exceeds the limit you receive a 504: retry, and set a client timeout of at least 120s. With streaming, errors before the detection starts are the same JSON responses; after that they arrive as the final error event, with the status in its data (see Streaming).
Credits & rate limits
Charged per confident card
One credit is charged for each card the model confidently detects, not per image. A clean photo with three cards costs three credits.
Uncertain cards are free
When the model is visually uncertain about a card, no credit is charged for it. An unclear card in the photo is free, so you never pay for guesses.
Your live balance is returned on every successful call as credits.remaining. Excessive request rates return 429; use exponential backoff. Streaming shows each card earlier but does not raise rate limits or throughput, and it costs the same. Football is one credit per photo with a result, also when the recognizer is not sure; a photo without a result is free.
Async jobs with webhooks
Sending many images without keeping a connection open for each one? Async jobs with a webhook callback are planned. If you need them, tell us at api@neosatoshi.com.
Ready to integrate?
Create an API key in your settings and start detecting cards in minutes. Detections use the same credit balance as the app.