Errors
Every failure comes back in the same shape, with an HTTP status and two fields:
{
"code": "unknown_metric",
"error": "unknown metric: foo",
"request_id": "ch-node01-01997f2a8b3c7d5e8f01abcdef123456"
}
Branch on code. It is part of the contract and will not change. error is a human-readable detail meant for logs and error messages, and its wording can be reworded at any time. A client that matches on error text will break; one that matches on code will not.
Request id
Every response carries an X-Request-ID header, and error bodies repeat the same value as request_id:
This is the one value worth capturing. The leading segment names the machine that served the call, so the id on its own is enough to find the log line. Without it, a 500 can only be matched by guessing at a time window, which often means it cannot be found at all.
It is present on successful responses too, so logging it beside your own records helps just as much when the numbers look wrong as when a call outright fails.
You may send your own X-Request-ID. It is recorded alongside ours so a trace can be followed across both systems, but the value echoed back is always ours.
A 404 for a path that does not exist is served before any of this runs, so those responses carry no id. Anything from a documented endpoint does.
Bad request
400 means the request itself is wrong. Nothing was run, and repeating it unchanged will fail identically.
code | Meaning |
|---|---|
invalid_request | The JSON body is malformed |
metrics_required | metrics was empty or missing |
unknown_metric | No such metric |
metric_not_available | Real metric, but this dimension does not compute it |
too_many_dimensions | More than one dimension |
unknown_dimension | No such dimension |
invalid_date_range | Missing, malformed, or outside the allowed span |
invalid_interval | The time bucket does not suit the range |
invalid_filter | Bad filter field, operator, or value |
event_filter_required | event:props:<key> without an event filter to scope it |
invalid_compare | compare is neither previous_period nor a valid pair |
compare_length_mismatch | The compare range is a different length from date_range |
site_id_required | An all-sites key omitted site_id |
limit_offset_misuse | limit or offset sent on an aggregate or time series |
unknown_metric and metric_not_available look alike and are not. The first means the name does not exist anywhere. The second means the name is fine but the dimension you paired it with cannot produce it, which is the more common mistake. See the dimension table in Query reference.
Authentication
| Status | code | Meaning |
|---|---|---|
401 | unauthorized | The token is missing, malformed, invalid, expired, or revoked |
All five causes answer the same way on purpose. A response cannot tell an attacker whether a token was ever real. If your own key stops working, check it under Settings → API: an expired key still appears in the table, marked Expired, while a revoked one is gone.
Refused
| Status | code | Meaning |
|---|---|---|
403 | insufficient_scope | The key lacks the permission the endpoint needs |
403 | key_not_scoped | A single-site key asked for a different site |
403 | tracking_inactive | The site is no longer collecting data |
insufficient_scope names the missing permission in the error field. Permissions are fixed when a key is created, so the fix is a new key rather than a changed one. See Permissions.
key_not_scoped is not a permissions problem you can fix by request. Access is fixed when a key is created, so reach a second site with a second key, or an all-sites key.
Not found
| Status | code | Meaning |
|---|---|---|
404 | unknown_site | The site_id does not exist, or you have no access to it |
404 | unknown_funnel | No such funnel on the resolved site |
The unknown_site overlap is deliberate. A site you cannot read is indistinguishable from one that was never created, same status and same code, so a key cannot be used to sweep for valid ids and learn which accounts exist.
The practical consequence: a 404 does not mean the site is gone. Call GET /sites to see what the key actually reaches. That is the authoritative list, and it is usually the answer.
Too many requests
| Status | code | Meaning |
|---|---|---|
429 | rate_limited | An hourly budget ran out |
The error field says which budget, per key or per account, and Retry-After says how many seconds to wait. See Rate limits.
Server error
| Status | code | Meaning |
|---|---|---|
500 | internal | Something failed on our side |
Nothing about your request needs changing. Retry with backoff, and if it persists, check Service status. When you report one, quote the request_id from the response.
What to retry
| Status | Retry? |
|---|---|
400 | No. Fix the request first |
401 | No. Get a working key |
403 | No. Use a key that reaches the site |
404 | No, unless you are confirming against GET /sites |
429 | Yes, after Retry-After |
500 | Yes, with backoff |
Only 429 and 500 are worth an automatic retry. Retrying the rest costs you rate-limit budget and returns the same answer.
Next steps
- Query reference: the request shapes most
400s come from - Authentication: what a working key looks like
Ready to take control of your web analytics? Try Statable free for 30 days. No credit card required, full feature access, built for GDPR. Start your free trial or view a live demo.