Wie baue ich eine Scanner-App für Sammelkarten?
Direkte Antwort
Eine Scanner-App für Sammelkarten nimmt ein Foto auf, schickt es an eine API zur Kartenerkennung und zeigt die Karten an, die zurückkommen. Mit der NeoSatoshi Detection API ist das eine einzige POST-Anfrage: Ein Bild als Base64 oder ein Bildlink geht hinein, JSON mit Name, Set, Nummer, Cardmarket-Produkt-ID und Tagespreis pro Karte kommt heraus, für Pokémon, One Piece, Disney Lorcana, Yu-Gi-Oh! und Magic. Das Python-Skript unten läuft so, wie es ist, mit dem öffentlichen Demo-Schlüssel (10 Erkennungen pro Tag, ohne Anmeldung); für den produktiven Einsatz erstellst du mit jedem bezahlten Plan einen eigenen Schlüssel.
Was du brauchst
- Python 3 und die Bibliothek requests (
pip install requests). - Ein Foto mit einer oder mehreren Karten, JPEG oder PNG, bis 12 MB. Mehrere Karten pro Foto gehen bei Pokémon; bei den anderen Spielen schickst du eine Karte pro Foto.
- Einen API-Schlüssel: den öffentlichen Demo-Schlüssel im Skript unten, oder später deinen eigenen in der Umgebungsvariable
NEOSATOSHI_API_KEY.
Schritt 1: ein Foto scannen
Speichere das als scan.py. Es liest das Foto, schickt es als Base64 und gibt jede Karte mit Set, Nummer, Cardmarket-Produkt-ID, 30-Tage-Durchschnittspreis und Konfidenz aus.
Python: scan.py
import base64
import os
import sys
import requests
API_URL = "https://europe-west1-neosatoshi.cloudfunctions.net/detectApi"
# Public demo key: 10 detections per day per network, no signup.
# Set NEOSATOSHI_API_KEY to use your own key instead.
API_KEY = os.environ.get(
"NEOSATOSHI_API_KEY",
"nsk_live_9c253fbed995042a98280ad13e5c47d3c4a59c9b4f0f7274",
)
def detect(path, tcg="pokemon"):
"""Send one photo and return the parsed JSON response."""
with open(path, "rb") as f:
image = base64.b64encode(f.read()).decode("ascii")
res = requests.post(
API_URL,
headers={"Authorization": f"Bearer {API_KEY}"},
json={"image": image, "tcg": tcg},
timeout=120, # synchronous: the cards come back in this response
)
if res.status_code == 422:
return {"count": 0, "cards": []} # no card found, nothing charged
res.raise_for_status()
return res.json()
if __name__ == "__main__":
tcg = sys.argv[2] if len(sys.argv) > 2 else "pokemon"
data = detect(sys.argv[1], tcg)
for card in data["cards"]:
card_set = card.get("set") or {}
cm = (card.get("prices") or {}).get("cardmarket") or {}
print(
card.get("name"),
"|", card_set.get("name"), card_set.get("number"),
"| Cardmarket ID", card.get("cardmarket_id"),
"| avg30", cm.get("avg30"), cm.get("currency", ""),
"| confidence", card.get("confidence"),
)
if "demo" in data:
print("Demo detections left today:", data["demo"]["remaining_today"])Starte python scan.py card.jpg für Pokémon oder gib das Spiel mit: python scan.py card.jpg lorcana (ebenso magic, yugioh und onepiece). Mit dem Demo-Schlüssel zeigt die letzte Zeile, wie viele Erkennungen heute noch übrig sind.
Schritt 2: einen Link statt einer Datei schicken
Liegt das Foto schon online, schick image_url statt image und spar dir das Base64. So kann auch ein Chat-Assistent ohne Codeausführung die API testen. Mit curl:
curl
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"}'Dasselbe in Python, mit dem Schlüssel aus scan.py:
Python
import requests
from scan import API_KEY, API_URL
res = requests.post(
API_URL,
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"image_url": "https://neosatoshi.com/static/images/example_api_lorcana.jpg",
"tcg": "lorcana",
},
timeout=120,
)
res.raise_for_status()
for card in res.json()["cards"]:
print(card.get("full_name") or card.get("name"), card.get("cardmarket_id"))- Der Link muss öffentlich sein, https auf dem Standardport, höchstens 2.048 Zeichen lang und ohne Benutzername oder Passwort darin.
- Die Datei muss ein JPEG oder PNG sein (geprüft am Inhalt, nicht am Namen), höchstens 12 MB gross und innerhalb von 15 Sekunden herunterladbar. Bis zu 3 Weiterleitungen werden verfolgt.
- Signierte Links aus deinem eigenen Speicher funktionieren, solange sie beim Eintreffen der Anfrage aus dem Internet erreichbar sind.
- Schick entweder
imageoderimage_url, nie beides.
Schritt 3: einen Ordner in eine CSV scannen
Speichere das neben scan.py als batch.py und starte python batch.py photos/ (oder python batch.py photos/ magic). Es schickt die Fotos nacheinander, wiederholt vorübergehende Fehler und schreibt eine Zeile pro erkannter Karte in cards.csv.
Python: batch.py
import csv
import pathlib
import sys
import time
import requests
from scan import detect # the script from step 1, saved as scan.py
FIELDS = ["file", "name", "set", "number", "rarity", "language",
"cardmarket_id", "trend_eur", "avg30_eur", "confidence"]
def error_code(response):
try:
return response.json().get("error")
except ValueError:
return None
def detect_with_retry(path, tcg, attempts=3):
for attempt in range(attempts):
last = attempt == attempts - 1
try:
return detect(path, tcg)
except requests.Timeout:
if last:
raise
except requests.HTTPError as err:
status = err.response.status_code
if error_code(err.response) == "demo_limit_reached":
sys.exit("Demo limit reached for today. Use your own API key.")
if status == 402:
sys.exit("No card scans left on this key.")
if status not in (429, 500, 502, 504) or last:
raise
time.sleep(5 * 2 ** attempt) # wait 5 s, then 10 s
def main(folder, tcg="pokemon", out="cards.csv"):
photos = sorted(p for p in pathlib.Path(folder).iterdir()
if p.suffix.lower() in (".jpg", ".jpeg", ".png"))
with open(out, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=FIELDS)
writer.writeheader()
for path in photos:
data = detect_with_retry(path, tcg)
for card in data["cards"]:
card_set = card.get("set") or {}
cm = (card.get("prices") or {}).get("cardmarket") or {}
writer.writerow({
"file": path.name,
"name": card.get("name"),
"set": card_set.get("name"),
"number": card_set.get("number"),
"rarity": card.get("rarity"),
"language": card.get("language"),
"cardmarket_id": card.get("cardmarket_id"),
"trend_eur": cm.get("trend"),
"avg30_eur": cm.get("avg30"),
"confidence": card.get("confidence"),
})
print(f"{path.name}: {len(data['cards'])} card(s)")
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else "pokemon")Die Spalten sind ein Anfang. Ergänze reverse_holo und first_edition für Pokémon, set.code und edition für Yu-Gi-Oh! oder prices.cardmarket_foil für Lorcana und Magic. Die Felder pro Spiel stehen in der Dokumentation (auf Englisch).
Timeouts
Die API ist synchron: Die Karten kommen in der Antwort auf dieselbe Anfrage zurück, es gibt nichts abzufragen. Eine warme Anfrage dauert etwa 5 Sekunden. Die erste Anfrage nach einer ruhigen Phase (ein Kaltstart) dauert 10 bis 15 Sekunden. Der Server gibt nach etwa 90 Sekunden auf und antwortet mit 504, setz das Timeout im Client also auf 120 Sekunden und nicht tiefer. Schick die Fotos eines Ordners nacheinander oder wenige parallel, nicht Hunderte auf einmal.
Fehlerbehandlung
- 400: Die Anfrage ist falsch, zum Beispiel kein Bild, kein JPEG oder PNG, über 12 MB, sowohl
imageals auchimage_urloder ein unbekanntestcg. Korrigier die Anfrage; wiederholen hilft nicht. - 401: Der Schlüssel fehlt oder ist falsch. Prüf den Header
Authorization: Bearer. - 402: In deinem Plan sind keine Karten-Scans mehr übrig.
- 422 (
no_card_detected): Es wurde keine Karte gefunden. Das wird nicht berechnet. Zeig deinem Nutzer einen Hinweis zu Licht und Bildausschnitt. Mit dem Demo-Schlüssel zählt es trotzdem als eine der 10 täglichen Anfragen. - 429: entweder
demo_limit_reached(die 10 Anfragen pro Tag des Demo-Schlüssels für dein Netzwerk sind aufgebraucht; der Zähler startet um Mitternacht UTC neu) oderrate_limited(zu viele Anfragen pro Minute mit deinem Schlüssel; warte und wiederhole mit wachsender Pause). - 500 oder 502: ein vorübergehender Fehler. Nach einer kurzen Pause wiederholen. Mit dem Demo-Schlüssel gibt ein Fehler auf unserer Seite den Versuch zurück.
- 504 (
detection_timeout): Die Erkennung hat zu lange gedauert, oft bei einem Kaltstart. Einmal wiederholen.
Fehlerantworten enthalten einen error-Code und eine message, logge also beides. Erfolgreiche Antworten enthalten eine request_id; gib sie an, wenn du dich beim Support meldest.
Schritt 4: das Ergebnis deinen Nutzern zeigen
- Zeig die Konfidenz an und markiere Karten unter einer Schwelle, die du wählst, mit "bitte prüfen".
- Biete
alternativesals Dropdown an, damit ein falscher erster Treffer mit einem Klick korrigiert ist. - Ist ein Druck offen (zum Beispiel
print_unconfirmedbei Magic oderrarity_unconfirmedbei Yu-Gi-Oh!), frag deinen Nutzer, statt zu raten. Die API lässt den Preis in diesem Fall absichtlich leer. - Zeig keine Zustandsnote an: Die API beurteilt den Zustand nicht, den setzt dein Nutzer.
- Behalte den Schlüssel auf deinem Server. Bau ihn nie in eine Handy-App oder Browser-Code ein, wo er ausgelesen werden kann. Eine Handy-App lädt das Foto auf dein Backend, und dein Backend ruft die API auf.
Schritt 5: auf einen eigenen Schlüssel wechseln
Der Demo-Schlüssel ist zum Testen da. Für den produktiven Einsatz erstellst du einen eigenen Schlüssel in den Einstellungen unter API-Schlüssel. Schlüssel brauchen einen bezahlten Plan: Start-Up, der kleinste bezahlte Plan, kostet 19 pro Monat (15 pro Monat bei jährlicher Zahlung, derselbe Betrag in EUR, USD oder CHF) und enthält 1.000 Karten-Scans pro Monat, gemeinsam mit der NeoSatoshi-App. Pro erkannter Karte wird ein Karten-Scan verbraucht, Fotos ohne Karte kosten nichts, und jede Antwort mit deinem eigenen Schlüssel zeigt credits.remaining statt demo. Aktuelle Pläne: Preise. Setz den Schlüssel mit export NEOSATOSHI_API_KEY=..., und die Skripte oben nutzen ihn ohne Änderung.
Mehr Beispiele und das volle Schema
- Dokumentation (auf Englisch) mit Live-Beispielen, Snippets für curl, JavaScript und Python, allen Feldern pro Spiel und einer Testbox.
- OpenAPI-Spezifikation, um einen Client in deiner Sprache zu erzeugen.
Häufige Fragen
Kann ich die API direkt aus einer Handy-App aufrufen?
Technisch ginge das, aber dann könnte jeder den Schlüssel aus der App auslesen. Schick das Foto an dein eigenes Backend und ruf die API von dort auf.
Welche Programmiersprachen kann ich nutzen?
Jede Sprache, die einen HTTPS-POST mit JSON schicken kann. Die Dokumentation hat Beispiele für curl, JavaScript und Python, und aus der OpenAPI-Spezifikation lassen sich Clients für die meisten anderen erzeugen.
Was kostet ein Scan?
Ein Karten-Scan pro erkannter Karte aus dem Monatsguthaben deines Plans; ein Foto mit drei Pokémon-Karten braucht drei, ein Foto ohne Karte keinen. Unter Preise steht, wie viele Scans jeder Plan enthält.
Welche API sollte ich für meine App wählen?
Das hängt von deinen Spielen, Sprachen und deinem Marktplatz ab. Unser datierter Vergleich mit Ximilar, CardSight, Open-Source-Modellen und Bild-KI-Modellen: welche API zur Kartenerkennung 2026.
Weiterlesen
Zur Dokumentation und zum Demo-Schlüssel
Alles aus dieser Anleitung mit allen Feldern pro Spiel, dazu eine Testbox für deine eigenen Fotos. 10 Gratis-Erkennungen pro Tag, ohne Anmeldung.
Für die ersten 10 Erkennungen ist kein Account nötig.