> ## Documentation Index
> Fetch the complete documentation index at: https://developers.teampascal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors and recovery

> Interpret request errors and asynchronous upload failures.

Expected request errors use this envelope. Unexpected server errors return a plain 500 without it:

```json theme={null}
{"error": {"code": "invalid_request", "message": "Validation error", "details": null}}
```

`code` and `message` are strings. `details` is an object, array, or null. All 401 responses carry `WWW-Authenticate: Bearer`.

| HTTP               | Code                  | When                                                                                                                                         |
| ------------------ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| 400                | `invalid_request`     | Malformed body/form or request validation failure                                                                                            |
| 400                | `invalid_record`      | Invalid batch; `details.records` lists `{index, id, reason}`; nothing staged                                                                 |
| 401                | `invalid_client`      | Token exchange authentication failed, including disabled connector                                                                           |
| 401                | `invalid_token`       | Data route bearer token is missing, invalid, expired, or issued before a key rotation                                                        |
| 403                | `connector_disabled`  | Connector disabled on a data route                                                                                                           |
| 403                | `datasource_mismatch` | Requested datasource differs from the token datasource                                                                                       |
| 404                | `upload_not_found`    | Unknown upload id                                                                                                                            |
| 404                | `not_found`           | Unknown path                                                                                                                                 |
| 405                | `method_not_allowed`  | HTTP method is not supported on this path                                                                                                    |
| 409                | `upload_not_open`     | PUT targets a status other than `in_progress` or an upload with `closed_at` set, or a sweep races close; details carry `status` and `closed` |
| 409                | `upload_processing`   | Another upload is processing; honor `Retry-After`                                                                                            |
| 410                | `upload_expired`      | Upload expired, including an upload replaced by a newer open                                                                                 |
| 413                | `batch_too_large`     | Batch exceeds limits; details include `records`, `bytes`, `max_records`, `max_bytes`                                                         |
| 413                | `payload_too_large`   | Body rejected before parsing; `details.max_bytes` gives the limit                                                                            |
| 422 (asynchronous) | `too_many_deletions`  | Full upload deletion brake; appears in the upload's `error` field, while close itself still returns 202                                      |
| 422 (asynchronous) | `validation_failed`   | Records failed schema validation; appears in the upload's `error` field with the report in `report`, while close itself still returns 202    |
| 429                | `rate_limited`        | Token exchange rate exceeded                                                                                                                 |
| 503                | `not_configured`      | Custom Source connector configuration is unavailable                                                                                         |
| 503                | `publish_failed`      | Close could not schedule the processing job; close again after `Retry-After`                                                                 |
| 503                | `service_unavailable` | Any route; transient service failure; `details.retryable` is a boolean                                                                       |

The `service_unavailable` error includes a retry hint in the envelope:

```json theme={null}
{"error": {"code": "service_unavailable", "message": "Transient service failure", "details": {"retryable": true}}}
```

`details.retryable` is a boolean; inspect it before deciding whether to retry.

A successful close means processing was submitted, not that records were accepted. On 503 `publish_failed`, the upload stays `in_progress`; honor `Retry-After: 5` and call close again to retry scheduling. Poll until the upload leaves `in_progress`. A worker failure keeps it `in_progress` while Pascal retries; after at least one hour, the hourly stuck-upload sweep marks it `failed` with `processing_timeout`. A rejected or failed upload carries its error envelope in the upload's `error` field; access the code as `upload["error"]["error"]["code"]`. `processing_timeout` appears only there.

Correct invalid records before resending the whole rejected batch. A PUT timeout can be retried with the same ids. On a data-route 401, re-exchange a token and resume the same upload. On 409 `upload_processing`, wait for `Retry-After` before trying to open again. For expired, rejected, or failed uploads, open a new upload and resend the intended records.
