# Errors and recovery behavior

## Stable public envelope

Operations published in the property OpenAPI contract return `error-v1` for
documented failures:

```json
{
  "schema_version": "error-v1",
  "error": {
    "code": "invalid_request",
    "message": "Request validation failed",
    "details": {}
  }
}
```

Validate it with the public schema at `https://cleanedweb.com/schemas/error-v1.json`. Use
`error.code` for program logic and retain `message` for diagnostics. `details` is
optional and can contain validation issues or canonical location candidates.

## Codes and recovery

| HTTP | Stable code | Meaning | Agent behavior |
| --- | --- | --- | --- |
| 401 | `authentication_required` | Session or API key missing, expired, invalid, or revoked | Stop and restore authentication. Do not retry unchanged. |
| 402 | `insufficient_units` | Workspace balance cannot cover the classified delivery | Stop and request an operator-approved Unit credit. |
| 403 | `access_forbidden` | Account, workspace, feature, or market is not entitled | Stop. Request access or choose an entitled market. |
| 404 | `not_found` | Canonical resource is absent from entitled current data | Treat the identifier as stale or unavailable; do not fabricate a replacement. |
| 409 | `max_units_exceeded` | Classified delivery exceeds the caller's spend ceiling | Narrow the search or request a new ceiling; do not silently raise it. |
| 409 | `idempotency_conflict` | The key was reused for a changed request or delivery | Generate a new key only for a genuinely new request. |
| 422 | `invalid_request` | Payload, range, identifier, or limit is invalid | Correct the request using OpenAPI; do not retry the same body. |
| 422 | `ambiguous_location` | Location text has multiple canonical matches | Present `details.candidates` or ask the user to choose. |
| 422 | `location_not_found` | No canonical location matches the text | Change or remove the location after user confirmation. |
| 429 | `rate_limited` | Shared API-key window is exhausted | Wait for `Retry-After`, then retry with the same idempotency key. |
| 503 | `service_unavailable` | Serving readiness or property service cannot answer safely | Retry with bounded exponential backoff and jitter; preserve the request unchanged. |

Unknown error codes must be treated as non-success. Preserve the response for
diagnostics and avoid destructive follow-up actions.

## Retry policy

Retry only transient failures such as `503` or a network failure where no response
was received. A reasonable client policy is 1, 2, 4, 8, then 16 seconds with jitter,
followed by a surfaced failure. Do not turn a `503` into an empty result set or an
empty capabilities manifest.

Reads can be retried safely. If a future mutation is added to the public contract,
follow its published idempotency contract rather than assuming retries are safe.

## Current response boundaries

`GET /v1/capabilities` and `GET /v1/properties/locations` are not currently covered
by the OpenAPI `error-v1` guarantee. Their framework errors use a top-level `detail`
field. Check the HTTP status first and parse either documented shape without
converting an error into data.

Webhook receiver retry semantics are separate from API client retries. See
`https://cleanedweb.com/docs/webhooks.md`.
