Skip to main content

Reference

Errors

The single error shape every endpoint returns, and what each code means.

Not available yet. The routes are not built. The error codes below are the planned set; the authentication codes are the ones the auth layer already emits.

Every error is one shape:

{
  "error": {
    "code": "insufficient_scope",
    "message": "This key does not have access to that site."
  }
}

code is a symbolic string, not a repeated HTTP status. Branch on code — it is stable, and it distinguishes cases that share a status.

message is for humans. It never contains anything from your request — no field name, no value, no site id. That is deliberate: a public endpoint that echoes input back is both a way to inject content and a way to probe. Do not parse it.

Every code

400 — the request is malformed

codeMeaningFix
invalid_requestA parameter is the wrong shape — a bad timezone, an unknown unit, a limit over 1000Check it against Endpoint reference
invalid_rangefrom without to, or to without fromSend both, or use period instead
conflicting_rangeperiod sent together with from/toUse one or the other
unknown_dimensionA breakdown dimension that does not existThe valid list is in Endpoint reference

Retrying without changing the request will not help.

401 — we do not know who you are

codeMeaning
unauthenticatedNo Authorization header
invalid_credentialThe key is wrong, revoked or expired

invalid_credential covers all three on purpose. Distinguishing "revoked" from "never existed" would tell someone probing that a key had once been real.

If your key worked yesterday: check whether it was revoked, whether it hit an expiry date, and whether you are on the right environment — a ts_test_ key against production is invalid_credential.

403 — we know who you are, and no

codeMeaningFix
insufficient_scopeThe key is valid but is not scoped to this siteCheck the key's site scope. The API itself is on every plan, so this is a scope problem rather than a plan problem.
site_not_accessibleThe site is not readable by this keySee below
insufficient_roleThe action needs a higher roleNot applicable to the read API today

404 — no such route

The path does not exist. Note that a site you cannot access returns 403, not 404 — see below.

429 — too many requests

codeMeaning
rate_limitedYou have exceeded your rate limit

The response carries Retry-After with the seconds to wait. Back off exponentially; do not retry in a tight loop. See Rate limits.

5xx — our problem

codeStatusMeaning
temporarily_unavailable503A transient failure. Retry with backoff.
internal_error500Something broke. Retrying may work; if it persists, tell us.

A 500 that repeats for the same request is worth reporting to support@truestat.io with the endpoint, the parameters and roughly when. Do not send your key.

403 vs 404

A site your key cannot reach returns 403, not 404 — and it returns the same 403 whether the site belongs to someone else or does not exist at all.

That is on purpose. If a nonexistent site gave 404 and someone else's site gave 403, the difference would tell a caller which site ids are real, which turns the endpoint into a way to enumerate other customers' sites.

So when you get site_not_accessible, the possibilities are: the id is wrong, the site is in another organisation, your key is scoped to specific sites and this is not one, or the site was deleted. Call /sites to see what the key can actually read.

Errors from the collector

/api/events — where your tracking script sends page views — is a different surface with a different error set. If you are debugging a tag rather than an API client, see Verifying it works, which lists every status the collector returns and what to do about each.

The one worth knowing here: the collector's success status is 202, not 200.

Handling errors well

const res = await fetch(url, { headers });
const body = await res.json();

if (!res.ok) {
  switch (body.error.code) {
    case "rate_limited":
      await sleep(Number(res.headers.get("Retry-After") ?? 60) * 1000);
      return retry();

    case "invalid_credential":
    case "insufficient_scope":
      // Configuration, not transient. Do not retry.
      throw new Error(body.error.code);

    case "temporarily_unavailable":
    case "internal_error":
      return retryWithBackoff();

    default:
      // 400-class. The request is wrong; retrying it unchanged will not help.
      throw new Error(`${body.error.code}: ${body.error.message}`);
  }
}

The distinction that matters: 4xx other than 429 means fix the request. 429 and 5xx mean wait and try again. Retrying a 400 in a loop only uses your rate limit.

Was this page helpful?

Last updated August 28, 2026