Skip to content

Errors

Every error is JSON with one envelope — including unknown paths under /v1, which never fall back to HTML. Branch on error.code (stable, machine-readable), show error.message to humans:

{
  "error": {
    "code": "validation_error",
    "message": "Unknown filter 'countyy'",
    "detail": { "filterable_columns": ["category", "county", "..."], "operators": ["..."] },
    "request_id": "d41f6f7c9c3a4e59a1b2c3d4e5f60718"
  }
}

detail appears only when there is structured context to add: unknown filters carry filterable_columns + operators, bad sort/fields carry columns, malformed parameters (e.g. limit out of range) carry a list of {param, message} pairs, and 429s carry retry_after_seconds.

request_id echoes the X-Request-ID response header (send your own well-formed X-Request-ID and it is echoed back; otherwise one is generated). Include it when reporting a problem — it is how we find your request in the logs.

Codes

HTTP code Meaning What to do
401 unauthorized Missing, malformed, unknown, or revoked API key Check the Authorization: Bearer rok_… header
404 not_found No such dataset/route, or the dataset has not been ingested yet Check the id against /v1/datasets
405 method_not_allowed The path exists but not for that verb — the API is read-only, so everything is GET/HEAD Use GET; the Allow header lists what the path accepts
422 validation_error The request is malformed: unknown filter column or operator, bad sort/fields, uncoercible value, cursor mixed with offset/sort, limit out of 1–1000 Fix the query — the message names the offending parameter
429 rate_limited Per-minute rate limit or monthly quota exceeded Wait Retry-After seconds and retry; see limits
502 upstream_error A dependency (e.g. the CKAN portal) failed Retry later
500 internal_error Unexpected server error Retry later; report if persistent

Design notes worth relying on

  • 422 instead of silent tolerance. A typo like ?countyy=CLUJ is an error, not an unfiltered 200 — you will never silently receive the whole dataset because of a misspelled filter.
  • One envelope, including the errors we don't raise ourselves. A wrong method or an unknown path is produced by the framework, not by our handlers, and still arrives in the shape above — there is no second error format to special-case.
  • Errors are cheap but not free — they pass through rate limiting like any request (rejected requests never consume monthly quota).
  • 429 bodies mirror the headererror.detail.retry_after_seconds carries the same value as Retry-After, for HTTP clients that only surface the JSON body.
  • 304 Not Modified is not an error — it's the success path of conditional requests; it has no body at all, not even the envelope.

Handling pattern

response = client.get("/v1/transport/vehicle_registry", params=params)
if response.status_code >= 400:
    error = response.json()["error"]
    match error["code"]:
        case "rate_limited":
            retry_after = int(response.headers.get("Retry-After", "5"))
            ...
        case "validation_error":
            raise ValueError(error["message"])  # a bug in our query — don't retry
        case _:
            response.raise_for_status()
const res = await fetch(url, { headers });
if (!res.ok) {
  const { error } = await res.json();
  switch (error.code) {
    case "rate_limited": {
      const wait = Number(res.headers.get("Retry-After") ?? 5);
      // back off and retry
      break;
    }
    case "validation_error":
      throw new Error(error.message); // a bug in our query — don't retry
    default:
      throw new Error(`${res.status}: ${error.message}`);
  }
}