#Error Responses

7 min read

The AiDial API returns structured JSON error responses for application-level failures. The response format varies by endpoint domain because some routers expose domain-specific contracts.

Infrastructure-level failures, such as an upstream proxy rate limit, may return a non-JSON body. Check the Content-Type header before parsing an error response.

#Error Response Shapes

Endpoint DomainShapeFields
HTTPException-based endpoints (general)Generalerror, status_code
FastAPI request validation (422)Validationerror, details, status_code
Partner API endpoints using core.error_envelopePartnererror.code, error.message, error.message_key, error.status, error.request_id, error.details
Billing and portal settings endpointsDomaincode, message, field_errors, block_reason, retry_after_seconds
Calls export and transcript-download endpointsExport/downloaderror, code, message, messageKey, retry_after_seconds
Compliance audit endpointsSecurityerror.code, error.message, error.message_key, error.request_id, error.details
Health endpointHealthstatus, version, database, secret_service, billing, reports_runtime

#General Error Response

Used when a route raises HTTPException and the global handler in api.main serializes it. Most error values are strings. Some dependencies pass structured detail objects, in which case error is that object.

FieldTypeDescription
errorstring or objectError description or structured detail
status_codeintegerHTTP status code (matches response status)
JSON
{
  "error": "Error description here",
  "status_code": 401
}

#Validation Error Response (422)

Returned when FastAPI request parameters or body validation fails and the global validation handler processes the error.

FieldTypeDescription
errorstringAlways "Validation error"
detailsarrayList of validation error objects
details[].locarrayPath to the invalid field (for example, ["query", "page"])
details[].msgstringHuman-readable validation message
details[].typestringError type (for example, missing, greater_than_equal)
status_codeintegerAlways 422

Some domain routers parse request bodies themselves and return 400 or 422 in their domain error shape instead of this framework validation shape. POST /v1/compliance/audit-events/decisions has a special validation handler that returns a 400 security envelope with error.code set to validation_failed.

#Partner Error Response

Used by partner-facing endpoints that call core.error_envelope, including partner identity/API-key/webhook routes and several partner project, phone-number, knowledge-base, catalog, and call-control routes.

FieldTypeDescription
error.codestringMachine-readable error code
error.messagestringHuman-readable, non-disclosing message
error.message_keystringLocalisation key
error.statusintegerHTTP status code
error.request_idstringCorrelation identifier
error.detailsobject (optional)Structured details, such as field_errors
JSON
{
  "error": {
    "code": "not_found",
    "message": "Resource not found.",
    "message_key": "not_found",
    "status": 404,
    "request_id": "9b603a24-4d95-4d0b-a865-20c0f8a2c5de"
  }
}

Partner validation errors use the same envelope with field errors under error.details.field_errors:

JSON
{
  "error": {
    "code": "validation_error",
    "message": "Validation failed.",
    "message_key": "validation_error",
    "status": 422,
    "request_id": "9b603a24-4d95-4d0b-a865-20c0f8a2c5de",
    "details": {
      "field_errors": [
        {
          "field": "project_id",
          "message": "Project is required.",
          "code": "required"
        }
      ]
    }
  }
}

#Domain Error Response

Used by billing endpoints and several portal-facing settings/compliance endpoints, such as business hours, call limits, compliance copy, transfer settings, tenant settings, notification preferences, MFA lifecycle, IP allowlist, retention policy, and deletion requests.

FieldTypeDescription
codestringMachine-readable error code
messagestringHuman-readable error description
field_errorsobject (optional)Field-level validation errors
block_reasonstring (optional)Reason a settings update is blocked
retry_after_secondsinteger (optional)Seconds to wait before retrying
JSON
{
  "code": "validation_failed",
  "message": "Request validation failed.",
  "field_errors": {
    "client_id": "client_id is required."
  }
}

Billing 429 responses include a Retry-After response header. MFA lifecycle rate-limit responses include both Retry-After and retry_after_seconds.

#Export/Download Error Response

Used by POST /v1/calls/export, GET /v1/calls/{call_id}/transcript-download, and GET /v1/calls/{call_id}/transcript/pdf.

FieldTypeDescription
errorstringError category (for example, rate_limited, content_too_large, not_found, bad_request)
codestringMachine-readable error code
messagestringHuman-readable description
messageKeystringLocalisation key
retry_after_secondsinteger (optional)Seconds to wait before retrying
field_errorsarray (optional)Field-level validation errors on request-validation failures
JSON
{
  "error": "rate_limited",
  "code": "EXPORT_THROTTLED",
  "message": "Export throttled. Please wait and try again.",
  "messageKey": "calls.export.messages.throttled",
  "retry_after_seconds": 60
}

#Security Error Response

Used by compliance audit endpoints that return SecurityErrorEnvelope.

FieldTypeDescription
error.codestringMachine-readable error code
error.messagestringUser-safe non-disclosing message
error.message_keystringLocalisation key
error.request_idstringCorrelation identifier
error.detailsobject (optional)Structured details for validation contexts
JSON
{
  "error": {
    "code": "not_found",
    "message": "Requested resource not found.",
    "message_key": "security.not_found",
    "request_id": "9b603a24-4d95-4d0b-a865-20c0f8a2c5de"
  }
}

#Health Error Response

GET /v1/health does not use the general error envelope. It returns the health payload with status 200 when healthy and 503 when unhealthy. The database block checks the local API metadata store and includes database.db_writable when the database check succeeds.

Overall health is currently driven by the local API metadata store and secret-service checks. billing and reports_runtime are included for operational visibility and do not currently determine whether the endpoint returns 200 or 503.

JSON
{
  "status": "unhealthy",
  "version": "1.0.0",
  "database": {
    "status": "error",
    "message": "Connection refused"
  },
  "secret_service": {
    "status": "connected",
    "host": "secret-service"
  },
  "billing": {
    "status": "error",
    "code": "billing_configuration_invalid",
    "message": "Billing Stripe key must be a restricted key."
  },
  "reports_runtime": {
    "status": "degraded",
    "schedules_table": false,
    "deliveries_table": false
  }
}

#Authentication and Scope Semantics

Protected routes using the shared authentication dependency resolve credentials in this order:

  1. X-API-Key
  2. Authorization: Bearer <jwt>

Both modes resolve to an AuthContext. Bearer authentication requires ZITADEL_ISSUER, ZITADEL_AUDIENCE, and ZITADEL_JWKS_URL; these settings are required and have no fallback defaults.

Missing, invalid, revoked, or expired credentials return 401. If a valid bearer token has no active portal assignment, the API returns 404 to avoid tenant enumeration. Out-of-scope resources also return 404, not 403.

Analytics and call-data access is routed by the strict (client_id, data_env) mapping in the tenant routing map. A missing route is server misconfiguration and returns 500. A routed host, tunnel, or database outage returns 503.

#PII Failure Mode

PII-sensitive call responses respect the client-level PII flags in aidial_clients. Phone and transcript decryption goes through server-side decryption helpers in the server-side decryption layer. When permissions are missing or decryption/retrieval fails, the API fails closed by redacting or omitting PII. Raw encrypted database fields such as *_encrypted, ciphertext, nonces, and wrapped keys are not partner response fields.

#Status Code Examples

#400 Bad Request

Domain routers use 400 for invalid JSON, invalid filters, stale settings state, or domain validation failures.

JSON
{
  "code": "validation_failed",
  "message": "Request validation failed.",
  "field_errors": {
    "updates": "Duplicate categories are not allowed."
  }
}

#401 Unauthorized

Missing, invalid, revoked, or expired API key or bearer token.

JSON
{
  "error": "Missing authentication. Provide X-API-Key or Authorization: Bearer token.",
  "status_code": 401
}

#403 Forbidden

Valid authentication, but the requested operation is explicitly forbidden. Tenant scope failures normally return 404 instead.

JSON
{
  "error": "Session management requires portal authentication.",
  "status_code": 403
}

Authenticated requests blocked by the tenant's own IP allowlist use a domain response:

JSON
{
  "code": "ip_allowlist_restricted",
  "message": "Access restricted."
}

#404 Not Found

Resource does not exist, is outside your client scope, or is hidden to avoid tenant enumeration.

Anti-enumeration behaviour: Accessing resources outside your client scope returns 404 Not Found (not 403 Forbidden). This prevents discovery of resources belonging to other organisations.

JSON
{
  "error": "Not found",
  "status_code": 404
}

#409 Conflict

The requested operation conflicts with current state, such as duplicate idempotency use, an export already in progress, or a stale revision.

JSON
{
  "error": {
    "code": "conflict",
    "message": "A matching request with this Idempotency-Key is already in progress or has already completed.",
    "message_key": "conflict",
    "status": 409,
    "request_id": "9b603a24-4d95-4d0b-a865-20c0f8a2c5de"
  }
}

#413 Payload Too Large

The request or generated content exceeds a route-specific limit.

JSON
{
  "error": "content_too_large",
  "code": "DOWNLOAD_PDF_TOO_LARGE",
  "message": "Transcript is too large for PDF generation. Please use TXT format instead.",
  "messageKey": "calls.transcriptDownload.messages.pdfTooLarge"
}

#422 Unprocessable Entity

Framework request validation failed. Check the details array for specific field errors.

JSON
{
  "error": "Validation error",
  "details": [
    {
      "type": "greater_than_equal",
      "loc": ["query", "page"],
      "msg": "Input should be greater than or equal to 1"
    }
  ],
  "status_code": 422
}

#429 Too Many Requests

Rate limit exceeded. See Rate Limiting for handling guidance.

HTTPException-based 429:

JSON
{
  "error": "Rate limit exceeded",
  "status_code": 429
}

Partner 429 (with Retry-After header):

JSON
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Please retry shortly.",
    "message_key": "rate_limited",
    "status": 429,
    "request_id": "9b603a24-4d95-4d0b-a865-20c0f8a2c5de"
  }
}

Billing 429 (with Retry-After header):

JSON
{
  "code": "billing_provider_rate_limited",
  "message": "Billing provider rate limit exceeded."
}

Export/download 429 (with retry timing in body):

JSON
{
  "error": "rate_limited",
  "code": "EXPORT_THROTTLED",
  "message": "Export throttled. Please wait and try again.",
  "messageKey": "calls.export.messages.throttled",
  "retry_after_seconds": 60
}

#500 Internal Server Error

An unexpected server error or server-side misconfiguration occurred. Contact support if this persists.

Examples include missing required bearer-auth configuration or a missing (client_id, data_env) route in the tenant routing map.

JSON
{
  "error": "Internal server error",
  "status_code": 500
}

#503 Service Unavailable

A dependency is temporarily unavailable. Retry with exponential backoff.

Examples include authentication dependency lookup failures, routed analytics host/tunnel/database outages, secret-service health failures, and domain-service outages.

JSON
{
  "error": "Data host is currently unavailable. Please try again later.",
  "status_code": 503
}