Error Handling
HTTP status codes, error response format, and retry strategies for the Aurion API.
Error Handling
The Aurion API uses standard HTTP status codes and returns structured JSON error responses.
HTTP Status Codes
| Code | Meaning | When |
|---|---|---|
200 | OK | Request succeeded |
201 | Created | Resource created successfully |
204 | No Content | Resource deleted successfully |
400 | Bad Request | Invalid request body or parameters |
401 | Unauthorized | Missing or invalid API key |
403 | Forbidden | Valid key but insufficient scope |
404 | Not Found | Resource does not exist |
409 | Conflict | Idempotency-Key reused with a different request body, or a request with the same key is still in progress |
422 | Unprocessable Entity | Validation error on request body |
429 | Too Many Requests | Rate limit exceeded |
500 | Internal Server Error | Unexpected server error |
503 | Service Unavailable | Temporary outage or maintenance |
Error Response Format
Error responses use FastAPI's standard shape — a single detail field. There is no machine-readable error code on standard 4xx/5xx responses. Branch on the HTTP status code plus the detail string, not on a non-existent error field. (The only exception is a handful of endpoints with their own per-endpoint limiter — see Rate Limiting below.)
{
"detail": "Human-readable description of what went wrong"
}| Field | Type | Description |
|---|---|---|
detail | string | Human-readable error description. For 422 validation errors, this is an array of error objects instead (see below). |
Example bodies by status
| Status | Example body |
|---|---|
400 | {"detail": "Subject must be between 1 and 255 characters"} |
401 (invalid key) | {"detail": "Invalid API key"} (also sets WWW-Authenticate: Bearer) |
401 (missing credentials) | {"detail": "Missing authentication credentials"} |
403 (missing scope) | {"detail": "API key missing required scope: tickets:write"} |
403 (denied endpoint) | {"detail": "API keys are not permitted for this endpoint"} |
404 | {"detail": "Ticket not found"} |
409 | {"detail": "Idempotency key reused with a different payload"} |
500 | {"detail": "Internal server error"} |
Validation errors (422)
For request-body validation, FastAPI returns a 422 whose detail is an array of error objects with loc / msg / type keys — not a custom {field, message} array:
{
"detail": [
{
"loc": ["body", "priority"],
"msg": "Input should be 'low', 'medium', 'high' or 'urgent'",
"type": "literal_error"
}
]
}Note that some endpoints surface business-rule validation as a plain-string 400 instead. For example, the public tickets endpoint raises 400 with {"detail": "<message>"} when ticket creation fails a business rule, rather than a structured 422.
Rate Limiting
Every API-key request passes two limits, and both count per API key.
Plan limit. It depends on your tenant's subscription plan and on the kind of endpoint. Each tier has a per-minute limit and a burst limit over 10 seconds; a request must fit both.
| Plan | General endpoints | Search | Export |
|---|---|---|---|
| Free, Starter | 100/min, burst 40 per 10 s | 20/min, burst 10 per 10 s | 5/min, burst 2 per 10 s |
| Pro, Enterprise | 300/min, burst 120 per 10 s | 60/min, burst 30 per 10 s | 15/min, burst 6 per 10 s |
Any other plan, and a tenant without an active subscription, gets the Free limits. Export applies
to endpoints whose path contains export (except GET on an …/exports list); Search applies to
/api/v1/conversations with a search or query parameter; everything else is General.
Responses that pass the plan limiter carry its headers for the tier the request used:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute |
X-RateLimit-Remaining | Requests left in the current minute |
X-RateLimit-Reset | Unix time one full minute window after this request |
X-RateLimit-Burst-Limit | Requests allowed per 10-second burst window |
X-RateLimit-Burst-Remaining | Requests left in the burst window |
X-RateLimit-Burst-Reset | Unix time one full burst window after this request |
When either window is exhausted, the API returns a 429 with those headers plus Retry-After:
HTTP/1.1 429 Too Many Requests
Retry-After: 59
X-RateLimit-Limit: 100
X-RateLimit-Burst-Limit: 40
X-RateLimit-Burst-Remaining: 0{
"error": "rate_limit_exceeded",
"message": "Rate limit exceeded. Try again in 59 seconds.",
"retry_after": 59,
"tier": "general",
"plan": "bundle_free"
}Per-key ceiling. On every plan, one API key may also make at most 300 requests per minute.
This check runs first; when it is exceeded the 429 carries Retry-After, X-RateLimit-Limit
and X-RateLimit-Remaining, and a detail body:
{
"detail": "API key rate limit exceeded",
"limit": 300
}A few specialized endpoints (for example, Knowledge Base ingestion) add stricter per-endpoint
limits, listed on their own pages. Most of those return {"error": "Rate limit exceeded: …"} and may omit
Retry-After; back off for the window shown on the endpoint's page.
Branch on the 429 status and honour Retry-After when it is present rather than parsing the body.
Retry Strategy
For transient errors (429, 500, 503), use exponential backoff with jitter:
import time
import random
import requests
def request_with_retry(method, url, max_retries=3, **kwargs):
for attempt in range(max_retries + 1):
response = requests.request(method, url, **kwargs)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 5))
time.sleep(retry_after)
continue
if response.status_code in (500, 503) and attempt < max_retries:
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
continue
return response
return responseasync function requestWithRetry(
url: string,
options: RequestInit,
maxRetries = 3
): Promise<Response> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = parseInt(
response.headers.get("Retry-After") ?? "5"
);
await new Promise((r) => setTimeout(r, retryAfter * 1000));
continue;
}
if ([500, 503].includes(response.status) && attempt < maxRetries) {
const delay = 2 ** attempt + Math.random();
await new Promise((r) => setTimeout(r, delay * 1000));
continue;
}
return response;
}
throw new Error("Max retries exceeded");
}Best Practices
- Branch on the HTTP status code — Don't depend on a machine-readable
errorfield; standard errors return adetailstring - Log the full response — Include the
detailstring for debugging - Watch
X-RateLimit-RemainingandX-RateLimit-Burst-Remaining— Pace your requests to stay under both the per-minute and the 10-second burst limit - Respect
Retry-After— On a429, don't retry faster than the header suggests - Don't retry
4xx— Client errors (except429) indicate a problem with your request