Error catalogue
Every error response has the same shape:
json
{
"error": {
"code": "FORBIDDEN_CAPABILITY",
"message": "Metric 'paygap.gender_median_gap' requires capability 'people.view_paygap'",
"detail": { "capability": "people.view_paygap" }
}
}detail is optional. Codes are stable — once published, never repurposed.
| Code | Status | When it fires |
|---|---|---|
UNAUTHENTICATED | 401 | No session cookie and no Authorization header, OR the API key is unknown / revoked / malformed |
METRIC_NOT_FOUND | 404 | The metric id does not exist in the registry. Check spelling and the reference |
FORBIDDEN_CAPABILITY | 403 | Authenticated, but the caller lacks the required capability. detail.capability names the capability — request it on the API key, or use a session with the appropriate role |
PERIOD_KIND_MISMATCH | 400 | Snapshot period (today) sent to a range metric, or a range period sent to a snapshot metric. Check periodKind on the metric metadata |
INVALID_PERIOD | 400 | ?period= couldn't be parsed. Misspelled key, malformed range, or empty string |
INVALID_AS_OF | 400 | ?asOf= is not a valid ISO date, or it's in the future |
INVALID_REQUEST | 400 | Generic validation failure — malformed JSON body in a batch, missing required fields, oversize batch (>50 queries), duplicate keys |
INTERNAL_ERROR | 500 | The resolver threw an unexpected error. File a bug — these should never happen in production |
Recovery
| Code | What to do |
|---|---|
UNAUTHENTICATED | Re-mint the key or sign in again |
METRIC_NOT_FOUND | Look it up in /api/v1/metrics |
FORBIDDEN_CAPABILITY | Tick the capability box when minting a new key, or sign in as an admin/owner |
PERIOD_KIND_MISMATCH | Read the metric's periodKind field and use the right kind of period |
INVALID_PERIOD / INVALID_AS_OF | The error message includes the offending input — fix the format |
INVALID_REQUEST | Read the message — it names the specific field at fault |
INTERNAL_ERROR | Retry once with backoff. If it persists, file a bug |
Batch
In POST /api/v1/metrics/batch, errors come in two layers:
- Top-level errors — malformed body, oversize batch, future
asOf, etc. Returned as a normal envelope withINVALID_REQUEST/INVALID_AS_OFand an HTTP 400. - Per-query errors — a query references an unknown metric or hits a capability that the caller doesn't have. The whole batch still returns
200 OK, with that query's slot containing{ ok: false, error: { code, message } }. Sibling queries are unaffected.
This is intentional: a dashboard with one denied panel should still render the other nine.