> For AI assistants: read [llms.txt](https://docs.saturation.io/llms.txt) first for the complete documentation map, API contract, and endpoint Markdown files.

# Errors

Every failed request uses the same format. Check `code` to decide what to do.

```json
{
  "success": false,
  "code": "permission_revoked",
  "message": "Your access to update transactions in this workspace was revoked.",
  "requestId": "req_3f9b2a7c",
  "requiredAbility": "update:Transaction"
}
```

| Field | Always present | Meaning |
|---|---|---|
| `success` | yes | Always `false`. |
| `code` | yes | A stable string from the set below. Switch on this. |
| `message` | yes | Human-readable text for logs or UI. |
| `requestId` | yes | The id of the failed request. Include it when reporting the failure. |
| `fieldErrors` | no | On validation failures: a map of field name to its error messages. |
| `requiredAbility` | no | On `permission_revoked` (403): the missing ability as `action:subject`, for example `update:Transaction`. |
| `retryAfter` | no | On `rate_limited` (429): how many seconds to wait. Same value as the `Retry-After` header. |

## The codes

The full set. These match the `error.code` enum in the [OpenAPI reference](/api-reference.md).

### 401 Unauthenticated

| Code | When |
|---|---|
| `unauthenticated` | No credentials resolved. |
| `missing_authorization` | No `Authorization` header. |
| `invalid_token` | Malformed or expired token. |
| `token_revoked` | The token was revoked. |

### 403 Forbidden

| Code | When |
|---|---|
| `permission_revoked` | Your access does not allow this action. Carries `requiredAbility`. |
| `scope_exceeded` | The action is outside the token's `scopes[]`. |
| `forbidden` | The action is not permitted. |
| `feature_not_available` | The feature is not enabled for this workspace or plan. |
| `role_ceiling_exceeded` | The write is above what your role can set. |
| `api_access_disabled` | API access is disabled for the workspace. |

### 402 Payment required

| Code | When |
|---|---|
| `upgrade_required` | The workspace plan does not include this operation. |
| `capacity_exceeded` | The workspace has reached a product limit. |

### 404 Not found

| Code | When |
|---|---|
| `not_found` | The item is missing, or you cannot read it. |
| `document_target_not_found` | The link target does not exist. |

### 400 Bad request

| Code | When |
|---|---|
| `validation` | The request body or parameters failed validation. |
| `cursor_invalid` | A cursor was reused after you changed the filter or sort. Start the list again. |
| `expand_invalid` | An unknown or too-deep `expand` key. The whole request is rejected, never partly applied. |
| `invalid_date_range` | `dateFrom` and `dateTo` do not agree. |
| `webhook_https_required` | A webhook `url` is not HTTPS. |
| `range_too_large` | A requested range is larger than the allowed window. |

### 409 Conflict

| Code | When |
|---|---|
| `account_path_ambiguous` | A budget account `path` matched more than one account. |
| `account_code_ambiguous` | An account number matched more than one budget line. |
| `budget_compute_stale` | A conditional budget write used stale computed data. |
| `approval_required` | The operation requires approval. |
| `idempotency_conflict` | You sent the same `Idempotency-Key` with a different body. |
| `already_linked` | This record is already linked to another target. |
| `status_unreachable_for_source` | The requested status is not allowed for this transaction `source`. |
| `po_invalid_status` | A purchase-order action is not valid for its current status. |
| `webhook_url_invalid_ssrf` | The webhook URL was unsafe and we blocked it at registration. |
| `phase_copy_unsupported` | The requested phase copy is not supported. |

### 423 Locked

| Code | When |
|---|---|
| `resource_locked` | The resource cannot change in its current state. |

### 413 Too large

| Code | When |
|---|---|
| `budget_too_large` | The budget document exceeds the line ceiling. Narrow it with `path`, `accountId`, or `phase`. |
| `bulk_too_large` | A bulk request exceeds the per-call row cap. |

### 422 Unprocessable entity

| Code | When |
|---|---|
| `field_read_only` | You tried to set a field the server owns. |
| `scenario_too_large` | The requested scenario exceeds its copy limit. |
| `source_not_postable` | A transaction `source` cannot be posted directly. |
| `webhook_url_blocked` | The webhook URL was unsafe and we blocked it. |

### 429 Rate limited

| Code | When |
|---|---|
| `rate_limited` | Too many requests. Carries a `Retry-After` header and `retryAfter`. See [Rate Limits](/rate-limits.md). |

### 5xx Server

| Code | Status | When |
|---|---|---|
| `signing_key_not_configured` | 500 | A webhook delivery needs a signing key that is not configured. |
| `internal_error` | 500 | Something failed on our side. Quote `requestId` when you report it. |
| `temporarily_unavailable` | 503 | A required product or provider is temporarily unavailable. |
| `budget_compute_timeout` | 504 | The budget total did not come back in time. Retry. The next call may be cached. |

## Retrying

Fix the request before retrying.

- **Don't retry:** `permission_revoked`, `scope_exceeded`, `token_revoked`, `validation`, `field_read_only`, `not_found`, `cursor_invalid`, `expand_invalid`, `idempotency_conflict`.
- **Safe to retry:** `rate_limited` (wait for `Retry-After`), `budget_compute_timeout`, `internal_error`. When an operation requires `Idempotency-Key`, reuse the same key for its retry.

## Next

- [Idempotency](/idempotency.md): make retried writes safe with an `Idempotency-Key`.
- [Rate Limits](/rate-limits.md): read `Retry-After` and back off on `429`.
