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 |
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": "enum"
}
]
}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
The API allows a default of 300 requests per minute per API key in both production and staging. When you exceed that limit, the API returns a 429 with the standard detail body:
HTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 0{
"detail": "API key rate limit exceeded",
"limit": 300
}The 429 response carries these headers:
| Header | Meaning |
|---|---|
Retry-After | Seconds to wait before retrying |
X-RateLimit-Limit | Requests allowed in the window (300) |
X-RateLimit-Remaining | Requests remaining in the window |
A few specialized endpoints (for example, Knowledge Base ingestion) enforce stricter, per-endpoint limits through a separate limiter and return an { "error": "rate_limit_exceeded", "message": "...", "retry_after": N } body instead — that envelope is the exception, not the norm.
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-Remaining— Pace your requests to stay under the per-key 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