Errors
Every AI92 API error — across all eight services — uses one envelope. Parse it once and you can handle errors everywhere.
The error envelope
json
{
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance (2) is below the cost of this operation (3). Top up at https://api.ai92.ai/account/credits.",
"status": 402,
"documentation_url": "https://ai92.ai/docs/errors"
}
}| Field | Meaning |
|---|---|
code | Stable, machine-readable identifier. Branch on this, never on the message. |
message | Human-readable explanation, safe to log and (carefully) surface. |
status | The HTTP status code, mirrored in the body for convenience. |
documentation_url | Deep link to the docs for this error (not present on every error). |
The HTTP status line always matches error.status.
HTTP status families
| Status | Meaning |
|---|---|
400 | Malformed request |
401 | Authentication problem (missing / invalid / expired / revoked credential) |
402 | Payment required — out of credits or over a spending cap |
403 | Authenticated but not allowed (IP, scope, suspended, read-only) |
404 | Not found (or not owned by you — existence is never leaked across accounts) |
409 | Conflict (idempotency key reused with a different body) |
422 | Semantically invalid (bad enum, bad window, brand not onboarded) |
429 | Rate limited |
503 | An upstream service was unavailable (retryable, not charged) |
Error code reference
Authentication & authorization
| Code | Status | Cause | Solution |
|---|---|---|---|
AUTHENTICATION_REQUIRED | 401 | No Authorization header, or not in Bearer <token> form. | Send Authorization: Bearer ai92_live_.... |
INVALID_API_KEY | 401 | Key not found, revoked, unrecognized token format, or a revoked OAuth token. | Check the key value; rotate if lost; re-issue OAuth tokens if revoked. |
EXPIRED_API_KEY | 401 | The key's expires_at has passed. | Create a new key and update your deployment. |
FORBIDDEN_SCOPE | 403 | An OAuth token is missing a scope the endpoint requires. | Re-authorize the user requesting the needed scope(s). |
IP_NOT_ALLOWED | 403 | Request IP is outside the key's ip_whitelist. | Call from an allowed CIDR, or update the allowlist via PATCH /v1/account/keys/{id}. |
TENANT_INACTIVE | 403 | The account is suspended. | Contact support / resolve billing. |
ACCOUNT_READ_ONLY | 403 | Account placed in read-only mode after long inactivity. | Reactivate the account (a paid action re-activates it). |
Credits & billing
| Code | Status | Cause | Solution |
|---|---|---|---|
INSUFFICIENT_CREDITS | 402 | Balance is below the operation's cost (checked before the call runs). | Top up via POST /v1/billing/checkout; check balance with GET /v1/account. |
KEY_SPENDING_LIMIT | 402 | The key hit its per-key daily_credit_limit or monthly_credit_limit. | Raise the cap via PATCH /v1/account/keys/{id}, wait for the period to reset, or use another key. |
Request validation
| Code | Status | Cause | Solution |
|---|---|---|---|
INVALID_REQUEST | 400 | The request is malformed (bad JSON, missing required field, wrong type). | Fix the payload to match the endpoint schema. |
UNPROCESSABLE_ENTITY | 422 | Well-formed but semantically invalid input. | Correct the value (see the message for which field). |
INVALID_REVEAL_MODE | 422 | mode on /v1/contacts/reveal was not email or email_phone. | Use a valid mode. |
INVALID_PLANNING_WINDOW | 422 | Creative campaign window is reversed or longer than 31 days. | Send planning_window_end ≥ start, window ≤ 31 days. |
BRAND_NOT_ONBOARDED | 422 | A brand-scoped endpoint was called with a brand_id that hasn't completed onboarding. | Register/onboard the brand via POST /v1/brands first. |
IDEMPOTENCY_KEY_REUSE | 409 | Same X-AI92-Idempotency-Key used with a different request body. | Use a new key for a new request. See Idempotency. |
Upstream / backend
| Code | Status | Cause | Solution |
|---|---|---|---|
BACKEND_UNAVAILABLE | 503 | The upstream service powering this endpoint returned a 5xx or was unreachable. You are not charged. | Retry with exponential backoff; it's transient. |
BACKEND_ERROR | 4xx / 502 | The upstream rejected the request (4xx is passed through; an unclassified failure becomes 502). | For 4xx, fix the input; for 502, retry, then contact support if it persists. |
Migrating from older clients: earlier drafts referenced
FEATURE_UNAVAILABLEandBACKEND_DISPATCH_FAILED. These map to the codes above — a disabled service surfaces as a503-class backend failure, and dispatch failures surface asBACKEND_UNAVAILABLE(503, not charged). Branch on the HTTP status family for forward compatibility.
Not found
| Code | Status | Cause | Solution |
|---|---|---|---|
NOT_FOUND | 404 | The resource doesn't exist — or exists but belongs to another account. | Verify the id; ids are scoped to your account. |
Handling errors well
- Switch on
error.code, not the message string (messages can change). - Retry
503and429with backoff; do not auto-retry4xx(fix the request instead). - Pair write retries with an idempotency key so a retried
POSTcan't double-charge. - Log
X-AI92-Trace-Idfrom the response — quote it to support to pinpoint the exact request.
python
r = httpx.post(url, headers=headers, json=payload)
if r.is_error:
err = r.json()["error"]
code, status = err["code"], err["status"]
if status in (429, 503):
... # retry with backoff
elif code == "INSUFFICIENT_CREDITS":
... # top up, then replay with the same idempotency key
else:
raise RuntimeError(f"{code}: {err['message']} (trace {r.headers.get('X-AI92-Trace-Id')})")