Dokumentation
Kennzeichen API – Referenz
Vollständige Referenz für die REST-API zur Kennzeichen-Reservierung.
Keine Registrierung erforderlich
Du brauchst keinen Account, um die Doku zu lesen. Für einen API-Key fordere Zugang an — wir melden uns mit Sandbox-Keys und allen Details.
Inhalt
Einstieg
Einführung
Die Kennzeichen API ist eine REST-Schnittstelle zur Reservierung von Wunschkennzeichen über 400+ Zulassungsbehörden in Deutschland. Sie deckt beide Schritte ab: die unverbindliche Verfügbarkeitsprüfung und die verbindliche Reservierung mit amtlichem Nachweis. Alle Anfragen und Antworten verwenden JSON. Zur Integration genügt ein einfacher HTTP-Client — kein SDK erforderlich.
Die API bietet Endpunkte zum Prüfen der Verfügbarkeit, Erstellen von Reservierungsaufträgen und Abrufen amtlicher Reservierungscodes und PDF-Nachweise sowie Webhooks für Push-Benachrichtigungen bei Statuswechseln.
Authentifizierung und Umgebungen
Basis-URL
Der Schlüssel bestimmt die Adresse. Jede Umgebung hat ihre eigene Basis-URL, und ein Schlüssel gilt nur auf der zu ihm gehörenden. Alle Pfade beginnen mit /v1.
| Umgebung | Basis-URL | Schlüssel |
|---|---|---|
| Produktion | https://api.kennzeichenapi.de/v1 | knz_live_… |
| Sandbox | https://dev.kennzeichenapi.de/v1 | knz_test_… |
| Staging | https://api.staging.kennzeichenapi.de/v1 | knz_live_… |
Ein Schlüssel auf der falschen Adresse wird abgewiesen — mit 403 und api_key_environment_mismatch. Die Antwort nennt im Kopf x-expected-key-mode, welcher Modus dort erwartet wird. Das ist der häufigste Fehlschlag beim allerersten Aufruf: Wer seinen knz_test_-Schlüssel gegen api.kennzeichenapi.de schickt, bekommt 403 — nicht, weil der Schlüssel ungültig wäre, sondern weil er auf dev.kennzeichenapi.de gehört.
Sandbox (dev.kennzeichenapi.de): feste Testfälle, keine Übermittlung an Zulassungsstellen, keine Kosten. Staging (api.staging.kennzeichenapi.de) ist etwas anderes und nicht der Übungsplatz: Dort gilt der knz_live_-Schlüssel, die Verfügbarkeit ist simuliert und verbindliche Reservierungen sind gesperrt — aber Datenbank, Kontingent und Zähler teilt es sich mit der Produktion.
Authentifizierung
Jede Anfrage muss einen gültigen API-Key im Authorization-Header enthalten — auch das Nachfassen auf eine laufende Prüfung. Schlüssel mit knz_test_ sind Sandbox-Keys und gehören auf dev.kennzeichenapi.de, knz_live_ sind Produktions-Keys für api.kennzeichenapi.de (und Staging). Welcher Schlüssel wohin gehört, steht oben unter Basis-URL.
curl https://api.kennzeichenapi.de/v1/reservations \ -H "Authorization: Bearer knz_live_..."API-Keys niemals in clientseitigem Code oder öffentlichen Repositories speichern. Schlüssel können im Dashboard jederzeit rotiert werden.
Die Endpunkte
Bezirke finden
Jede Prüfung und jede Reservierung braucht den fünfstelligen kreisschluessel. Diese Liste liefert ihn — zusammen mit der zuständigen Behörde, allen dort gültigen Unterscheidungszeichen und den anfallenden Gebühren.
/v1/districtscurl https://api.kennzeichenapi.de/v1/districts \ -H "Authorization: Bearer knz_live_..."{ "kreisschluessel": "09277", "name": "Pfarrkirchen", "behoerde": "LRA Rottal-Inn", "unterscheidungszeichen": "PAN", "unterscheidungszeichenAlle": ["EG", "GRI", "PAN", "VIB"], "bundesland": "BY", "status": "online", "reservationCostEur": 12.8, "upfrontFee": { "minEur": 12.8, "maxEur": 12.8 }}status ist online oder offline. Offline-Bezirke nehmen derzeit keine Reservierung an.
Mehrere Unterscheidungszeichen je Bezirk
Viele Kreise geben mehr als ein Kürzel aus. unterscheidungszeichen nennt das Hauptkürzel, unterscheidungszeichenAlle alle wählbaren. Wer den Kreisschlüssel zu einem bestimmten Kürzel sucht, filtert über die vollständige Liste: Der Landkreis Rottal-Inn ist über PAN, VIB, EG und GRI erreichbar — alle vier führen auf 09277.
Vom Kürzel zum Kreisschlüssel
Dafür gibt es einen eigenen Filter: GET /v1/districts?uz=EG trifft exakt und durchsucht auch die Nebenkürzel. Eine gesonderte Zuordnungstabelle brauchen Sie nicht. Die Reihenfolge in unterscheidungszeichenAlle hat keine Bedeutung — das Hauptkürzel steht nicht zwingend vorn (bei 109 der 400 Bezirke tut es das nicht), und alphabetisch ist die Liste auch nicht durchgängig. Prüfen Sie mit includes, nicht über den Index.
Schicken Sie das gewählte Kürzel mit
Ohne uz setzen wir das Hauptkürzel des Bezirks ein — Ihr Kunde wählt VIB und bekäme eine Auskunft zu PAN, ohne Fehlermeldung. Das betrifft die Hälfte aller Bezirke: 200 von 400 führen mehr als ein Kürzel.
Wenn ein Kürzel mehrere Bezirke trifft
?uz=VIB liefert drei Kreise. Nehmen Sie dann nicht den ersten Treffer: Bei diesen Zeichen ist der Nummernvorrat zwischen den Stellen aufgeteilt, jede vergibt nur ihren Teil — eine Auskunft der falschen Stelle ist nicht bloß unscharf, sondern falsch. Maßgeblich ist der Wohnsitz des Halters (§ 75 Abs. 2 FZV). Den passenden Kreisschlüssel liefert GET /v1/zustaendigkeit?plz=84347 — ohne Schlüssel abrufbar, zählt weder auf die Minutengrenze noch auf das Monatskontingent. Landet eine Suche bei einem geteilten Vorrat, trägt die Antwort zusätzlich ein Feld geteilterVorrat mit den anderen Behörden und einem fertigen Hinweistext.
Behörde und Standort
name ist der Standort der Dienststelle, behoerde die zuständige Behörde. Beide weichen häufig ab — der Bezirk Elmenhorst gehört zum LK Herzogtum Lauenburg. Für die Anzeige beim Endkunden gehört behoerde in die Bestätigung.
Gebühren
reservationCostEur nennt die Reservierungsgebühr der Behörde, upfrontFee den vorfällig zu zahlenden Anteil. Bei 360 der 400 Bezirke fällt vorab nichts an; feste Beträge sind 2.60 (13 Bezirke) und 12.80 (13 Bezirke). Bei zwei Bezirken nennt die Behörde eine Spanne — dann weichen minEur und maxEur voneinander ab, und Sie brauchen dafür einen eigenen Zweig. Wichtig: 0 heißt „kostet nichts", null heißt „nicht bekannt" — die beiden Fälle sind nicht dasselbe.
Verfügbarkeit prüfen
Vor einer Reservierung lässt sich unverbindlich prüfen, ob ein Wunschkennzeichen in einem Bezirk frei ist. Der Endpunkt akzeptiert GET (Query-Parameter) oder POST (JSON) mit denselben Feldern: ks (5-stelliger Kreisschlüssel), uz (Unterscheidungszeichen), buchstaben, ziffern.
/v1/searchcurl https://api.kennzeichenapi.de/v1/search?ks=09162&uz=M&buchstaben=AB&ziffern=1234 \ -H "Authorization: Bearer knz_live_..."# 200 OK — Prüfung gestartet{ "ok": true, "outcome": "QUEUED", "jobId": "a1b2c3d4-…", "pollUrl": "/v1/search/jobs/a1b2c3d4-…"}# dann nachfassen, bis job.status done oder failed ist:# GET /v1/search/jobs/a1b2c3d4-… (mit demselben Authorization-Header){ "ok": true, "job": { "id": "a1b2c3d4-…", "status": "done", "attempts": 1 }, "result": { "outcome": "AVAILABLE", "count": 1, "plates": ["M-AB 1234"] }} # ACHTUNG: kein outcome auf oberster Ebene — es liegt unter result.# payload.outcome ist hier IMMER undefined.Antwortet die API mit outcome: "QUEUED" plus jobId/pollUrl, rufe die pollUrl ab — mit demselben Authorization-Header. Bei Behörden, die wir über einen Browser bedienen, ist QUEUED der Regelfall und nicht die Ausnahme; manche Bezirke antworten direkt mit dem Endergebnis. Nachfassen ist kostenlos und zählt weder auf die Minutengrenze noch auf das Monatskontingent. Die Prüfung ist unverbindlich und reserviert nichts.
Abbruchbedingung: job.status — nicht result.outcome.
Fasse nach, solange job.status queued oder running ist. Bei done und failed ist der Auftrag beendet — beides endgültig, mehr kommt nicht.
Umgekehrt ist result bei einem laufenden Auftrag in zweierlei Hinsicht irreführend. Erstens ist es meistens null: Solange kein Versuch gelaufen ist, trägt der Auftrag gar kein Ergebnis — payload.result.outcome wirft dann einen TypeError, statt einen Wert zu liefern. Zweitens steht dort nach einem Fehlversuch der Zwischenbefund dieses Versuchs, während der Auftrag weiterläuft — am 08.08.2026 live gemessen: job.status: "running", attempts: 2, result.outcome: "TEMPORARILY_UNAVAILABLE" — und drei Sekunden später done mit AVAILABLE. Wer das als Endergebnis liest, meldet einen Ausfall für ein Kennzeichen, das frei ist. Lies result deshalb erst, wenn job.status den Auftrag als beendet meldet — und greif vorher nie ungeprüft hinein. failed heißt übrigens nicht „vergeben“ — was los war, sagt dann result.outcome.
Hier stand bis zum 13.08.2026 „bis das Ergebnis AVAILABLE oder NOT_AVAILABLE lautet“. Das war eine Endlosschleife: Ein beendeter Auftrag trägt auch TEMPORARILY_UNAVAILABLE, OFFLINE oder ACTION_REQUIRED — dann ist job.status längst failed, und die alte Bedingung wurde nie erfüllt.
Hier stand bis zum 13.08.2026 „Es steht dort auf QUEUED, solange nichts feststeht“. Das war in beide Richtungen falsch: QUEUED wird nie in das Auftragsergebnis geschrieben — es ist der Ausgang der Suchantwort, nicht des Auftrags. Wer sich darauf verließ, dass dort ein String steht, bekam einen TypeError auf null — dieselbe Fehlerklasse, gegen die dieser Kasten warnt, eine Ebene tiefer.
Reservierung erstellen
Ein Commit stößt die verbindliche Behördenreservierung asynchron an. Er benötigt einen LIVE-API-Key, den Header Idempotency-Key mit 8–128 Zeichen sowie authorityTermsAccepted: true. Die Antwort bestätigt mit HTTP 202 nur die Annahme des Auftrags, noch nicht die amtliche Reservierung.
Pflichtfelder sind ks, uz, customer und einwilligung: true. Im Kundenobjekt sind Straße, PLZ und Ort erforderlich; Privatpersonen senden zusätzlich Vorname und Nachname, Unternehmen stattdessen anrede: "Firma" und den exakten firmenname. Je nach Zulassungsbezirk werden bei Privatpersonen außerdem Anrede, Geburtsdatum und Geburtsort benötigt; welche Felder ein Bezirk verlangt, liefert GET /v1/districts/{kreisschluessel}/reservation-requirements. customerReference ist eine optionale eigene Auftragsreferenz.
buchstaben und ziffern stehen bewusst nicht in dieser Liste, obwohl Sie beide brauchen: Das Schema nimmt eine Anfrage ohne sie an, die Reservierung weist sie dann aber mit 422 invalid_plate_formatab. Ein Kennzeichen ohne Buchstaben oder ohne Ziffern ist ein Platzhalter, und ein Platzhalter lässt sich nicht reservieren. Der Unterschied ist für Ihre Fehlerbehandlung wichtig: Sie bekommen keinen 400er über ein fehlendes Feld, sondern einen 422er über ein ungültiges Kennzeichen.
Für ein Unternehmen setzt du customer.anrede auf "Firma" und übergibst den exakten rechtlichen Namen in customer.firmenname. Der ältere Vorname-/Nachname-Fallback bleibt nur für unverbindliche Prepare-Aufträge kompatibel; ein Commit ohne exakten Firmennamen wird abgelehnt.
Hier standen bis zum 14.08.2026 vier Felder zu viel (buchstaben, ziffern, Hausnummer und E-Mail). hausnummer ist wirklich optional. Und email ebenfalls — aber Weglassen ist hier keine Kleinigkeit: Wir setzen dann unsereAdresse ein, damit das Behördenformular durchgeht. Die Rückmeldung der Zulassungsstelle landet danach bei uns und nicht bei Ihrem Endkunden. Wer sie erreichen will, schickt seine Adresse mit.
/v1/reservationscurl -X POST https://api.kennzeichenapi.de/v1/reservations \ -H "Authorization: Bearer knz_live_..." \ -H "Idempotency-Key: order-1042-reservation-1" \ -H "Content-Type: application/json" \ -d '{' "ks": "03403", "uz": "OL", "buchstaben": "AA", "ziffern": "123", "executionMode": "commit", "einwilligung": true, "authorityTermsAccepted": true, "customerReference": "ORDER-1042", "customer": { "anrede": "Frau", "vorname": "Erika", "nachname": "Muster", "strasse": "Musterweg", "hausnummer": "1", "plz": "26122", "ort": "Oldenburg", "email": "erika@example.de", "geburtsdatum": "01.01.1990", "geburtsort": "Oldenburg", "land": "Deutschland" } }'# 202 Accepted{ "ok": true, "outcome": "RESERVATION_QUEUED", "jobId": "053e4ce1-0a6c-42b5-a2b5-4b5d08bb8945", "statusUrl": "/v1/reservations/053e4ce1-0a6c-42b5-a2b5-4b5d08bb8945", "plate": "OL-AA 123", "reservationStatus": "queued", "executionMode": "commit", "availabilityRecheck": { "required": true, "performed": true, "outcome": "AVAILABLE", "cached": false }}Wiederholst du denselben Request mit demselben Idempotency-Key, erhältst du denselben Auftrag. Wird derselbe Schlüssel mit einem veränderten Payload verwendet, antwortet die API mit HTTP 409.
Status abfragen
Gibt den aktuellen Auftragsstatus anhand der jobId aus dem POST zurück. Nur status: "reserved" bestätigt eine amtliche Reservierung. Dann enthält reservationCode die Behörden-PIN beziehungsweise den Reservierungscode, sofern die Zulassungsstelle einen ausgibt.
reservationId ist portalabhängig und oft null — im Beispiel unten steht sie, weil manche Portale eine eigene Vorgangsnummer vergeben. Über 90 Tage gemessen war sie bei 49 % der bestätigten Reservierungen leer: Ganze Portalfamilien nennen auf ihrer Bestätigungsseite überhaupt keine solche Nummer. Ein leeres Feld ist dort also kein Fehlschlag und kein Grund, den Auftrag erneut zu senden. Verlassen Sie sich auf reservationCode und validUntil — beide lagen in denselben 90 Tagen bei jeder bestätigten Reservierung vor.
/v1/reservations/{jobId}curl https://api.kennzeichenapi.de/v1/reservations/053e4ce1-0a6c-42b5-a2b5-4b5d08bb8945 \ -H "Authorization: Bearer knz_live_..."# 200 OK{ "ok": true, "reservation": { "id": "053e4ce1-0a6c-42b5-a2b5-4b5d08bb8945", "reference": "RES-2A7F-9C31", "customerReference": "ORDER-1042", "kreisschluessel": "03403", "plate": "OL-AA 123", "status": "reserved", "outcome": "RESERVED", "reservationId": "R-77120", "reservationCode": "123456", "validUntil": "2026-08-19T23:59:59Z", "confirmation": { "codeAvailable": true, "documentsAvailable": true }, "action": null, "documents": [{ "id": "7c42b6e5-6a7a-4f80-8f2c-821852d0be32", "kind": "CONFIRMATION", "filename": "reservierungsbestaetigung.pdf", "contentType": "application/pdf", "sizeBytes": 85421, "sha256": "9d6f8a3d7b3c7186acfd3b86d7ac67b4611f489003f459fe8452fe9156fd6e92", "downloadUrl": "https://signed-storage.example/reservierungsbestaetigung.pdf?token=...", "downloadUrlExpiresAt": "2026-07-20T16:15:00Z", "downloadStatus": "AVAILABLE", "downloadError": null }] }}Amtliche PDF-Dokumente stehen in documents[]. Die downloadUrl ist privat signiert und nur kurz gültig; nach Ablauf liefert ein erneuter GET-Aufruf eine neue URL. Falls die Signierung vorübergehend fehlschlägt, bleiben Reservierungsdaten und Dokumentmetadaten verfügbar; das Dokument meldet dann downloadStatus: "UNAVAILABLE" und kann beim nächsten GET erneut signiert werden. Bei action_required benennt action.type den noch offenen Schritt.
Webhooks
Reservierungen werden asynchron verarbeitet und dauern manchmal länger. Statt zu pollen, kannst du dir jeden Statuswechsel per Webhook zustellen lassen. Endpunkte legst du im Kundendashboard unter Einstellungen → Webhooks an (max. 10 aktive je Konto). Die Ziel-URL muss HTTPS sein und auf einen öffentlich auflösbaren Host zeigen (kein localhost, keine internen/privaten IPs, keine Zugangsdaten in der URL). Bei jedem Statuswechsel eines Reservierungsauftrags senden wir ein signiertes reservation.updated-Event. Der Status-Endpunkt bleibt der dauerhafte Polling-Fallback.
Jede Zustellung trägt vier Header:
| Header | Bedeutung |
|---|---|
| X-KennzeichenAPI-Event | Event-Typ, z. B. reservation.updated |
| X-KennzeichenAPI-Delivery | Eindeutige Zustell-ID — zur Deduplizierung (Zustellung erfolgt mindestens einmal). |
| X-KennzeichenAPI-Timestamp | Unix-Zeit in Sekunden — Teil des signierten Strings. |
| X-KennzeichenAPI-Signature | Signatur im Format v1=<hex> (siehe unten). |
Beispiel-Payload (das reservation.updated-Event):
{ "schemaVersion": 1, "event": "reservation.updated", "occurredAt": "2026-07-19T14:55:12Z", "data": { "id": "053e4ce1-0a6c-42b5-a2b5-4b5d08bb8945", "reference": "WO-2026-000123", "customerReference": "bestellung-42", "kreisschluessel": "09162", "plate": "M-AB 1234", "status": "reserved", "outcome": "RESERVED", "reservationId": "R-77120", "reservationCode": "472918", "validUntil": "2026-08-19T23:59:59Z", "confirmation": { "codeAvailable": true, "documentsAvailable": true }, "action": null, "updatedAt": "2026-07-19T14:55:12Z" }}status ist einer von queued, processing, reserved, action_required, failed. Bei action_required wird noch ein Schritt von uns bearbeitet; du bekommst den finalen Status automatisch als weiteres reservation.updated-Event. Amtliche PDFs werden nicht im Webhook übertragen; confirmation.documentsAvailable signalisiert, dass eine kurzlebige Download-URL über den Status-Endpunkt bereitsteht.
Signatur prüfen
Die Signatur ist v1= gefolgt von einem HMAC-SHA256 (Hex, Kleinbuchstaben) über den String <timestamp>.<raw-body>. Schlüssel ist dein whsec_…-Secret (beim Anlegen/Rotieren einmalig im Dashboard angezeigt). Signiere den rohen Body vor jedem JSON-Parsen.
const crypto = require("crypto");const ts = req.headers["x-kennzeichenapi-timestamp"];const sig = req.headers["x-kennzeichenapi-signature"];const raw = rawRequestBody; // Buffer/String VOR JSON.parseconst expected = "v1=" + crypto.createHmac("sha256", secret) .update(ts + "." + raw).digest("hex");const fresh = Math.abs(Date.now() / 1000 - Number(ts)) <= 300;const ok = fresh && sig && sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));// ok === false => mit 400 ablehnen und NICHT verarbeitenLehne Zustellungen ab, deren Timestamp mehr als ~5 Minuten von der aktuellen Zeit abweicht (Replay-Schutz), und dedupliziere über X-KennzeichenAPI-Delivery.
Zustellung & Wiederholung
Antworte innerhalb weniger Sekunden mit einem 2xx. Jede andere Antwort (oder ein Timeout) gilt als Fehlschlag und wird mit wachsendem Abstand erneut versucht (ca. 15 s, 30 s, 1 min, 2 min … max. 6 h) bis zu 12 Versuchen; danach gilt die Zustellung als DEAD und wird nicht weiter versucht. Da mehrere Versuche denselben Auftrag betreffen können, verarbeite idempotent und nutze notfalls den Status-Endpunkt zum Abgleich. Über „Test senden" im Dashboard löst du eine sofortige Zustellung mit dem Event webhook.test aus, um Empfang und Signaturprüfung zu verifizieren.
Freigabe und Storno
Die behördliche Freigabe einer amtlich bestätigten Reservierung ist derzeit nicht über die API implementiert.
DELETE /v1/reservations/{jobId} ist kein Storno, sondern der Löschweg nach Art. 17: Er entfernt die Halterdaten unwiderruflich, die Reservierung bleibt bestehen.
Bitte wende dich für die Freigabe direkt an die zuständige Zulassungsstelle. Halte dafür reservationCode bereit — und reservationId, falls das Portal eine vergeben hat (bei knapp der Hälfte der Bezirke gibt es keine). Bis zur Bestätigung der Behörde ist die Reservierung als aktiv zu behandeln.
Fehlerbehandlung
Status-Werte
Eine Reservierung durchläuft folgende Status-Werte:
- queuedDer verbindliche Auftrag wurde angenommen und wartet auf die Verarbeitung.
- processingDer Auftrag wird aktuell bei der zuständigen Behörde bearbeitet.
- reservedDie Behörde hat die Reservierung eindeutig bestätigt. Erst dieser Status gilt als amtlicher Erfolg.
- action_requiredDer Vorgang erfordert einen zusätzlichen Schritt, bevor die Behörde bestätigen kann. Das Kennzeichen ist noch nicht amtlich reserviert.
- failedDer Auftrag konnte nicht erfolgreich abgeschlossen werden; es liegt keine bestätigte Reservierung vor.
Fehlercodes
Die API verwendet Standard-HTTP-Statuscodes. Fehlerantworten enthalten immer ok: false und ein error-Feld (ein maschinenlesbarer Code oder — bei Validierungsfehlern — ein Objekt mit fieldErrors); je nach Fall zusätzlich outcome oder retryable.
| Code | Bezeichnung | Bedeutung |
|---|---|---|
| 400 | Bad Request | Ungültige Anfrage, fehlende Zustimmung oder fehlender/ungültiger Idempotency-Key. |
| 401 | Unauthorized | API-Key fehlt, ist ungültig oder abgelaufen. |
| 403 | Forbidden | API-Key passt nicht zur Umgebung (LIVE/TEST) oder das Konto ist gesperrt. |
| 404 | Not Found | Ressource nicht gefunden (z. B. unbekannte Reservierungs-ID). |
| 409 | Conflict | Kennzeichen nicht verfügbar oder Idempotency-Key mit verändertem Payload wiederverwendet. |
| 410 | Gone | Die angeforderte Ressource ist abgelaufen und steht nicht mehr zur Verfügung. |
| 422 | Unprocessable Content | Kennzeichenformat, Bezirk oder erforderliche Antragstellerdaten sind ungültig. |
| 429 | Too Many Requests | Rate-Limit überschritten. Bitte Anfragen drosseln. |
| 500 | Internal Server Error | Unerwarteter Serverfehler. Bitte erneut versuchen. |
| 503 | Service Unavailable | Verbindliche Reservierung oder eine notwendige Systemkonfiguration ist vorübergehend nicht verfügbar. |
Bereit, live zu gehen?
Fordere deinen API-Zugang an — Sandbox-Keys und alle Details per E-Mail.