Upload a phone photo, get back the top-3 candidate cards with a confidence label and an image-quality score. Pipeline: OpenCV cascade → DINOv2-S 384-dim FAISS lookup over 80k+ vectors → Tesseract OCR (EN + CJK) → 5-signal weighted scoring → optional rotation-fallback.
{
"candidates": [
{ "card_id": 3782, "name": "Charizard", "set_name": "Base Set",
"score": 0.92, "rotation_used": 0 },
{ "card_id": 3801, "name": "Charizard ex", "set_name": "FireRed & LeafGreen",
"score": 0.71 }
],
"confidence_label": "high", // high | medium | low | conflict
"detection_method": "hybrid_embedding",
"capture_quality": {
"overall_score": 0.78,
"color_reliability": "high",
"glare_severity": "low",
"verdict": "likely_real",
"fake_risk": 0.04
},
"recognition_hint": null
}
Session-cookie users (web app, mobile app) are unlimited. API-key callers need Starter+. Daily caps mirror app/tiers.py:
curl -X POST \
-H "Authorization: Bearer $CSM_KEY" \
-F "file=@charizard_front.jpg" \
"https://collectorstashmarket.com/api/cards/recognize"
import os, requests, sys
CSM = "https://collectorstashmarket.com"
HDR = {"Authorization": f"Bearer {os.environ['CSM_KEY']}"}
with open(sys.argv[1], "rb") as f:
r = requests.post(f"{CSM}/api/cards/recognize", headers=HDR, files={"file": f}, timeout=30)
if r.status_code == 403:
sys.exit("Recognition requires a Starter+ key.")
r.raise_for_status()
data = r.json()
top = (data["candidates"] or [None])[0]
print(f"{top['name']} ({top['set_name']}) — {data['confidence_label']} ({top['score']:.0%})")
# Self-learn: confirm the correct candidate so it becomes a future-scan exemplar
if top and data["confidence_label"] in ("medium", "low"):
requests.post(
f"{CSM}/api/cards/recognize/confirm",
headers=HDR,
json={"scan_id": data["scan_id"], "card_id": top["card_id"], "correct": True},
)
async function recognise(file: File, apiKey: string) {
const fd = new FormData();
fd.append("file", file);
const r = await fetch("https://collectorstashmarket.com/api/cards/recognize", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}` },
body: fd,
});
if (!r.ok) throw new Error(await r.text());
return r.json() as Promise<{
candidates: { card_id: number; name: string; set_name: string; score: number }[];
confidence_label: "high" | "medium" | "low" | "conflict";
capture_quality: { overall_score: number; verdict: string; fake_risk: number };
}>;
}
<?php
$key = getenv("CSM_KEY");
$ch = curl_init("https://collectorstashmarket.com/api/cards/recognize");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer $key"],
CURLOPT_POST => 1,
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_POSTFIELDS => ["file" => new CURLFile("photo.jpg")],
]);
$resp = json_decode(curl_exec($ch), true);
echo "Best match: " . $resp["candidates"][0]["name"] . PHP_EOL;
echo "Confidence: " . $resp["confidence_label"] . PHP_EOL;
undiciimport { fetch, FormData, fileFrom } from "undici";
const fd = new FormData();
fd.set("file", await fileFrom("./photo.jpg", "image/jpeg"));
const r = await fetch("https://collectorstashmarket.com/api/cards/recognize", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.CSM_KEY}` },
body: fd,
});
console.log(await r.json());
For graded slabs use the dedicated back-scan endpoint to also resolve the cert number:
curl -X POST \
-H "Authorization: Bearer $CSM_KEY" \
-F "file=@cgc_back.jpg" \
"https://collectorstashmarket.com/api/cards/recognize/slab-back?card_id=3782&grader_hint=CGC"
Returns cert_info + a follow_up_actions array (verify-URL, 👍/👎 confirm, create-listing). Slow grader lookups (CGC behind Cloudflare) complete asynchronously — poll /api/recognition/scan/{scan_id} for the final status.