Errors
Every error response wraps a structured payload in the same envelope as success. Status code is in the HTTP response; machine-readable code is in error.code; human-readable message is in error.message. The meta envelope is unchanged from success responses — same request_id you would have gotten on a 200.
Envelope
{
"error": {
"code": "TIER_RESTRICTED",
"message": "Endpoint /api/v1/area requires a Starter plan or above.",
"status": 403
},
"meta": {
"request_id": "req_8aZpmK4qX0nT",
"timestamp": "2026-05-20T14:22:08Z",
"response_time_ms": 4
}
}Always log request_id with your error handler — it lets us trace the call in our logs if you open a support ticket.
Status codes
| Status | Code | When |
|---|---|---|
| 400 | INVALID_BODY | Malformed JSON, missing required field, or schema violation in the request body. |
| 401 | UNAUTHENTICATED | Missing or invalid X-API-Key. (Account endpoints: missing or invalid Supabase JWT.) |
| 402 | QUOTA_EXHAUSTED | Free tier's monthly call cap (10) reached. Upgrade or wait for the calendar-month reset. |
| 403 | TIER_RESTRICTED | Endpoint requires a higher tier (e.g., /area on Free, /portfolio on Free, /facility/{id}/report on Starter). |
| 404 | NOT_FOUND | Registry ID does not exist in our index, address could not be geocoded, or user has no API key row yet. |
| 422 | VALIDATION_ERROR | Parameter out of range (e.g., radius_miles > 25) or batch size over the tier limit. |
| 429 | RATE_LIMIT_EXCEEDED | Monthly call limit hit, or demo IP burst limit (5/hour), or regenerate throttle (1/minute). |
| 429 | REGENERATE_THROTTLED | POST /account/key/regenerate called within 60s of the prior regenerate. Inspect the `Retry-After` header for the wait. |
| 500 | INTERNAL_ERROR | Server-side failure. Retry with exponential backoff. Cache misses are never surfaced as 500. |
Retry guidance
429withRetry-After: honor the header. See Rate limits.500: server-side failure. Retry once after ~1s; if it fails again, file an issue with therequest_id. Cache misses are never surfaced as 500.4xxother than 429: do not retry blindly — fix the input. Themessagefield is human-readable for triage.