Developers
The public ÖffiGo API
Everything the measurement network produces live is also available as an open JSON API — the same one the app and the status page use. No key, no sign-up; please use it fairly (limits below).
https://api.oeffigo.app| Endpoint | Description |
|---|---|
GET /v1/health | Quick health check of the API |
GET /v1/status | Status of all system components (basis of the status page) |
GET /v1/predictions | Live delay predictions from the measurement network |
GET /v1/announcements | Active service announcements |
GET /v1/history | Aggregated reliability history (delays by line, stop, hour) |
Ground rules
- CORS is open (
Access-Control-Allow-Origin: *) — calling directly from the browser is fine. - Rate limit: 120 requests per minute per IP. Above that you get
429withRetry-After: 30. - Successful responses carry
Cache-Control: public, max-age=10. /v1/predictionsreturns anETagper measurement cycle (~35 s). Poll withIf-None-Match→ an empty304as long as nothing changed. Recommended for battery-friendly clients.- Breaking changes only ship as a new path (
/v2). New fields can appear on/v1at any time — please ignore unknown fields.
GET /v1/health
Responds 200 when the last measurement pass is fresh, otherwise 503.
{
"ok": true,
"vehiclesLive": 1312,
"lastCycleAt": "2026-07-03T10:15:30.000Z"
}GET /v1/status
Component status (measurement, data source, Wiener Linien, traffic, database, API) with plain-text details in German — overall is operational, degraded or down. This is exactly what the status page renders.
{
"generatedAt": "2026-07-03T10:15:35.000Z",
"overall": "operational",
"components": [
{
"id": "ingestion",
"name": "Messung (Fahrzeug-Beobachtung)",
"status": "operational",
"detail": "Letzter Messdurchlauf vor 28 s, 1.312 Fahrzeuge."
}
]
}GET /v1/predictions
Live delay predictions from the measurement network's in-memory snapshot. One lookup mode per request:
| Parameter | Meaning |
|---|---|
lat + lon | Predictions around a coordinate. Optional radius in metres (default 2000, max 25000) and limit (default 50, max 200). |
line | Exact line label, case-insensitive. Combinable with lat/lon. |
jid | Direct journey ID → exactly one prediction or 404. |
jids | Comma-separated journey IDs (max 40) for a whole departure board. |
curl 'https://api.oeffigo.app/v1/predictions?lat=47.0707&lon=15.4395&radius=3000&limit=20'{
"generatedAt": "2026-07-03T10:15:35.000Z",
"count": 1,
"predictions": [
{
"jid": "1|12345|0|80|03072026",
"line": "U1",
"product": "U-Bahn",
"direction": "Leopoldau",
"lat": 48.2084,
"lon": 16.3725,
"delayMin": 1.2,
"feedDelayMin": 1,
"confidence": "high",
"progress": 0.642,
"updatedAt": "2026-07-03T10:15:30.000Z",
"distanceM": 116
}
]
}delayMin is our own prediction in minutes, feedDelayMin the official display (or null if there is none), confidence the model's certainty. Predictions expire ~3 minutes after the last refresh.
GET /v1/announcements
Active service announcements (e.g. maintenance windows), max 20, newest first.
{
"generatedAt": "2026-07-03T10:15:35.000Z",
"announcements": [
{
"id": 42,
"severity": "warning",
"title": "S-Bahn Einschränkung",
"body": "Auf der Stammstrecke kommt es derzeit zu Verzögerungen.",
"url": null,
"startsAt": "2026-07-03T09:00:00.000Z",
"endsAt": null
}
]
}GET /v1/history
Aggregated reliability history for Austria's public transport — the data behind the reliability atlas. It reads pre-aggregated daily tables (5-minute server cache + CDN), so a request never touches raw data. All spans are day-based:
| Parameter | Meaning |
|---|---|
from + to | Date range as YYYY-MM-DD. Defaults to the last 90 days; the span may not exceed 366 days. |
line | Exact line label (e.g. S1), matched against the stored label. Max 80 chars. |
product | Filter by mode (e.g. Bus, Straßenbahn, S-Bahn). Max 40 chars. |
curl 'https://api.oeffigo.app/v1/history?from=2026-06-01&to=2026-07-12&product=S-Bahn'{
"generatedAt": "2026-07-12T08:15:00.000Z",
"period": { "from": "2026-06-01", "to": "2026-07-12" },
"dataSince": "2026-06-20",
"coverage": {
"journeys": 48213,
"samples": 912847,
"days": 23,
"cancellationCoverage": "explicit_events_only"
},
"totals": {
"onTimeShare": 0.8412,
"avgDelayMin": 1.83,
"late3Share": 0.1104,
"late10Share": 0.0212,
"cancellationRate": 0.0038
},
"daily": [
{ "day": "2026-06-20", "journeys": 2104, "samples": 39218, "avg_delay_min": 1.71, "on_time_pct": 85.2, "late_10_pct": 1.9 }
],
"lines": [
{ "line": "S1", "product": "S-Bahn", "journeys": 1842, "samples": 41230, "avg_delay_min": 2.44, "on_time_pct": 81.0, "late_3_pct": 13.2, "late_10_pct": 3.1, "p90_delay_min": 6.0 }
],
"stops": [
{ "stop_id": "900068", "stop_name": "Graz Hauptbahnhof", "journeys": 5120, "samples": 88210, "lines": 22, "avg_delay_min": 2.01, "on_time_pct": 83.4, "late_10_pct": 2.6, "p90_delay_min": 5.0 }
],
"heatmap": [
{ "dow": 1, "hour": 7, "journeys": 412, "avg_delay_min": 2.9, "on_time_pct": 78.1 }
],
"products": [
{ "product": "S-Bahn", "journeys": 21044, "avg_delay_min": 1.83, "on_time_pct": 84.1 }
],
"serviceEvents": [
{ "event_type": "cancelled", "day": "2026-07-02", "events": 7 }
]
}Every block carries its own sample size (journeys/samples) — a long GPS trace never counts more than a short journey, and cancellations appear only from explicit disruption/cancel events (cancellationCoverage), never inferred from a missing vehicle. Supports If-None-Match → 304.
Questions?
The API is young and grows with the measurement network. If you build something with it or a field is missing: get in touch.
