aethexDocs

Errors

Every status code the API returns, with concrete JSON bodies.

Standard error envelope

Every error response carries four top-level JSON fields: error (human-readable message), code (stable slug for programmatic branching — e.g. not_found, validation_error, rate_limited), request_id (opaque string also returned in the X-Request-ID response header — a UUID when the server generates it, but echoed verbatim if the client supplied X-Request-ID on the request, so do not assume a UUID format), and detail (legacy field — usually a string mirror of error, but on schema-validation failures (Pydantic-layer 422s) it is an array of per-field objects matching fields, and a few payloads (e.g. billing 402) return a structured object; handle non-string shapes). Log request_id on your side and include it when you contact our team at [email protected]; it is the fastest way to look up a specific request in our logs.Schema-validation failures (Pydantic-layer 422s) include a fields array (the same array-shaped detail) listing per-field objects: loc is the JSON path of the failing field, msg is the human-readable reason, and type is the validation rule that fired. Fix every entry before retrying. Business-logic 422s (route handlers that explicitly raise HTTPException(422) for cases like missing update fields or a disallowed resource_type) return detail as a plain string and do not include fields — check the type of detail (or the presence of fields) before iterating.

Generic error shape
json
{
  "error": "Missing API key",
  "code": "authentication_error",
  "request_id": "0cf409f7-382a-4c24-be9f-ae1192c38619",
  "detail": "Missing API key"
}

Authentication & authorization

Returned when your key is missing, invalid, or lacks the scope an endpoint requires. None of these are safe to retry without changing the request: a retry with the same key will get the same response.

401Unauthorized: missing API key

No X-API-Key, xi-api-key, or Authorization: Bearer '<'key'>' header on the request, OR the header was sent under another name. The gateway never falls back to "no auth" on metered routes.

Add X-API-Key: $AETHEX_API_KEY, xi-api-key: $AETHEX_API_KEY, or Authorization: Bearer $AETHEX_API_KEY to every request. See Authentication for the equivalent header forms. Not safe to retry without adding the header.

json
{
  "error": "Missing API key",
  "code": "authentication_error",
  "request_id": "0cf409f7-382a-4c24-be9f-ae1192c38619",
  "detail": "Missing API key"
}
401Unauthorized: invalid or expired key

Key is malformed, has been revoked from the dashboard, was rotated and you are still using the old value, or belongs to an account that has been deleted or disabled. The key prefix you sent is logged on our side; check X-Request-ID if you need us to confirm which key was rejected.

Generate a fresh key at developers.aethexai.com/dashboard/api-keys and rotate the value in your environment. If you just rotated, double-check you redeployed every service that holds the key. Not safe to retry without a new key.

json
{
  "error": "Invalid or expired API key",
  "code": "authentication_error",
  "request_id": "0cf409f7-382a-4c24-be9f-ae1192c38619",
  "detail": "Invalid or expired API key"
}
403Forbidden: missing scope

Your key is valid but does not carry the scope the endpoint requires. The detail field names the missing scope verbatim. Example: sending a calls:read-only key to POST /calls/trigger, or a voices:read-only key to POST /tts. Scopes are set when the key is created and cannot be edited after, only re-created.

Look at the endpoint in the API reference to see which scope it requires, then create a new key with that scope set. The full scope chart is at Scopes. Not safe to retry until you swap to a key with the right scopes.

json
{
  "error": "Missing scope: 'calls:write'",
  "code": "forbidden",
  "request_id": "0cf409f7-382a-4c24-be9f-ae1192c38619",
  "detail": "Missing scope: 'calls:write'"
}

Bad requests

Returned when the request itself is malformed, oversized, or fails schema validation. These are caller-side issues; retrying the same payload will return the same error.

400Bad Request

Generic malformed request. Typical causes: body is not valid JSON, the Content-Type header is missing or wrong, conflicting query parameters were sent (e.g. both limit and a contradictory pagination flag), or a path parameter is malformed (non-UUID in a UUID slot).

Verify the body parses as JSON (echo $body | jq . is a quick sanity check), confirm the Content-Type: application/json header is set, and double-check the path / query parameters against the API reference. Once you fix the payload, retry.

json
{
  "error": "Invalid JSON body",
  "code": "bad_request",
  "request_id": "0cf409f7-382a-4c24-be9f-ae1192c38619",
  "detail": "Invalid JSON body"
}
413Payload Too Large

The request body exceeded the per-route size limit. Most common on transcription endpoints when uploading audio inline (POST /transcribe) past ~10MB. Inline uploads are capped by design; large audio belongs on the presigned-upload path.

For audio larger than ~10MB, use the presigned upload flow: POST /uploads/presign to get a signed upload URL, PUT the bytes directly to that URL, then call POST /transcribe/by-upload with the returned upload_id. This is documented under Transcription.

json
{
  "error": "Request body too large",
  "code": "payload_too_large",
  "request_id": "0cf409f7-382a-4c24-be9f-ae1192c38619",
  "detail": "Request body too large"
}
422Unprocessable Entity: validation error

A request body field or query parameter failed schema validation. The detail field is an array of per-field error objects (not a single string), so you can map each entry back to the exact JSON path that failed. Common triggers: missing required field, wrong type (string where int expected), string below the minimum length, value not in an allowed enum, malformed UUID / phone number / URL.

For each entry in the detail array: loc is the JSON path of the bad field (["body", "voice_id"]), msg is the human-readable reason, and type is the validation rule that fired (missing, string_too_short, string_pattern_mismatch, etc.). Fix every entry, then retry; partial fixes will still 422 on the remaining ones.

json
{
  "error": "Validation failed",
  "code": "validation_error",
  "request_id": "3fddfe81-53f3-4ca1-9cea-f5fbc73ce5a2",
  "detail": [
    {
      "loc": ["body", "voice_id"],
      "msg": "Field required",
      "type": "missing"
    },
    {
      "loc": ["body", "system_prompt"],
      "msg": "String should have at least 1 character",
      "type": "string_too_short"
    }
  ],
  "fields": [
    {
      "loc": ["body", "voice_id"],
      "msg": "Field required",
      "type": "missing"
    },
    {
      "loc": ["body", "system_prompt"],
      "msg": "String should have at least 1 character",
      "type": "string_too_short"
    }
  ]
}

Resource state

Returned when the resource you asked for is missing, owned by another account, or in a state that conflicts with the requested action.

404Not Found

The path resource (agent, call, voice, phone number, etc.) does not exist on your account. Note: cross-account access also returns 404 (not 403) to prevent ID enumeration. So a 404 means either the resource genuinely does not exist, or it exists but belongs to a different account.

Verify the ID by listing resources first (GET /agents etc.) to confirm it appears on your account. If you just created the resource, give it a beat to commit; the POST response is the canonical source of truth for the new ID. Safe to retry once you confirm the right ID.

json
{
  "error": "Agent 'd53208ef-2f34-4be2-8bea-35cd55b399ac' not found",
  "code": "not_found",
  "request_id": "0cf409f7-382a-4c24-be9f-ae1192c38619",
  "detail": "Agent 'd53208ef-2f34-4be2-8bea-35cd55b399ac' not found"
}
409Conflict

The resource is in a state that conflicts with the requested action. Concrete cases: ending a WebRTC session that already ended ("Session is not active"), idempotency-key reuse with a different request body (Stripe-style protection against double-charge bugs), or a uniqueness violation on registration (e.g. registering the same phone number twice).

Inspect the current state via the matching GET endpoint before retrying. For idempotency conflicts, generate a fresh key per logical operation. For uniqueness conflicts, look up the existing resource and decide whether to update it instead of recreating it. Not safe to blind-retry.

json
{
  "error": "Session is not active. The pipeline may have already ended.",
  "code": "conflict",
  "request_id": "0cf409f7-382a-4c24-be9f-ae1192c38619",
  "detail": "Session is not active. The pipeline may have already ended."
}

Rate limits & billing

Returned when your account is healthy but the request was throttled or your account has a billing block preventing metered operations.

402Payment Required

Your credit balance reached zero, the account is in dunning after a failed renewal charge, or the card was declined so many times it is locked. Credits are prepaid: at zero the account is hard-blocked on the spot, and nothing accrues as debt. Affects metered routes only: voice call placement, WebRTC sessions, TTS, transcription. Read routes (agents, usage, conversations) keep working so you can investigate without flying blind. The top-level code is a billing slug such as billing.no_credits or billing.payg_card_repeatedly_failed (not the generic payment_required), and detail is a structured object {code, message, action_url} — branch on the inner code slug for billing-specific handling.

Visit developers.aethexai.com/dashboard/billing to add or update a payment method, top up credits, or upgrade plans. After Stripe confirms the new payment, metered routes unblock on the next request. See Billing for the full lifecycle.

json
{
  "error": "Card declined on multiple retry attempts. Update your payment method to resume calls.",
  "code": "billing.payg_card_repeatedly_failed",
  "request_id": "0cf409f7-382a-4c24-be9f-ae1192c38619",
  "detail": {
    "code": "billing.payg_card_repeatedly_failed",
    "message": "Card declined on multiple retry attempts. Update your payment method to resume calls.",
    "action_url": "/dashboard/billing"
  }
}
429Too Many Requests

You hit the per-minute or per-day request cap on the key making the request. Caps default to 60 RPM and 10,000 per day per key; the server returns the current state in X-RateLimit-Remaining-RPM / X-RateLimit-Remaining-Daily on every response so you can back off before hitting the wall. The Retry-After header on the 429 tells you how many seconds until the bucket refills.

Honor Retry-After for the first retry. For sustained 429s, implement exponential backoff with jitter (a hard-loop just lengthens the throttle). If you need higher throughput, split traffic across multiple keys (each gets its own bucket) or contact our team at [email protected] so we can raise the cap. See Rate limits for the full headers.

json
{
  "error": "Rate limit exceeded",
  "code": "rate_limited",
  "request_id": "0cf409f7-382a-4c24-be9f-ae1192c38619",
  "detail": "Rate limit exceeded"
}

Server side

5xx errors are on us. They are always worth one retry with backoff; if they persist, contact our team at [email protected] with the X-Request-ID from the failing response and we can trace it directly.

500Internal Server Error

Unexpected server-side error. Every 500 is logged with the request ID and an alert fires on our side. If you see a sustained pattern of 500s on one endpoint, that is a real outage we are likely already investigating.

Retry once with exponential backoff. If it persists, contact our team at [email protected] with the X-Request-ID value from any of the failing responses. Check status.aethexai.com first for any active incident.

json
{
  "error": "Internal server error",
  "code": "internal_error",
  "request_id": "0cf409f7-382a-4c24-be9f-ae1192c38619",
  "detail": "Internal server error"
}
502Bad Gateway

An upstream dependency the API relies on returned an error or timed out. The API itself is healthy; the dependency is not.

Retry with exponential backoff. If Retry-After is present, honor it. Persistent 502s on one specific surface (TTS, transcription, etc.) usually indicate an upstream incident; check status.aethexai.com.

json
{
  "error": "Upstream service unavailable",
  "code": "bad_gateway",
  "request_id": "0cf409f7-382a-4c24-be9f-ae1192c38619",
  "detail": "Upstream service unavailable"
}
503Service Unavailable

Service is in maintenance mode, overloaded, or temporarily at capacity for the requested language. The response may include a Retry-After header telling you when to retry; if missing, default to exponential backoff.

Honor Retry-After if present, otherwise back off and retry. For repeated 503s on voice / WebRTC routes specifically, the service is at capacity; spreading load across a longer window or reaching out for capacity planning are the only fixes.

json
{
  "error": "Service temporarily unavailable",
  "code": "service_unavailable",
  "request_id": "0cf409f7-382a-4c24-be9f-ae1192c38619",
  "detail": "Service temporarily unavailable"
}