Error shapes
PyAI returns two error shapes depending on which layer produced the error. Branch
on the stable code, never the human message.
Data plane (gateway) — OpenAI-compatible
Auth, scope, rate-limit, and billing errors use the OpenAI envelope:
{
"error": {
"message": "Rate limit exceeded.",
"type": "rate_limit_error",
"code": "rate_limit_exceeded",
"param": null
}
}
Control plane — RFC 7807 problem+json
Request-validation and resource errors (e.g. 400, 404, 409) use
application/problem+json. The stable code is the last path segment of
type:
{
"type": "https://api.pyai.com/problems/idempotency_conflict",
"title": "Conflict",
"status": 409,
"detail": "Idempotency-Key was reused with a different request body.",
"request_id": "req_..."
}
Error code reference
| HTTP | Code | Meaning | Retry? |
|---|
| 401 | unauthorized | Missing/invalid/revoked key | No |
| 403 | forbidden | Key lacks the required scope | No |
| 403 | origin_not_allowed | Publishable-token origin not allow-listed | No |
| 400 | invalid_agent_id | agent_id on a realtime/Omni URL is malformed (control chars / too long) | No — fix the id |
| 402 | credit_exhausted | Org out of prepaid credit | No — add credit |
| 402 | key_budget_exceeded | Per-key monthly cap reached | No — raise budget |
| 402 | insufficient_quota | Plan quota exhausted | No — upgrade |
| 409 | idempotency_conflict | Idempotency-Key reused with a different body | No — use a new key |
| 429 | rate_limit_exceeded | Per-key/IP rate limit | Yes — honor Retry-After |
| 429 | concurrency_limit_exceeded | Too many concurrent realtime sessions | Yes — wait + retry |
| 429 | daily_cap_exceeded | Sandbox/publishable daily cap | Yes — after reset |
Rate limits
Every key has a per-second rate limit (with burst) and a cap on concurrent
realtime sessions, set by your plan. Exceeding either returns 429 with a
Retry-After header (seconds to wait). Back off and retry; the official SDKs do
this automatically.
Idempotency
POST /v1/transcription/jobs accepts an Idempotency-Key header so a retried
request can’t create a duplicate job:
- Same key + same body → the original response is replayed (no new job).
- Same key + different body →
409 idempotency_conflict.
Send a fresh idempotency key (e.g. a UUID) per logical operation, and reuse it
when retrying that exact operation after a network blip.
List endpoints are cursor-paginated, newest first. Pass limit (1–100, default
20) and the previous page’s next_cursor as cursor. next_cursor is null
on the last page.