# Supply API error codes

Controller-handled attempts receive a fresh server-issued `X-Request-Id`; an incoming header cannot select or suppress an audit record. Save the returned ID for support. Use `Idempotency-Key`, not the request ID, for logical write retries. Authenticated controller-handled validation, readiness, and conflict errors are audited after any mutation transaction rolls back; failed attempts do not record rolled-back field changes. An audit persistence failure fails closed rather than returning an apparently successful audited result.

For endpoint-specific plausible HTTP code/status pairs in the released Phase 1 catalog, see the [endpoint error matrix](endpoint-error-matrix.md). The branch-local, uncertified feed/ack routes have a separate `reservation-preview-error-matrix.md`, available only in the checkout or to platform operators until publication is approved. The global registry below also includes preview and operator-only codes that do not apply to every partner route.

The [machine-readable error registry](error-registry.json) separates HTTP problem `codes`, asynchronous `operation_codes`, and the additional `operator_codes` category. It lists permitted HTTP statuses for problems, the public pointer roots used by field errors, and the resource surfaces that report each operation status. Some HTTP codes arise only in the branch-local reservation preview or internal Workbench flow; their presence in `codes` does not make them partner endpoint responses. The Supply request specs validate emitted `application/problem+json` responses against the HTTP catalog. A source-level contract check also compares every literal Supply error code/status pair with this registry; the Rack-generated `429 rate_limited` is exercised by its own middleware spec. Forwarding helpers are inventoried, but this static check does not prove every endpoint-specific error path or dynamically assembled field pointer. Keep this page and the registry in sync when adding or changing an error.

Supply application errors use `application/problem+json`. Treat `code` as the stable machine-readable value. `title` is the HTTP status title and `detail` explains this occurrence. `request_id` is a correlation ID for support and, for authenticated controller-handled requests, the request audit ledger. An early throttle or invalid credential may have no partner-attributed ledger row. The registry's `retry_contract` makes the transport rule machine-readable: `retryable` is true for application responses with HTTP 429 or 5xx; it is false for the documented validation, authorization, and concurrency responses. Only `rate_limited` requires a numeric-seconds `Retry-After` header.

Every released endpoint may return `401 invalid_api_key`, `403 connection_suspended` or `insufficient_scope`, and `429 rate_limited`. A body on any endpoint, including GET, can trigger `413 payload_too_large` or `415 unsupported_media_type` through shared request checks; POST and PATCH can additionally return `400 invalid_json`. Do not send a body on GET. Calendar readback can also return `413 expanded_update_limit_exceeded`; an unapproved calendar read returns `409 property_not_approved`. These statuses are declared on each corresponding OpenAPI operation so generated clients can handle them consistently.

Selected revision and cursor conflicts include `context` with exact integer recovery values. This object is present only for the codes listed under `problem_contexts` in the registry. Do not parse localized `detail` or `errors[].message` to find a revision or sequence. A `null` `predecessor_sequence` means the property has no preceding event; it does not grant permission to skip the reported conflict.

```json
{
  "type": "https://outreserve.com/developers/supply/errors/stale_revision",
  "title": "Precondition Failed",
  "status": 412,
  "code": "stale_revision",
  "detail": "The calendar has changed since the submitted revision.",
  "request_id": "37f2386c-f746-4ff8-9c5b-0b2b62bfd8bb",
  "retryable": false,
  "context": {"current_revision": 8, "submitted_revision": 7}
}
```

```json
{
  "type": "https://outreserve.com/developers/supply/errors/inventory_exceeds_capacity",
  "title": "Unprocessable Content",
  "status": 422,
  "code": "inventory_exceeds_capacity",
  "detail": "Expected an integer from 0 to 6.",
  "request_id": "37f2386c-f746-4ff8-9c5b-0b2b62bfd8bb",
  "retryable": false,
  "errors": [{
    "code": "inventory_exceeds_capacity",
    "pointer": "/updates/0/available_count",
    "message": "Expected an integer from 0 to 6.",
    "constraints": { "minimum": 0, "maximum": 6 }
  }]
}
```

The `type` URL identifies the error documentation. A pointer names a submitted JSON field or query field. It follows RFC 6901: a literal `~` in one field name becomes `~0`, and a literal `/` becomes `~1`; for example, the unexpected key `vendor~/field` is reported as `/vendor~0~1field`. The same code can have different HTTP statuses when a different operation creates the failure; use both `status` and `code`. Exact human text may change and is localized; do not parse it for recovery.

## Auth, ownership, and lifecycle

| HTTP | `code` | Meaning and action |
| --- | --- | --- |
| 401 | `invalid_api_key` | Missing, malformed, unknown, or revoked Supply Bearer credential. Replace it; the response does not disclose which condition applied. |
| 403, 409 | `connection_suspended` | The partner or connection is not enabled for this operation (403), or Workbench attempted credential issuance on an unavailable connection (409). A sales-suspended connection can still use `GET /connection` and reservation pull/ack with suitable scopes, but content/ARI access remains closed; a revoked connection or suspended partner cannot use either surface. Resolve the account state with OutReserve. |
| 403 | `insufficient_scope` | The credential lacks the operation's approved scope: `supply:read`, `supply:write`, `supply:reservations:read`, or `supply:reservations:ack`. Use a least-privilege credential with that scope. |
| 403 | `approval_required` | An adopted property is awaiting verified OutReserve approval; content and media cannot be changed yet. |
| 403, 409 | `ownership_conflict` | The connection does not own the required property domain, or another connection has claimed the exclusive owner slot. Resolve the mapping with OutReserve. |
| 404 | `resource_not_found` | The resource is missing or invisible to this connection. Check its UUID and source account. |
| 409 | `property_not_approved` | Calendar read/write requires OutReserve approval. Check the property's `approval_status`. |
| 409 | `property_already_connected` | This connection already has a mapping for the property. Use that mapping. |
| 409 | `external_id_conflict` | The partner's property/unit external ID is already mapped within its permitted scope. Reconcile identifiers. |

## Content, units, and media

| HTTP | `code` | Meaning and action |
| --- | --- | --- |
| 409 | `currency_immutable` | Property currency cannot change after creation. Request a reviewed correction. |
| 409 | `timezone_immutable` | Property IANA time zone cannot change after creation. Request a reviewed correction. |
| 409 | `immutable_field` | Attempt to change unit identity/type/mode or an imported media source/owner. Create or associate the correct resource. |
| 409, 422 | `invalid_status_transition` | A requested lifecycle transition is unavailable. Partners may deactivate their property, units, and media; activation requires OutReserve review. |
| 409 | `inventory_exceeds_capacity` | Reducing a pool's capacity would invalidate a reported count for today or a future property-local date. Lower those counts first; past dates do not block a reduction. |
| 409 | `reservation_guest_capacity_conflict` | Lowering or clearing `max_occupants` would invalidate an open future booking, or one has an unknown guest count. Keep the existing capacity and reconcile the booking with OutReserve; the error points to `/unit/max_occupants`. |
| 409 | `reservation_pool_capacity_conflict` | A pooled Unit capacity cut would leave more overlapping open OutReserve stays than places. Reconcile those stays before retrying; a low reported ARI count alone does not release existing commitments. The error points to `/unit/capacity`. |
| 409 | `reservation_pool_capacity_review_limit` | More than 10,000 open future bookings exceed the synchronous capacity-review bound. The Unit was not changed; contact OutReserve for a supervised review instead of repeatedly retrying. The error points to `/unit/capacity`. |
| 503 | `reservation_capacity_review_busy` | A booking row is being changed during a capacity review. The change was not accepted; retry the same PATCH after a short delay. This is a retryable concurrency error, not evidence that a booking was cancelled or inventory changed. |
| 409 | `reservation_unit_occupancy_unavailable` | An active reservation unit cannot have a missing or zero `max_occupants`. Supply a positive integer, or deactivate the unit before clearing it. This code is also used by the operator activation review. |
| 422 | `required_field_missing` | A required property, unit, or ARI `updates` field is absent. Supply the field named by the JSON pointer. |
| 422 | `unknown_field` | A field is outside the endpoint contract. Remove it or read the documented schema. |
| 422 | `unsupported_currency` | Property currency is not supported by OutReserve. Select one advertised by the connection. |
| 404, 422 | `location_not_found` | An exact country, state, or destination identifier in `GET /locations` has no match (404), or property content names an unmapped location (422). Correct it using the discovered hierarchy; an empty `q` search result is a normal 200 response instead. |
| 409 | `ambiguous_location` | More than one taxonomy record shares an exact public country code, state code, or destination slug in its parent scope. Do not guess; ask OutReserve to repair the mapping. Property creation also rejects ambiguous mappings. |
| 422 | `invalid_location_filter` | A location `level` or filter has invalid syntax, type, or hierarchy. Correct the field at `errors[].pointer`. |
| 422 | `invalid_pagination` | Location `page` must be 1–1000 and `per_page` 1–100, as decimal query strings. |
| 422 | `invalid_amenity_filter` | Amenity catalog `q` must be a nonblank string of at most 100 characters. Correct the query parameter. |
| 422 | `invalid_amenity_ids` | `property.amenity_ids` must be an array of at most 200 positive JSON integer IDs; inspect the array or indexed pointer. |
| 422 | `duplicate_amenity` | The same amenity ID appears twice in one effective-set replacement. Send each ID once. |
| 422 | `unknown_amenity` | An ID is absent from the canonical amenity catalog. Re-read `GET /amenities`; no association changes were applied. |
| 422 | `amenity_not_applicable` | The ID is not a selectable property amenity (for example, a unit amenity or category). Use a writable property catalog ID. |
| 422 | `amenity_meta_required` | The amenity requires structured per-property values, which the minimal Supply v1 amenity field cannot carry. Ask OutReserve for a reviewed mapping instead of creating a value-less link. |
| 422 | `invalid_timezone` | The required property time zone is not a supported IANA identifier. Correct the time zone before staging the property. |
| 422 | `invalid_external_property_id` | External property ID is blank or longer than 255 characters. Correct and retry. Property creation points to `/property/external_property_id`; existing-property association points to `/connection_request/external_property_id`. |
| 422 | `invalid_external_unit_id` | External unit ID is blank or longer than 255 characters. Correct and retry. |
| 422 | `invalid_rate` | A default rate is not an integer from 1 through 100,000,000 in the property's currency minor units. Correct and retry. |
| 422 | `unsupported_unit_type` | Unit type is not one of the exact OutReserve `supported_unit_types`. |
| 422 | `unsupported_inventory_mode` | Inventory mode is neither `physical` nor `pooled`. |
| 422 | `field_not_applicable` | A field does not apply to this unit type or scope; RV fields apply only to `RV`, and inventory counts require unit scope. |
| 422 | `invalid_capacity` | Physical capacity must be 1; pooled capacity must be an integer from 1 through 10,000. |
| 422 | `invalid_field_type` | A content, unit, or media field has the wrong JSON primitive or exceeds its field bound. Correct the value at `errors[].pointer`; arrays and objects are not silently omitted from scalar fields, and JSON strings are not accepted for booleans or numeric fields. |
| 422 | `invalid_media_url` | Image source is not a public HTTPS URL within the length limit. Correct the URL. |
| 422 | `validation_failed` | Resource model validation or general ARI validation failed. Inspect `errors[]` and correct each pointer. |

Media import can fail after the initial HTTP `202`. Poll `GET /operations/{id}` or `GET /properties/{property_id}/media/{media_id}`. These are resource-status codes, **not** HTTP problem codes:

| `error_code` | Surfaces | Meaning and action |
| --- | --- | --- |
| `media_deactivated` | operation | A queued import was cancelled by media deactivation. Keep the media closed; create a new import only if still needed. |
| `media_enqueue_failed` | media, operation | Six spaced queue-publication attempts could not complete the import. Report the operation UUID to OutReserve; do not create duplicates blindly. |
| `media_import_failed` | media, operation | The download or format check failed. Correct the public image source and create a new logical import. |

OutReserve's scheduled recovery checks at most 100 stale operations every five minutes and exposes exhausted imports as failed rather than leaving them pending indefinitely.

## ARI and request handling

| HTTP | `code` | Meaning and action |
| --- | --- | --- |
| 400 | `invalid_json` | The declared JSON body cannot be parsed. Send syntactically valid UTF-8 JSON and retry with a new logical request key. |
| 400 | `invalid_idempotency_key` | An onboarding create/association/submission or ARI write omitted the key or supplied something outside 8–128 letters, digits, or `._:-`. Give one key to each logical write. |
| 404, 422 | `unknown_unit` | A readback unit filter is unknown (404), or a unit in a write is foreign/deleted (422). Reconcile the UUID. |
| 409 | `idempotency_key_reused` | This key previously identified a different request body or path. Retry the original unchanged request, or choose a new key for a new write. |
| 412 | `stale_revision` | `If-Match` does not equal the current `calendar_revision`. `context.current_revision` and `context.submitted_revision` are integers. Read back, recompute absolute desired state, then send a new key and revision. |
| 413 | `payload_too_large` | The actual JSON request body exceeds 1 MiB, even when Content-Length is absent or inaccurate. Split the content or calendar batch into smaller requests. |
| 413, 422 | `expanded_update_limit_exceeded` | A readback exceeds 1,000 unit-days (413), or a write exceeds 1,000 scope-days (422). Split the window or filter to one unit. |
| 415 | `unsupported_media_type` | A body-bearing Supply request was not sent as `application/json`. Correct the content type; do not send a body on GET. |
| 422 | `currency_mismatch` | Optional ARI currency assertion disagrees with the approved property currency. Correct it. |
| 422 | `duplicate_update` | More than one batch entry addresses the same target/date. Merge those entries. |
| 422 | `invalid_date_range` | Calendar date is malformed, reversed, or outside the write horizon; readback range can also exceed its limit. Correct the range. |
| 422 | `inventory_exceeds_capacity` | Supplied unit count is not an integer within physical `0..1` or pooled `0..capacity`. Correct the count. |
| 422 | `field_not_applicable` | A property-scoped update tried to assign `available_count` or `incorporated_reservation_sequence`; use unit scope for inventory snapshots. |
| 422 | `reservation_watermark_required` | A unit `available_count` omitted `incorporated_reservation_sequence`. Send both fields in the same entry; `0` is valid before any reservation event is incorporated. |
| 422 | `reservation_watermark_requires_inventory` | A unit entry supplied `incorporated_reservation_sequence` without `available_count`. Include the absolute count for those same dates; a rate-only write cannot advance incorporation. |
| 422 | `invalid_reservation_watermark` | The watermark is not a nonnegative JSON integer at or below 9,007,199,254,740,991. Do not send a string, null, fraction, negative value, or number that JavaScript cannot represent exactly. |
| 409 | `reservation_watermark_ahead` | The claimed sequence exceeds this property's applied reservation cursor. `context.applied_reservation_sequence` and `context.submitted_sequence` identify the ceiling and claim; connection-wide sequence values can skip unrelated properties. Durably apply and acknowledge that property's preceding events before reporting their incorporation. |
| 409 | `reservation_watermark_regression` | An expanded unit/date row already has a higher incorporated sequence. `context.current_incorporated_sequence` and `context.submitted_sequence` identify the conflict. Reconcile the count and send a nondecreasing watermark with a new idempotency key. |
| 422 | `validation_failed` | Empty/malformed updates, invalid booleans/integers, or inconsistent stay limits. Dated `rate_minor` is 1–100,000,000; `min_stay` and `max_stay` are 1–365. Inspect each field pointer and `constraints` object. |
| 428 | `precondition_required` | ARI write omitted a correctly quoted numeric `If-Match` revision. Read the calendar and resend with its current revision. |
| 429 | `rate_limited` | Wait for integer-seconds `Retry-After` and apply jittered backoff. Supply Bearer credentials have a default 600 requests/minute quota; an `apikey` query parameter cannot change that quota identity. A separate Supply IP limit is 1,200/minute. Missing or syntactically malformed Bearer credentials have a tighter 60/minute IP limit; well-formed but unknown tokens are subject to the IP and per-token limits because authentication occurs after throttling. The response also includes `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `X-Request-Id` matching the problem body's `request_id`, including early middleware rejections. |

`invalid_field` appears as a **field-level** code within `validation_failed.errors[]` for a model validation. An unknown field may be the top-level `unknown_field` code with the pointer echoed inside `errors[]`.

JSON request bodies are capped at 1 MiB. A `413` is independent of the 1,000 expanded scope-day ARI limit; both constraints must be satisfied.

## Reservation feed preview (not certified or booking-ready)

These branch-local feed/ack and internal publication codes are not yet part of the released OpenAPI/Postman contract. The checkout-local/operator-only `reservation-preview-error-matrix.md` separates actual poll and ack responses from internal publication and Workbench errors, with exact pointer and recovery guidance. The outlet remains non-bookable until [reservation release gates](reservations.md) pass. Publication failures arise inside OutReserve booking transitions, not as responses to a partner's feed/ack call. For partner calls, `errors[].pointer` names the invalid query or `/ack` body field.

| HTTP | `code` | Meaning and action |
| --- | --- | --- |
| 403 | `insufficient_scope` | Use a credential with `supply:reservations:read` for polling or `supply:reservations:ack` for acknowledgement on an approved or sales-suspended connection. Generic content scopes do not grant reservation access. |
| 404 | `resource_not_found` | Event UUID is absent or not owned by this connection. Do not probe another account. |
| 409 | `reservation_connection_unavailable` | Booking publication has no approved availability-owner connection. Keep inventory non-bookable and reconcile the mapping. |
| 409 | `reservation_booking_unpersisted` | Internal publication was attempted before a booking exists; this is an OutReserve integration fault, not a partner retry. |
| 409 | `reservation_revision_conflict` | A create was already published, or an amendment/cancellation has no valid predecessor. Reconcile lifecycle state. |
| 409 | `reservation_revision_key_reused` | A lifecycle key is being reused for different unit/dates or guest identity. Keep the original event and publish a new amendment key for a genuine change. |
| 409 | `reservation_allocation_invalid` | Unit or date coverage cannot be mapped to inventory; keep sales closed and reconcile. |
| 422 | `reservation_guest_count_invalid` | A Supply booking or amendment has no explicit positive integer total-person count, or the count exceeds the selected unit's guest capacity. Correct the booking input or reviewed amendment before publication. |
| 409 | `reservation_pricing_lock_missing` | OutReserve has no persisted booking pricing lock for the revision. This is an internal publication failure; keep sales closed and escalate, not a partner retry. |
| 409 | `reservation_pricing_lock_mismatch` | Booking currency or total no longer agrees with its frozen pricing lock. Stop publication and reconcile the booking. |
| 409 | `reservation_nightly_rates_missing` | The locked per-date rates do not exactly cover the revision's inventory dates, or a rate is missing, negative, nonnumeric, or not representable in exact currency minor units. Reconcile the pricing snapshot before publication. |
| 409 | `reservation_policy_snapshot_missing` | The frozen booking cancellation-policy fields are incomplete. Reconcile the booking policy before publication. |
| 409 | `reservation_cursor_ahead` | `after_sequence` is greater than `context.latest_published_sequence`; `context.submitted_sequence` is the rejected position. Reconcile the persisted read position before polling again; a read cursor is not an application acknowledgement. |
| 409 | `reservation_cursor_expired` | The requested replay page includes an intentionally erased guest payload, or `after_sequence` skips past an erased event without that event's supervised checkpoint. Stop automatic polling and ask OutReserve for one-event reconciliation. Do not invent a later cursor or infer PMS application. |
| 409 | `reservation_stream_gap` | The requested page crosses missing committed reservation history. Stop polling and contact OutReserve; `context.expected_sequence` is the first missing event and `context.latest_published_sequence` is the sampled portfolio head. Never advance the read cursor or infer that the missing event was applied. |
| 409 | `reservation_predecessor_unacknowledged` | The ack skips an earlier event for this property. `context.last_applied_property_sequence`, `context.predecessor_sequence`, and `context.submitted_sequence` identify the gap; the predecessor may be `null` only for inconsistent state requiring OutReserve support. Durably apply and acknowledge that property's predecessor first; unrelated properties may be applied independently. |
| 409 | `reservation_partner_identity_mismatch` | `/ack/partner_reservation_id` differs from the preceding applied revision's PMS reservation ID. Reconcile that booking in the PMS before applying and acknowledging this revision. The applied cursor does not advance. |
| 409 | `acknowledgement_conflict` | A retry changed the partner reservation ID/revision or durable outcome. Reconcile; do not assign a second PMS reservation. |
| 409 | `reservation_event_rejected` | The event already has a semantic rejection. The affected property is suspended; OutReserve must verify partner remediation and requeue the same event before an applied ack can succeed. Requeue does not reopen sales. |
| 409 | `reservation_payload_unavailable` | Protected guest details cannot be decrypted or have expired. Stop replay and request supervised reconciliation. |
| 422 | `invalid_reservation_cursor` | `after_sequence` must be a nonnegative decimal integer; `limit` must be 1–100. |
| 422 | `invalid_ack_sequence` | `/ack/sequence` must be a JSON integer equal to the path event's sequence. |
| 422 | `invalid_ack_result` | `/ack/result` must be `applied` or `rejected`. |
| 422 | `invalid_partner_reservation_id` | Applied ack needs a nonblank partner ID of at most 255 bytes. |
| 422 | `invalid_partner_revision` | Optional partner revision must be a string of at most 255 bytes. |
| 422 | `invalid_rejection_code` | Rejected ack needs a lowercase snake-case code of at most 64 characters. |
| 422 | `invalid_rejection_detail` | Optional rejection detail must be a string of at most 500 bytes. Do not include guest/payment data. |

## Workbench-only validation

These codes occur when OutReserve administrators approve, issue credentials, or manage a reservation outlet; they are included here so operational logs can be interpreted, but are not outcomes of ordinary partner ARI writes. The activation path is branch-local and not a public partner endpoint.

| HTTP | `code` | Meaning |
| --- | --- | --- |
| 422 | `approval_evidence_required` | Review method/reference missing for a property approval. |
| 409 | `property_migration_required` | Existing property needs a reviewed ownership or migration decision. |
| 422 | `invalid_scope` | Requested credential scope is unsupported. |
| 422 | `invalid_rotation_grace` | Credential overlap period is outside the allowed range. |
| 422 | `invalid_credential_expiry` | Requested credential expiration is outside the allowed range. |
| 422 | `rejection_reason_required` | Property rejection lacks a recorded reviewer reason. |
| 409 | `reservation_activation_disabled` | The deployment has not enabled operator reservation cutover; approval and ARI uploads remain non-bookable. |
| 409 | `reservation_activation_state` | The property link is not in the required approved, non-bookable state for activation, or is not active for suspension. |
| 422 | `unit_publication_confirmation_required` | Workbench must receive explicit operator approval before clearing a newly staged unit's stop sell and publishing it. This is not a partner HTTP error. |
| 422 | `reservation_certification_incomplete` | One of the required operator evidence references—mapping, delivery, pricing, privacy, reconciliation, or general reference—is missing or is not a bounded, non-personal case identifier. |
| 422 | `reservation_horizon_invalid` | Activation horizon must be a JSON integer from 1 through 730 days. |
| 422 | `reservation_horizon_conflict` | The certified horizon is shorter than the property's configured minimum advance-booking window. Extend the certified ARI coverage or review the property rule. |
| 409 | `reservation_pricing_not_managed` | The property is not configured for Supply-owned pricing. Reconcile ownership and pricing configuration before activating bookings. |
| 409 | `reservation_feed_lag` | The connection has unacknowledged reservation events; apply and acknowledge the backlog before activation. This value may also appear as an ARI readback blocking reason rather than an HTTP error. |
| 409 | `reservation_mapping_incomplete` | No eligible staged or resumable unit with a stable Supply mapping is available. |
| 409 | `reservation_sequence_exhausted` | Internal publication cannot allocate another exactly representable JSON sequence; an operator must migrate the stream before booking resumes. This is not a partner request error. |
| 409 | `reservation_activation_too_large` | The requested cutover exceeds one million expanded unit-days; split the cutover with OutReserve review. |
| 409 | `reservation_inventory_incomplete` | At least one candidate unit/date lacks a valid, fresh count and incorporation watermark over the proposed horizon. |
| 409 | `reservation_rate_incomplete` | At least one candidate unit/date lacks a positive applicable rate over the proposed horizon. |
| 422 | `reservation_suspension_reason_required` | An operator suspension needs a bounded, non-personal incident or case reference; free-text reasons and contact details are not accepted. |

## Retry rules and transport failures

An exact onboarding retry keeps the same HTTP method, path, raw body bytes, and `Idempotency-Key`. This includes property/unit/media creation, existing-property association, and review submission. After current credential and property-access checks, an exact retry returns the original HTTP status and JSON body with `Idempotency-Replayed: true`; a revoked credential, suspended connection, or revoked property link can instead return `401`, `403`, or `404`. A changed method/path/body with that key returns `409 idempotency_key_reused`. For media, poll the resource after replay because asynchronous import status may have changed. Each retry has its own request audit row linked to the canonical write. The original response body is stored as authenticated ciphertext, not exposed in the redacted request/audit listing.

An exact ARI retry additionally keeps the same `If-Match` and returns the original applied revision. A timeout leaves the outcome unknown; retry the exact request first, then read back. A `412` requires readback and a **new** logical request/key. For `429`, honor `Retry-After`. Other documented 4xx responses require correcting or reconciling before retrying. The application does not promise a JSON problem envelope for DNS/TLS failures, connection resets, or an upstream proxy error; inspect the content type before decoding and preserve the idempotency key while the outcome is uncertain.
