# Errors

Every error response uses the same envelope, with a stable, machine-readable `code` and the `request_id` of the call. The HTTP status tells you the class of problem; `code` tells you exactly what went wrong. The `message` text follows `Accept-Language` (pt-BR, en-US, es-ES, fr-FR, defaulting to en-US); `code` never changes.

```json
{
  "error": {
    "code": "validation_failed",
    "message": "The request has invalid fields.",
    "request_id": "01J8Z3ZQ4H8K2M0T1S9P7YQF5C",
    "details": [
      { "field": "name", "code": "required", "message": "Name is required." }
    ]
  }
}
```

- `code` is the only field your integration should branch on. Handle any code you do not recognize by its HTTP status.
- `details` is always present. It lists the individual problems, typically on `400` and `422`, and is an empty array when there is nothing to itemize. Each item carries `field` (request body field) or `parameter` (query parameter), `code` (the rule that was violated), and `message`. Business rule rejections add `reason` with the specific cause.
- `request_id` matches the `X-Request-Id` response header and the entry in the audit trail (`GET /requests/{request_id}`). Include this value when you contact support.
- A resource outside the scope of the current credentials returns `404`, exactly like a resource that does not exist. The API never confirms the existence of something you cannot see.
- The API fails closed: if the credential state, its permissions, or the rate limiter cannot be verified, the response is `503`, never access.
- A validation error executes nothing and does not consume the `Idempotency-Key`. A `429` response does not count against the rate limit window.

## Codes

| Code | HTTP | When |
|---|---|---|
| `unauthenticated` | 401 | The credential is missing, has an unrecognized format, or does not exist. |
| `credential_revoked` | 401 or 422 | The credential has been revoked. Credential management operations return 422 instead. |
| `credential_suspended` | 401 | The credential has been suspended by Cademí. |
| `credential_expired` | 401 | The credential has expired. |
| `ambiguous_credentials` | 401 | The call uses two authentication modes at once, or sends an ID token instead of an access token. |
| `human_context_required` | 401 | The credential requires human mode, but the call sent only the API key. |
| `invalid_access_token` | 401 | The administrator's access token is invalid, expired, or revoked. |
| `instance_blocked` | 403 | The account is blocked or has been removed. |
| `instance_unavailable` | 403 | The credential's account is not available to the API. |
| `credential_state_unverifiable` | 503 | The credential state could not be verified, so the API fails closed. |
| `permission_denied` | 403 | The credential lacks the permission required by the operation. |
| `not_found` | 404 | The resource does not exist, has been deleted, or is not accessible with the current credentials. |
| `operation_not_catalogued` | 503 | The route is not a recognized public operation, so the request is rejected as a safety measure. |
| `not_acceptable` | 406 | The request's `Accept` header does not allow `application/json`. |
| `unsupported_media_type` | 415 | The `Content-Type` is not `application/json`. |
| `payload_too_large` | 413 | The request body exceeds 1 MiB. |
| `malformed_json` | 400 | The request body is not valid JSON. |
| `json_too_deep` | 400 | The JSON is nested more than 16 levels deep. |
| `invalid_utf8` | 400 | The request body contains bytes that are not valid UTF-8. |
| `validation_failed` | 422 | A required field is missing or a value was rejected. `details` lists each field and rule. |
| `unknown_field` | 422 | The request body contains a field that is not in the schema. |
| `unknown_parameter` | 400 | The query string contains a parameter that is not in the schema. |
| `invalid_cursor` | 400 | The cursor was altered, belongs to a different sort order, or has expired (24 hours). |
| `method_not_allowed` | 405 | The HTTP method is not supported by the route. |
| `revision_mismatch` | 412 | The `If-Match` value does not match the current revision of the resource. |
| `invalid_transition` | 422 | The requested state change is not available for the resource. |
| `credential_not_removable` | 422 | The credential has usage history and can only be revoked. |
| `delegation_limit_exceeded` | 403 | The delegated policy tried to grant more than the issuing credential has. |
| `idempotency_key_required` | 422 | A state-changing `POST` request was sent without an `Idempotency-Key`. |
| `idempotency_key_invalid` | 422 | The `Idempotency-Key` is malformed (it must be 1 to 128 characters). |
| `idempotency_key_reused` | 422 | The same key was already used with a different body or route. |
| `idempotency_in_progress` | 409 | A request with the same key is still being processed. |
| `result_uncertain` | 409 | The first attempt did not reach a recorded result. Retry the call. |
| `secret_not_replayable` | 409 | A replay does not return a secret that is shown only once. |
| `result_not_replayable` | 409 | The recorded result cannot be replayed for this call. |
| `operation_type_unknown` | 422 | The batch `type` is not a supported operation type. |
| `duplicate_item_key` | 422 | Two items in the batch share the same `key`. |
| `operation_backlog_exceeded` | 422 | The account already has too many open operations. |
| `operation_not_cancelable` | 422 | The operation has already finished. |
| `operation_not_resumable` | 422 | The operation has not failed or has no recoverable items. |
| `item_invalid` | 422 | The payload of a batch item does not have the shape its type requires. |
| `too_many_items` | 422 | An array exceeds the maximum size. For operation batches, the limit depends on the operation type (up to 1,000 items). |
| `operation_ownership_lost` | 409 | Another attempt took over the operation, and this attempt was discarded. |
| `domain_rule_violation` | 422 | A business rule was violated that has no dedicated public code. |
| `http_error` | Same as the response | Generic HTTP error. The response status is the status of the failure. |
| `internal_error` | 500 | An unexpected failure occurred. No further detail is provided. |
| `audit_unavailable` | 503 | The audit trail could not be recorded, so the call is not confirmed. |
| `rate_limit_exceeded` | 429 | The per-minute limit for the source, the account, or the credential was reached. |
| `rate_limiter_unavailable` | 503 | The rate limiter did not respond, so the API fails closed. |
| `state_conflict` | 409 | The resource is in a state that does not allow this call. |
| `upload_integrity_mismatch` | 422 | The uploaded bytes do not match the declared size, checksum, or type. |
| `storage_unavailable` | 503 | The file storage service did not respond. |
| `sandbox_only` | 403 | The operation is available only in sandbox accounts. |
| `production_only` | 403 | The operation is available only in production accounts. |
| `replica_readonly` | 403 | The record is a replica copied from another account and is read-only, except for `status`. Other edits, publishing, and deletion are rejected. |
| `order_set_mismatch` | 422 | The reorder request does not contain exactly the set of IDs in scope. |
| `hierarchy_violation` | 422 | The target would break the hierarchy (a parent inside its own child, or an invalid level). |
| `banner_type_unavailable` | 422 | The banner type is not available for the account. |
| `already_exists` | 409 | A resource with the supplied identifier (`external_id`, `slug`) already exists. |
| `plan_limit_reached` | 409 | The account's plan does not allow another resource of this type. |
| `email_recently_sent` | 409 | The same email was sent recently. Wait for the window to pass. |
| `feature_disabled` | 409 | The feature is turned off in the account settings. |
| `tag_in_use` | 409 | The tag is in use and cannot be deleted. |
| `template_not_previewable` | 409 | The template has nothing to preview. |
| `format_unsupported` | 409 | The requested format is not supported by the resource. |
| `human_mode_not_allowed` | 403 | The credential does not allow human mode. |
| `admin_not_eligible` | 403 | The administrator of the access token is not linked to the credential. |
| `tenant_mismatch` | 403 | The administrator of the access token belongs to another account. |
| `too_many_streams` | 429 | The limit of event streams per credential (5) or per account (20) was reached. |
| `stream_busy` | 409 | The event stream already has an open connection. |
| `cursor_expired` | 410 | The cursor points to an event that is no longer retained (30-day retention). |
| `sandbox_required` | 409 | The test scenario runs only in a sandbox account. |
| `unsupported_resource` | 422 | The resource type is not accepted by this operation. |
| `plan_invalid` | 422 | The declarative manifest failed validation. |
| `plan_stale` | 409 | The plan was computed against a state that has since changed. |
| `plan_expired` | 409 | The plan has expired and must be computed again. |

## What to do for each class

| Status | Action |
|---|---|
| `400`, `404`, `405`, `406`, `409`, `412`, `413`, `415`, `422` | Client error. Retrying without changes produces the same result. |
| `401`, `403` | Credential or permissions. Review the credential and its permissions before retrying. |
| `429` | Wait for `retry_after_seconds` (or the `Retry-After` header) and retry. See [Limits, quotas, and retention](https://cademi.dev/api/limits.md). |
| `500`, `503` | Server failure or unavailability. Retry with exponential backoff; for state-changing requests, use the same `Idempotency-Key`. |

If a state-changing request returns `500`, `503`, or times out, the outcome is uncertain: retry with the same `Idempotency-Key`. If the first attempt was recorded, the retry returns the same response with `Idempotent-Replayed: true` and creates nothing new.
