Realtime/SL

Access, CORS and errors

No key, no rate limit, and CORS open to any origin — so a browser can call it directly. What that means for calling it from a page, and the error shapes you can rely on.

There is no API key, no token, no sign-up and no rate limit. Every endpoint on this site is a plain GET (one POST) that anyone can call:

the whole authentication story
curl "$API_BASE/api/vehicles?limit=3"

That is a deliberate choice for a service that reads from an already-public transport feed and holds nothing per-user. It also has a concrete benefit: because there is no credential header to send, a browser can call the API directly, which is what makes the Try it consoles on this site real rather than decorative.

CORS

The API allows any origin:

response headers
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Expose-Headers: Warning
Access-Control-Max-Age: 86400

Two details worth knowing if you are calling this from a page:

  • Most requests never preflight. A GET with no custom headers is a simple request, so the browser sends it straight out. You only trigger an OPTIONS round trip if you add a header of your own — which you have no reason to do here.
  • Warning is exposed on purpose. Browsers hide most response headers from JavaScript unless a server opts them in. The punctuality endpoints signal a silent fallback through Warning (see below), so it is explicitly exposed — otherwise res.headers.get("Warning") would always return null in a browser and you would never learn the answer had backed off.

The Warning header

Predict and baseline accept a routeId. If the static archive does not know that route, the request still succeeds — but the answer silently describes a broader slice of the network than you asked about. Rather than fail, the API tells you:

response header
Warning: 199 - "routeId 9011001099999999 is not in the static archive"

The response body says the same thing in a structured way — basis will read mode or global rather than route, and data.query.route will be absent. Reading basis is the more robust check; the header is there so a generic HTTP client notices too.

Errors

Every error body carries both error and message. error is a stable machine-readable token; message is a human sentence that names the offending value and, where it can, tells you what would have worked. Neither is ever missing, so a client can read message unconditionally.

StatuserrorWhen
400 request_failed A parameter was malformed — an enum outside its list, a non-numeric bound, an unparseable timestamp, a page size out of range.
404 request_failed The route exists but the thing you named does not — no such vehicle, trip, alert or route id.
404 not_found No route matches the path at all. The distinct error token is how you tell a typo'd URL from a missing record.
422 request_failed The request was well-formed and the resource exists, but the operation is impossible — asking to forecast a trip that reports no delay anywhere.
500 internal_error A bug. The message is deliberately generic; the detail is in the server log.
real error bodies
# 400 — an enum outside its list. The message lists what was allowed.
{
  "error": "request_failed",
  "message": "status must be one of: INCOMING_AT, STOPPED_AT, IN_TRANSIT_TO. Got \"NOPE\"."
}

# 404 — the route exists, the record does not.
{
  "error": "request_failed",
  "message": "No vehicle with id \"does-not-exist\"."
}

# 404 — no such route. Note the different `error` token.
{
  "error": "not_found",
  "message": "No route matches GET /api/nope."
}