Skip to content

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:

X-Request-ID: ch-node01-01997f2a8b3c7d5e8f01abcdef123456

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.

codeMeaning
invalid_requestThe JSON body is malformed
metrics_requiredmetrics was empty or missing
unknown_metricNo such metric
metric_not_availableReal metric, but this dimension does not compute it
too_many_dimensionsMore than one dimension
unknown_dimensionNo such dimension
invalid_date_rangeMissing, malformed, or outside the allowed span
invalid_intervalThe time bucket does not suit the range
invalid_filterBad filter field, operator, or value
event_filter_requiredevent:props:<key> without an event filter to scope it
invalid_comparecompare is neither previous_period nor a valid pair
compare_length_mismatchThe compare range is a different length from date_range
site_id_requiredAn all-sites key omitted site_id
limit_offset_misuselimit 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

StatuscodeMeaning
401unauthorizedThe 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

StatuscodeMeaning
403insufficient_scopeThe key lacks the permission the endpoint needs
403key_not_scopedA single-site key asked for a different site
403tracking_inactiveThe 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

StatuscodeMeaning
404unknown_siteThe site_id does not exist, or you have no access to it
404unknown_funnelNo 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

StatuscodeMeaning
429rate_limitedAn 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

StatuscodeMeaning
500internalSomething 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

StatusRetry?
400No. Fix the request first
401No. Get a working key
403No. Use a key that reaches the site
404No, unless you are confirming against GET /sites
429Yes, after Retry-After
500Yes, 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


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.