Errors
Every non-2xx response follows the same envelope (derived from RFC 7807):
{
"error": {
"code": "INVALID_DATE_RANGE",
"message": "from_date must be earlier than to_date",
"details": { "from_date": "2026-04-10", "to_date": "2026-04-01" }
}
}
codeis stable and machine-readable. Changing a code is a breaking change.messageis human-readable. It may be improved at any time; do not pattern-match on it.detailsis optional and context-specific.
Error codes
| HTTP | Code | Meaning |
|---|---|---|
| 400 | INVALID_DATE | Date string not YYYY-MM-DD. |
| 400 | INVALID_DATE_RANGE | from_date > to_date, or demographics range > 90 days. |
| 400 | INVALID_CURSOR | Pagination cursor cannot be decoded. |
| 400 | REQUIRED_PARAMETER | A required query param is missing or malformed. |
| 401 | UNAUTHORIZED | API key missing or invalid. |
| 403 | FORBIDDEN | Key is valid but lacks the permission for this endpoint. |
| 404 | NOT_FOUND | Resource doesn't exist, or it's outside your key's company scope. |
| 429 | RATE_LIMITED | See Rate Limiting. |
| 500 | INTERNAL_ERROR | Unexpected server error (uncaught exception). Retrying may succeed if transient. |
| 503 | UPSTREAM_UNAVAILABLE | A dependency (metrics, demographics pipeline) is down. Safe to retry. |
404 vs 403
We return 404 NOT_FOUND for any resource outside your company scope — even if the underlying record exists. 403 FORBIDDEN is only for permission mismatches (e.g. hitting /demographics with a key that only has partner:assets:read). This prevents cross-tenant existence probing.
Retrying
| Code | Retry? |
|---|---|
INVALID_*, REQUIRED_PARAMETER, NOT_FOUND, FORBIDDEN, UNAUTHORIZED | No. Fix the request. |
RATE_LIMITED | Yes, after retry_after_seconds. |
INTERNAL_ERROR, UPSTREAM_UNAVAILABLE | Yes, with exponential backoff — start at 1s, double to 60s, cap at ~5 attempts. |