Grundlagen
Adresse und Version
Alle Endpunkte liegen unter https://developer.bahnhoftafel.de/v1. Die Version steht im Pfad. Innerhalb von v1 kommen höchstens neue Felder und Endpunkte dazu; nichts wird umbenannt oder entfernt. Lies also nur die Felder, die du brauchst, und rechne damit, dass es mehr werden.
Anmeldung
Jede Anfrage schickt den Schlüssel deiner App im Header Authorization:
Authorization: Bearer bt_abcdefghij_…Der Teil zwischen den Unterstrichen ist die öffentliche ID deiner App, der Rest das Geheimnis. Wir speichern nur einen Prüfwert davon; wenn du den Schlüssel verlierst, erzeugst du im Portal einen neuen. Der alte gilt dann sofort nicht mehr.
Der Schlüssel gehört auf deinen Server. Nicht in eine Webseite, nicht in eine App, die andere herunterladen, nicht in ein öffentliches Repo. Die API schickt deshalb auch keine CORS-Header: Aufrufe direkt aus dem Browser gehen nicht.
Wer mehr Schutz will, signiert seine Anfragen zusätzlich, siehe Signierte Anfragen. Nur GET /v1/status geht ohne Schlüssel.
Formate
| Antworten | JSON in UTF-8, Bilder als PNG, Live-Ströme als Server-Sent Events. |
| Zeiten | ISO 8601 mit Zeitzone, z. B. 2026-10-15T18:18:00+02:00. Verspätungen in ganzen Minuten. |
| Bahnhöfe | EVA-Nummer als Text, z. B. "8011160" für Berlin Hbf. |
| Fahrten | trip_id ist undurchsichtig. Nimm sie, wie sie kommt; ihr Aufbau kann sich ändern. |
| Felder | Englisch in snake_case, damit sie zu üblichen Werkzeugen passen. Die Doku ist deutsch. |
In der Referenz heißt feld?, dass ein Feld fehlen kann, und ?typ, dass es null sein kann.
Fehler
Bei einem Fehler kommt ein passender HTTP-Status und immer derselbe Aufbau: ein fester code zum Auswerten, eine message für Menschen und je nach Fehler weitere Felder.
{
"code": "invalid_request",
"message": "Ungültige Anfrage",
"errors": {
"duration": "höchstens 180",
"foo": "unbekannter Parameter"
}
}Unbekannte Parameter sind ein Fehler. So fallen Tippfehler sofort auf, statt dass ein Filter still nicht greift.
| code | Status | Bedeutung |
|---|---|---|
invalid_request | 400 | Parameter fehlen, haben den falschen Typ oder liegen außerhalb des Bereichs. errors nennt die Felder. |
unauthorized | 401 | Kein oder ein ungültiger API-Schlüssel. |
invalid_signature | 401 | Signatur fehlt, ist falsch, zu alt oder wurde schon einmal benutzt. |
forbidden | 403 | Der Schlüssel darf diesen Endpunkt nicht nutzen (Bereich fehlt oder App gesperrt). |
not_found | 404 | Diesen Endpunkt gibt es nicht. |
station_not_found | 404 | Bahnhof unbekannt. |
trip_not_found | 404 | Fahrt unbekannt oder nicht mehr verfügbar. |
formation_not_found | 404 | Für diesen Zug liegt keine Wagenreihung vor. |
method_not_allowed | 405 | Methode für diesen Pfad nicht erlaubt. |
payload_too_large | 413 | Anfrage zu groß. |
rate_limited | 429 | Zu viele Anfragen. retry_after sagt, wie lange zu warten ist. |
temporarily_blocked | 429 | Nach sehr vielen fehlerhaften Anfragen ist der Schlüssel kurz gesperrt. |
upstream_unavailable | 502 | Die Datenquelle antwortet gerade nicht. Später erneut versuchen. |
internal_error | 500 | Unerwarteter Fehler auf unserer Seite. |
Wie aktuell die Daten sind
Wir fragen die Quellen je Bahnhof selbst in kurzen Abständen, und alle Apps teilen sich diese Abrufe. Öfter fragen bringt also nichts Neueres. Wer ständig aktuelle Abfahrten braucht, nimmt den Live-Strom.