Public REST errors use a stable machine-readable `error.code` and a human-readable `error.message`. Branch on the code or HTTP status, not on message text.

## Canonical errors

| HTTP | Code | Meaning | Retry unchanged? |
| --- | --- | --- | --- |
| 401 | `unauthorized` | Bearer token is missing or invalid | No |
| 402 | `account_inactive` | An active paid account is required | No |
| 402 | `quota_exhausted` | The account has no request quota remaining | No |
| 404 | `not_found` | The requested job does not exist for this account | No |
| 409 | `idempotency_conflict` | The key was used for different input | No |
| 422 | `credentials_required` | The capability needs stored upstream credentials | No |
| 429 | `rate_limited` | The account or key exceeded its current rate | Yes, after delay |
| 500 | `internal_error` | Upscrape encountered an unexpected failure | Usually, with idempotency |

Capability jobs can also finish with capability- or upstream-specific error codes in the failed job representation.
When `Prefer: wait=N` returns a failed job inline, its HTTP status reflects the known failure class. An unrecognized terminal code returns HTTP `500`; HTTP `200` is reserved for completed work and idempotent replay envelopes.

## Rate limits

HTTP `429` includes a `Retry-After` header in seconds and `error.retry_after_ms` in the JSON body. Wait at least that long and add jitter before retrying.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "rate limit exceeded",
    "retry_after_ms": 1250
  }
}
```

## Safe retry policy

- Retry rate limits after the advertised delay.
- Retry transient transport failures and internal errors only with the original idempotency key.
- Poll an existing pending job instead of resubmitting it.
- Do not retry authentication, billing, credential, validation, or idempotency-conflict failures unchanged.
- Put a total deadline and attempt limit around every retry loop.

## Redaction

Job failure messages are sanitized before they enter the public response. Even so, applications should avoid copying entire upstream responses into their own logs without an additional data-sensitivity review.
