#Rate Limiting

6 min read

The AiDial API enforces application-level and infrastructure-level rate limits to protect service availability. When a request is rejected by a rate limiter, the API returns a 429 Too Many Requests response.

#Detecting Rate Limits

A 429 HTTP status code indicates you have been rate-limited. Before parsing the response body, check the Content-Type header and retry signals:

  • JSON body (application/json): Parse the response for error details and retry timing
  • Non-JSON body (text/plain or text/html): Infrastructure-level rate limit; do not attempt to parse as JSON
  • Retry timing: Prefer the Retry-After response header when present; otherwise use a documented retry field in the JSON body, such as retry_after_seconds

#Current Application and Edge Limits

SurfaceCurrent limitRetry signal
Edge proxy main API routerTraefik average 1,200 requests per 1-minute period with a burst of 300No JSON contract guaranteed
Partner API key and webhook operations5 requests per authenticated identity, target tenant, and operation per 60 seconds for API-key create/rotate and webhook create/rotate-secret/test routesRetry-After header
CSV export3 requests per user/tenant pair, and 12 requests per tenant, per 600 secondsretry_after_seconds body
Transcript downloads10 requests per user/tenant pair, and 60 requests per tenant, per 300 seconds across TXT and PDF downloadsretry_after_seconds body
SCIM security endpoints30 configuration updates per authenticated identity, tenant, data environment, and operation per 60 seconds; 120 provisioning events per scoped API-key identity, tenant, and data environment per 60 secondsRetry-After header
Communication email routingAdd email: 10 per user/tenant per 24 hours; resend verification: 3 per user/email/tenant per 1 hour; public token verification: 10 per source IP per 60 secondsRetry-After header and retry_after_seconds body
Account export jobs3 create requests per user/tenant/data environment, and 10 per tenant/data environment, per 24 hoursRetry-After header
Report schedule sample sends5 per actor/tenant per 600 seconds, and 3 per tenant/frequency/recipient-set per 900 secondsRetry-After header and details.retry_after_seconds body
Business-hours project options20 requests per authenticated actor per 60 secondsfield_errors.retry_after_seconds body
Portal MFA lifecycle mutation5 requests per client, user, and action per 60 secondsRetry-After header and retry_after_seconds body
Support portal surfacesRoute-local per-actor/operation limits over 60 seconds: ticket search 30, badge/open-count 60, related-call validation 60, related-call attach/detach 20, recipient add/remove 20, attachment readiness/list/scan-status 60, attachment finalise/download 30, attachment session-start/delete 12No retry timing
Public demo dial-meCurrent configured limits are 3 per phone per hour, 10 per phone per day, and 10 per resolved public source IP per day; active call and runtime capacity blocks also return 429No retry timing

#Response Formats

For public demo dial-me IP limiting, the source IP is resolved through the trusted request-provenance helper before counters are updated. Spoofed forwarded headers from untrusted direct peers are ignored, while malformed trusted proxy chains or chains that resolve only to internal/trusted infrastructure are rejected with 400 before dispatch.

#General HTTPException-Based Routes

Some routes return the general error shape from the global FastAPI exception handler:

Text
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
JSON
{
  "error": "Rate limit exceeded",
  "status_code": 429
}

These responses do not guarantee retry timing.

When an HTTPException carries a structured detail object, the global handler wraps that object in error. The Support portal surfaces use this form, raising a 429 with a rate_limit_exceeded code and an operation-specific message and no retry timing:

JSON
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Too many requests."
  },
  "status_code": 429
}

#Partner Endpoints

Partner endpoints using the shared rate limiter return the partner error envelope and include a Retry-After response header:

Text
HTTP/1.1 429 Too Many Requests
Retry-After: 3
Content-Type: application/json
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"
  }
}

SCIM rate limits use the same partner envelope with code set to rate_limited; the message text is SCIM-specific.

#Billing Endpoints

Billing 429 responses use the billing domain error shape. When the billing provider supplies retry timing, AiDial includes a Retry-After response header with the number of seconds to wait:

Text
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
JSON
{
  "code": "billing_provider_rate_limited",
  "message": "Billing provider rate limit exceeded."
}

#Export and Transcript Download Endpoints

POST /v1/calls/export, GET /v1/calls/{call_id}/transcript-download, and GET /v1/calls/{call_id}/transcript/pdf return export/download error bodies. Their 429 responses include retry_after_seconds in the response 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
}

Transcript TXT and PDF downloads share the same download limiter and use DOWNLOAD_THROTTLED:

JSON
{
  "error": "rate_limited",
  "code": "DOWNLOAD_THROTTLED",
  "message": "Transcript download throttled. Please wait and try again.",
  "messageKey": "calls.transcriptDownload.messages.throttled",
  "retry_after_seconds": 9
}

Concurrent CSV exports for the same user and tenant return 409 Conflict with EXPORT_IN_PROGRESS, not 429.

#Portal Domain Endpoints

Several portal-facing domain endpoints return a flat domain error shape.

Communication email routing uses code: "rate_limited" and includes both Retry-After and retry_after_seconds:

JSON
{
  "code": "rate_limited",
  "message": "Request rate limit exceeded.",
  "retry_after_seconds": 60
}

Account export job creation uses code: "export_rate_limited" and includes Retry-After:

JSON
{
  "code": "export_rate_limited",
  "message": "Account export request limit reached for this user."
}

Report schedule sample sends include retry timing under details and set Retry-After:

JSON
{
  "code": "rate_limited",
  "message": "Too many requests. Please try again later.",
  "details": {
    "reason": "actor",
    "retry_after_seconds": 600
  }
}

Business-hours project options use the domain error shape and put retry timing in field_errors:

JSON
{
  "code": "business_hours_options_rate_limited",
  "message": "Request rate limit exceeded.",
  "field_errors": {
    "retry_after_seconds": "60"
  }
}

#Portal MFA Lifecycle

PUT /v1/portal-mfa-lifecycle applies a route-local limiter per client, user, and action. Its 429 response uses the domain error shape and includes both Retry-After and retry_after_seconds:

Text
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
JSON
{
  "code": "rate_limited",
  "message": "Too many requests. Please wait and try again.",
  "retry_after_seconds": 60
}

#Infrastructure-Level Rate Limiting

Infrastructure-level rate limiting is enforced by Traefik for the main API router with an average of 1,200 requests per minute and a burst of 300. It may return a 429 response with a non-JSON body (plain text or HTML). Your integration must handle this gracefully:

Text
HTTP/1.1 429 Too Many Requests
Content-Type: text/plain

Rate limit exceeded

Use exponential backoff with jitter:

  1. Check Content-Type before parsing the 429 body
  2. If Retry-After header is present: Wait the specified number of seconds
  3. If JSON with documented retry timing: Use retry_after_seconds, details.retry_after_seconds, or field_errors.retry_after_seconds where documented for that endpoint
  4. If non-JSON or no retry timing: Wait a minimum of 60 seconds
  5. On subsequent retries: Double the wait time (60s, 120s, 240s) up to a maximum of 10 minutes
  6. Add jitter: Add a random delay of 0-5 seconds to avoid thundering herd effects