Errors
Standard HTTP status codes plus a single JSON envelope for the body. If your client knows how to read error.code, it has everything it needs to decide whether to retry, surface, or fix.
Error envelope
Every non-2xx response returns:
{
"error": {
"code": "site_not_found",
"message": "Site does not exist or is not accessible",
"details": { "website_id": 999 }
}
}
code is a stable machine-readable identifier. Match on it. message is human-friendly and may be reworded. details is optional and varies per error.
HTTP status codes
| Status | Meaning | Typical cause |
|---|---|---|
400 Bad Request | Malformed request | Missing required param, unparseable JSON body, bad period value. |
401 Unauthorized | No / expired auth | Cookie missing, JWT expired and refresh failed, logged out elsewhere. |
402 Payment Required | Plan limit exceeded | Monthly pageview cap hit on Free / Hobby tier. |
403 Forbidden | Auth ok, access denied | Site is not owned by the authenticated user and not public. |
404 Not Found | Resource does not exist | Wrong website_id, deleted goal, unknown path. |
422 Unprocessable Entity | Validation failed | Body parsed but semantically invalid (e.g. duplicate goal name). |
429 Too Many Requests | Rate limit hit | Planned. No per-plan limit is enforced today. |
500 Internal Server Error | Bug or infra issue | Investigate. Please report with request_id if you have one. |
Common error codes
code | HTTP | When |
|---|---|---|
invalid_params | 400 | Required param missing or has an unsupported value. |
invalid_body | 400 | JSON body could not be decoded. |
auth_required | 401 | No auth_token cookie present. |
token_expired | 401 | Access token expired and refresh token also failed. |
forbidden | 403 | Authenticated user has no permission on this resource. |
site_not_found | 404 | website_id does not match any site you own or share. |
goal_not_found | 404 | Goal id not found on this site. |
plan_limit_exceeded | 402 | Monthly pageview quota for the plan exhausted. |
validation_failed | 422 | Semantic validation failed (see details for fields). |
rate_limit_exceeded | 429 | Planned. Will ship with Rate limits. |
internal_error | 500 | Unhandled server error. |
Examples
Missing auth
Log in (see Authentication) and replay with the cookie jar.
Wrong site
The site either doesn't exist, was deleted, or belongs to another user (and isn't public).
Bad period
{
"error": {
"code": "invalid_params",
"message": "Invalid period",
"details": { "param": "period", "allowed": ["1r","1h","24h","7d","30d","12m","c","a"] }
}
}
Plan cap hit
{
"error": {
"code": "plan_limit_exceeded",
"message": "Monthly pageview limit reached on Free plan",
"details": { "limit": 10000, "used": 10000 }
}
}
Upgrade in Settings → Plan & Billing or wait for the next billing month.
Retry guidance
- 5xx: exponential backoff. Start at 1 s, double up to 60 s, give up after a few attempts. Usually transient.
- 429: when rate limiting ships, it will return
Retry-After(seconds). Honor it. Don't retry until that wall-clock has passed. - 401 with
token_expired: refresh and retry once. The cookie auto-refresh middleware handles this for you on subsequent calls. Explicit Bearer auth (when shipped) will require an explicit refresh round-trip. - 400 / 403 / 404 / 422: don't retry. The request is wrong. Fix the payload, the
website_id, or your permissions. - 402: don't retry until the plan is upgraded or the next billing month starts.
Reporting bugs
For a 500 you can't explain, please share:
- Endpoint and full URL (with params).
- Request body (sanitized of secrets).
- Approximate timestamp (UTC).
- Any
request_idreturned in the response headers.
Email [email protected] or open an issue on the public roadmap. Including request_id is the fastest path to a fix. It lets us pull the exact log entry server-side.
Ready to take control of your web analytics? Try Statable free for 30 days — no credit card required, full feature access, GDPR-compliant by default. Start your free trial or view a live demo.