Scan a card
curl --request POST \
--url https://api.cromos.so/v1/scan \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: multipart/form-data' \
--form image=@example-fileimport requests
url = "https://api.cromos.so/v1/scan"
files = { "image": ("example-file", open("example-file", "rb")) }
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('image', '<string>');
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
options.body = form;
fetch('https://api.cromos.so/v1/scan', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.cromos.so/v1/scan",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: multipart/form-data"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.cromos.so/v1/scan"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.cromos.so/v1/scan")
.header("Authorization", "Bearer <token>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.cromos.so/v1/scan")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"data": {
"analysis": {
"game": "<string>"
},
"matches": [
{
"score": 123,
"card": {
"id": "<string>",
"name": "<string>",
"category": "<string>",
"rarity": "<string>",
"number": "<string>",
"image": {
"low": "<string>",
"high": "<string>"
},
"expansion": {
"id": "<string>",
"name": "<string>"
}
}
}
],
"unidentified": {
"reason": "print_unreadable",
"message": "<string>"
}
},
"error": {}
}{
"data": {},
"error": {
"code": "<string>",
"message": "<string>"
}
}{
"data": {},
"error": {
"code": "<string>",
"message": "<string>"
}
}{
"data": {},
"error": {
"code": "<string>",
"message": "<string>"
}
}{
"data": {},
"error": {
"code": "<string>",
"message": "<string>"
}
}{
"data": {},
"error": {
"code": "<string>",
"message": "<string>"
}
}{
"data": null,
"error": {
"code": "scan_unavailable",
"message": "scanning is not configured"
}
}Scanning
Scan a card
Identify a card from a photo.
POST
/
v1
/
scan
Scan a card
curl --request POST \
--url https://api.cromos.so/v1/scan \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: multipart/form-data' \
--form image=@example-fileimport requests
url = "https://api.cromos.so/v1/scan"
files = { "image": ("example-file", open("example-file", "rb")) }
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('image', '<string>');
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
options.body = form;
fetch('https://api.cromos.so/v1/scan', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.cromos.so/v1/scan",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: multipart/form-data"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.cromos.so/v1/scan"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.cromos.so/v1/scan")
.header("Authorization", "Bearer <token>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.cromos.so/v1/scan")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"data": {
"analysis": {
"game": "<string>"
},
"matches": [
{
"score": 123,
"card": {
"id": "<string>",
"name": "<string>",
"category": "<string>",
"rarity": "<string>",
"number": "<string>",
"image": {
"low": "<string>",
"high": "<string>"
},
"expansion": {
"id": "<string>",
"name": "<string>"
}
}
}
],
"unidentified": {
"reason": "print_unreadable",
"message": "<string>"
}
},
"error": {}
}{
"data": {},
"error": {
"code": "<string>",
"message": "<string>"
}
}{
"data": {},
"error": {
"code": "<string>",
"message": "<string>"
}
}{
"data": {},
"error": {
"code": "<string>",
"message": "<string>"
}
}{
"data": {},
"error": {
"code": "<string>",
"message": "<string>"
}
}{
"data": {},
"error": {
"code": "<string>",
"message": "<string>"
}
}{
"data": null,
"error": {
"code": "scan_unavailable",
"message": "scanning is not configured"
}
}POST /v1/scan identifies a trading card from a photo of it. Send a roughly straight-on photo
of a single card. You get back the closest catalog matches, each with a confidence score.
The card does not have to fill the frame, since the service locates it and reads it up close,
but the more of the frame it fills the better: the collector number is the smallest print on
the card, and it is what pins the match.
Send a file
Sendmultipart/form-data with the photo in an image field (max 10 MB):
curl -X POST https://api.cromos.so/v1/scan \
-H "Authorization: Bearer $API_KEY" \
-F "image=@card.jpg"
Send a URL
Sendapplication/json with a publicly reachable https URL (max 10 MB, no redirects):
curl -X POST https://api.cromos.so/v1/scan \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://images.example.com/card.jpg"}'
The printed identity
The image is the entire request. There are no fields for the card’s name, number or set code, and that is deliberate: reading those off the card is the work you are calling this endpoint to do. The scan identifies the card two ways at once: the printed identity (name, collector number, set code) read from the photo, and visual similarity against the catalog. A legible print pins the match to the cards that agree with it, which keeps lookalikes out ofmatches; the
similarity ranks what is left. An unreadable print costs nothing: similarity decides alone,
less sharply. The collector line is small print, so a sharper photo turns a close visual race
into a single answer.
Anything read that matches no card is discarded rather than trusted, and any extra keys you
send are ignored.
The response
{
"data": {
"analysis": { "game": "pokemon" },
"unidentified": null,
"matches": [
{
"score": 0.91,
"card": {
"id": "base1-4",
"name": "Charizard",
"category": "Pokemon",
"rarity": "Rare",
"number": "4",
"image": {
"low": "https://assets.cromos.so/cards/base1-4/low.webp",
"high": "https://assets.cromos.so/cards/base1-4/high.webp"
},
"expansion": { "id": "base1", "name": "Base Set" }
}
}
]
},
"error": null
}
scoreis a similarity from 0 to 1, higher is better. Matches are best-first.- A confident scan returns a single match. A close race returns up to five, so show them and let the user pick.
- An empty
matches(with"game": null) is a valid 200, not an error, andunidentifiedthen says which kind of “don’t know” it was. - Photos are not stored.
When nothing matched
The scan returns nothing rather than a guess. A card it cannot read and cannot recognise scores about the same against thousands of unrelated cards, so a list built from that is noise wearing a confidence score, which is worse than an empty answer because an app cannot tell the two apart.{
"data": {
"analysis": { "game": null },
"matches": [],
"unidentified": {
"reason": "print_unreadable",
"message": "Could not read the card's text. Fill the frame with the card, straight on."
}
},
"error": null
}
reason | What happened | What helps |
|---|---|---|
print_unreadable | The name, number and set code could not be read, and nothing looked close | Retake: fill the frame, straight on, less glare |
no_confident_match | The print was read, but no card it points at matches the artwork | Usually a card not yet in the catalog |
unidentified is null whenever matches is non-empty.
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request | Body is not one of the two supported shapes |
| 400 | unsupported_image | Bytes are not a decodable image |
| 400 | url_not_allowed | URL is not https, or resolves to a non-public address |
| 400 | url_fetch_failed | URL could not be fetched, redirected, timed out after 10 seconds, or exceeded the size |
| 413 | payload_too_large | Image larger than 10 MB |
| 503 | scan_unavailable | Scanning is not configured on this deployment |
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Body
multipart/form-dataapplication/json