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:
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:
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
GETwith no custom headers is a simple request, so the browser sends it straight out. You only trigger anOPTIONSround trip if you add a header of your own — which you have no reason to do here. -
Warningis exposed on purpose. Browsers hide most response headers from JavaScript unless a server opts them in. The punctuality endpoints signal a silent fallback throughWarning(see below), so it is explicitly exposed — otherwiseres.headers.get("Warning")would always returnnullin 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:
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.
| Status | error | When |
|---|---|---|
| 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. |
# 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."
}