# Error codes and responses

Every Paxa API failure is an RFC 9457 problem+json body whose title field is a stable machine-readable code.

Every failure is an RFC 9457 problem details response with the `application/problem+json` content type, at the minimum shape. Two fields describe the failure:

- `title` carries the stable code to branch on.
- `status` mirrors the HTTP status.

Two codes add two fields that name the field at fault. A [schema_invalid](https://paxalabs.com/docs/errors#schema_invalid) or a [validation](https://paxalabs.com/docs/errors#validation) refusal of one field carries `path`, the field, and `reason`, a stable code. A validation `path` starts with where the field was sent, as in `body.voice` or `headers.idempotency-key`, and its reasons are these:

| reason | Meaning |
| --- | --- |
| missing | A required field is absent. |
| wrong_type | The field holds the wrong JSON type, such as a string where a number goes. |
| too_short | A string or list is shorter than the field's minimum. |
| too_long | A string or list is longer than the field's maximum. |
| too_small | A number is below the field's minimum. |
| too_large | A number is above the field's maximum. |
| not_allowed | The value is not one of the values the field accepts. |
| malformed | A string does not match the pattern or format the field requires. |
| unparsable | The body is not valid JSON or form data. No field could be read. |
| invalid | The field breaks another rule of the endpoint schema. |

Responses carry no prose. This page is the reference for every code's meaning and the recommended handling.

Error body:

```json
{"title": "insufficient_credits", "status": 402}
```

## Handle an error

Branch on `title`. Two codes can share a status, and the status alone does not identify a failure: both [rate_limited](https://paxalabs.com/docs/errors#rate_limited) and [concurrency_limited](https://paxalabs.com/docs/errors#concurrency_limited) answer 429. Retry `rate_limited` after `retry-after`, retry `concurrency_limited` when one of your requests finishes, refill on [402](https://paxalabs.com/docs/errors#insufficient_credits), and treat [502](https://paxalabs.com/docs/errors#provider_error) as refunded.

TypeScript:

```typescript
const response = await fetch("https://api.paxalabs.com/v1/tts", options);

if (!response.ok) {
  const problem = await response.json();
  // Branch on title: it is the stable code the errors reference documents.
  switch (problem.title) {
    case "rate_limited": {
      const seconds = Number(response.headers.get("retry-after") ?? "1");
      await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
      break;
    }
    case "insufficient_credits":
      notifyBilling();
      break;
    default:
      throw new Error(problem.title);
  }
}
```

Python:

```python
import time

response = requests.post(f"https://api.paxalabs.com/v1/tts", headers=headers, json=body)

if not response.ok:
    problem = response.json()
    # Branch on title: it is the stable code the errors reference documents.
    if problem["title"] == "rate_limited":
        time.sleep(int(response.headers.get("retry-after", "1")))
    elif problem["title"] == "insufficient_credits":
        notify_billing()
    else:
        raise RuntimeError(problem["title"])
```

> Validation runs before authentication, which means a malformed body sent with a bad key answers `400 validation`.

## Retry policy

Four codes are retryable. Retry [429 rate_limited](https://paxalabs.com/docs/errors#rate_limited) after the `retry-after` header. Retry [429 concurrency_limited](https://paxalabs.com/docs/errors#concurrency_limited) once one of the account's in-flight requests on that product finishes. That code carries no `retry-after`. Retry [502 provider_error](https://paxalabs.com/docs/errors#provider_error) with a new [Idempotency-Key](https://paxalabs.com/docs/idempotency); the failed charge is already refunded. Retry [503 provider_unavailable](https://paxalabs.com/docs/errors#provider_unavailable) once [GET /health](https://paxalabs.com/docs/api/health) reports the model served again. Every other code is terminal for that request. A 400-class code fails the same way until the request changes, [402 insufficient_credits](https://paxalabs.com/docs/errors#insufficient_credits) fails until the balance changes, and [429 feedback_limit](https://paxalabs.com/docs/errors#feedback_limit) fails until older reports leave the 24-hour window.

Every authenticated product request answers with an `x-request-id` header, failures included. Log it alongside the failure. The code and the id together make a complete incident report, which [POST /v1/feedback](https://paxalabs.com/docs/api/feedback) takes with `kind` set to `api`.

## Error reference

| Status | Code | Meaning | What to do |
| --- | --- | --- | --- |
| 400 | `validation` | The request body, query, or headers failed validation against the endpoint schema, or broke a rule the endpoint's reference states. A field the schema refused adds two fields to the problem body. path names the first failing field by location and name in dot form, such as body.voice or headers.idempotency-key. A body that could not be read gives the path body alone. reason is one stable code from the validation reasons list. | Fix the field at path as the reason says and retry. Compare the request against the endpoint's schema in this reference when the body names no field. |
| 400 | `unknown_model` | The model field does not name a served model. | GET /v1/models lists the catalog of served model ids. |
| 400 | `unknown_voice` | The voice id names no voice on the model's roster and no designed voice this account holds. | GET /v1/voices lists the id of every catalog voice, and GET /v1/voices/designed lists the account's designed voices. A designed voice id (voice_...) speaks only on Paxa TTS Flash v2 for the account that saved it, and stops once deleted. |
| 400 | `draft_invalid` | The draft field holds no draft this account received from POST /v1/voices/design. It is altered or cut short, belongs to another account, or no longer opens after a platform update. Nothing was charged. | Send the draft string exactly as the design response returned it, with a key of the same account. A draft that no longer opens cannot be recovered. Design the voice again. |
| 400 | `voice_name_taken` | The name is a catalog voice's name, which no designed voice can take. Nothing was saved. | Choose another name. GET /v1/voices lists the catalog's names. |
| 400 | `text_too_long` | The text exceeds the model's per-request character ceiling. | Split the text into shorter requests. GET /v1/models reports each model's ceiling as max_chars. |
| 400 | `tag_invalid` | The text holds a tag the model refuses. On Paxa TTS Flash v2, the bracket asks for what the model does not do, such as an accent, singing, or a sound outside the supported list. It can also hold no letters or carry a weight that is not a number. On a Paxa TTS Flash voice that takes emotion tags, the bracketed word is outside the tag list, or its weight is not a number. Nothing was charged. | On Paxa TTS Flash v2, the Direction tags guide lists every tag the model refuses. On Paxa TTS Flash, use a word from the tag list in the Emotion tags guide. Its weight is a number from 0 to 1 or a percentage, as in [sad:0.5], and a value past either end reads as that end. |
| 400 | `request_too_large` | The request's billable characters exceed the model's total ceiling. Translation bills every field: instructions, context, examples, do_not_translate strings, and the glossary count toward the ceiling alongside the text. | Shorten the request or move terms out of the glossary. GET /v1/models reports the total ceiling as max_request_chars and the text-only ceiling as max_chars. |
| 400 | `document_invalid` | The document field could not be read as a PDF, PNG, JPEG, or WebP file. A damaged or truncated PDF answers this code. Nothing was charged. | Base64-encode the raw file bytes and send them in the document field. GET /v1/models reports the accepted formats. |
| 400 | `document_password_required` | The PDF needs a password to open. A PDF that carries permissions-only encryption, the kind that opens without being asked for a password, is read normally. Nothing was charged. | Send a copy that opens without a password. |
| 400 | `too_many_pages` | The document has more pages than the model's per-request ceiling. Nothing was charged. | Split the PDF into shorter documents and send each as its own request. GET /v1/models reports the ceiling as max_pages. |
| 400 | `schema_invalid` | The extraction schema is outside the dialect. The problem body carries two extra fields: path, the offending field in dot form (empty for the schema as a whole), and reason, one stable code from the schema reasons list. Nothing was charged. | Fix the field at path as the reason says and retry. The Document Extraction reference lists every reason and the dialect it checks against. |
| 400 | `audio_invalid` | The audio field could not be read as an MP3, WAV, FLAC, Ogg, M4A, AAC, or WebM recording, or its length could not be read from the container. A damaged or truncated file answers this code. Nothing was charged. | Base64-encode the raw file bytes and send them in the audio field. Re-export a file that fails, since a container without a readable length cannot be billed. GET /v1/models reports the accepted formats. |
| 400 | `audio_too_long` | The recording is longer than the model's per-request ceiling. Nothing was charged. | Split the recording at a pause and send each part as its own request. GET /v1/models reports the ceiling as max_duration_seconds. |
| 400 | `diarization_audio_too_long` | The recording reaches the length at which speaker attribution stops, which is well below the transcription ceiling. Nothing was charged. | Send the recording without diarization, or split it at a pause and diarize each part. GET /v1/models reports the ceiling as diarization_limit_seconds. |
| 401 | `unauthorized` | The request carried no API key, or the key is invalid or disabled. | Send a key from the dashboard as 'Authorization: Bearer pxa_...' or in the x-api-key header. |
| 402 | `insufficient_credits` | The account does not have enough credits for this request. Nothing was charged. | Top up or upgrade in the dashboard, then retry. |
| 403 | `key_limit` | This API key reached its spending cap. Nothing was charged. | Raise or remove the cap in the dashboard, or send the request with another key. |
| 403 | `voice_slots_full` | The account holds as many designed voices as its plan allows. Nothing was saved. | Delete a voice with DELETE /v1/voices/designed/{id}, or upgrade the plan for more slots. GET /v1/voices/designed reports the slots in use and the plan's limit. |
| 404 | `not_found` | No endpoint exists at the requested method and path. | Check the path against the API reference at https://paxalabs.com/docs. |
| 413 | `document_too_large` | The decoded document exceeds the model's size ceiling. Nothing was charged. | Compress the document or split it, then retry. GET /v1/models reports the ceiling in bytes as max_bytes. |
| 413 | `audio_too_large` | The decoded recording exceeds the model's size ceiling. Nothing was charged. | Re-encode at a lower bitrate, as Opus or MP3, or split the recording, then retry. GET /v1/models reports the ceiling in bytes as max_bytes. |
| 409 | `idempotency_in_flight` | Another request with this Idempotency-Key is in flight right now. | Wait for it to finish, then retry to replay its outcome. |
| 409 | `idempotency_refunded` | The original request under this Idempotency-Key failed and was refunded. | Send the request again with a new Idempotency-Key. |
| 422 | `idempotency_mismatch` | This Idempotency-Key was already used for a different request. | Reuse a key only to retry an identical request. New content needs a new key. |
| 400 | `unsupported_language` | The source field names a language the model does not accept as an explicit pin. | GET /v1/models lists each translation model's accepted source codes. Send "auto" to translate from any language with automatic detection. |
| 400 | `unsupported_format` | The model does not serve this format, or this format at the requested sample rate. | Paxa TTS Flash v2 serves pcm and wav at every listed sample rate, and mulaw and alaw at 8000 Hz. Paxa TTS Flash serves pcm and wav at 24000 Hz only, and no mulaw or alaw. mp3 and opus take only their own rate on both. Omit sample_rate to take the format's own rate. |
| 422 | `content_blocked` | The upstream safety system declined to process this content. The charge was refunded. | Revise the input. Retrying the same content fails again. If the block looks wrong, contact support with the x-request-id. |
| 422 | `unspeakable_text` | The text contains nothing the model can voice: only tags, emoji, punctuation, or a script it does not speak. A text of tags alone is refused before the charge; any other is charged and refunded. | Send text with Thai, English, or Mandarin Chinese words in it. Retrying the same text fails again. |
| 400 | `live_protocol` | A frame on a live WebSocket violated the session protocol. The frame arrived before start, repeated start, or followed end. | Open the session with one start frame, send the session's frames while it is active, and finish with end. The connection closes with code 4400 after this error. |
| 429 | `rate_limited` | Requests per minute for the plan are exhausted. One window covers the whole account, across every product and every key. | Honor the retry-after header, or upgrade the plan for a higher limit. |
| 429 | `concurrency_limited` | The account holds the plan's full count of concurrent requests for this product. Nothing was charged. Each product is limited separately, and an open live connection holds one slot of its product for its lifetime. | Wait for one of the account's in-flight requests on this product to finish, then retry. There is no retry-after header because the wait depends on your own requests. Upgrade the plan for more concurrency. |
| 429 | `feedback_limit` | The account reached a daily feedback cap: 50 written reports, or 1,000 reports that carry only a rating, in the last 24 hours. Nothing was stored. | Send the report later, once older reports leave the 24-hour window. Before sending one again, GET /v1/feedback with request_id shows whether it already arrived. |
| 500 | `internal` | Request state was inconsistent on the server. | Retry with a new Idempotency-Key. If it persists, POST /v1/feedback with the x-request-id. |
| 502 | `provider_error` | Model inference failed after the request was charged. | A fresh request is refunded automatically; a replayed Idempotency-Key keeps its original charge and is never recharged. Retry with a new Idempotency-Key. If it persists, POST /v1/feedback with the x-request-id. |
| 503 | `provider_unavailable` | The model behind this endpoint is not available right now. Nothing was charged. | Retry later; GET /health reports per-model availability. |
