# Errors

Reading this page once will remove most of the reasons you would otherwise write to support.

## Shape

```json
{
  "success": false,
  "value": null,
  "error": {
    "detail": "insufficient_scope",
    "message": "This access token cannot perform \"create_price_override\": missing scope rates:write.",
    "requestId": "0f7e4c1a-2b8d-4a3f-9c11-6e5b0d7a2f34",
    "data": {
      "missingScope": "rates:write",
      "grantedScopes": ["account:read", "listings:read"],
      "operation": "create_price_override",
      "docsUrl": "https://www.bookwithhaven.com/developers/02-scopes/02-scope-reference",
      "reauthorizeUrl": "https://www.bookwithhaven.com/oauth/authorize?response_type=code&client_id=hvci_…"
    }
  }
}
```

Branch on `detail`. It is a stable, machine-readable string and is part of the versioned contract. `message` is written for a human reading a log and may be reworded at any time; do not parse it or match on it.

`data` carries per-code structured context. Its presence and shape depend on `detail`, and the codes that carry a documented `data` payload are listed below.

## Codes

| `detail`                | Status | Meaning                                                         | Retry?                          |
| ----------------------- | ------ | --------------------------------------------------------------- | ------------------------------- |
| `invalid_input`         | 400    | Malformed body, bad parameter, unknown field                    | No, fix the request             |
| `invalid_token`         | 401    | Token missing, expired, or revoked; see `data.reason`           | Depends, see below              |
| `unauthenticated`       | 401    | Not used on this surface; you will get `invalid_token`          | No                              |
| `insufficient_scope`    | 403    | Valid token, missing permission                                 | No, re-authorize with the scope |
| `partner_suspended`     | 403    | Your application is suspended                                   | No, contact Haven               |
| `forbidden`             | 403    | Permitted scope, but the host's role or the resource forbids it | No                              |
| `not_found`             | 404    | No such object, or not inside your grant                        | No                              |
| `conflict`              | 409    | Idempotency-key mismatch, or a source-of-truth lock             | Depends, see below              |
| `rate_limited`          | 429    | Budget exhausted                                                | Yes, after `Retry-After`        |
| `internal_server_error` | 500    | Haven's fault                                                   | Yes, with backoff               |

## The ones worth handling specifically

**`insufficient_scope`** carries `missingScope` (one scope, the first one this call needed and your grant lacks), `grantedScopes`, `docsUrl` and a prebuilt `reauthorizeUrl`. No competitor names the missing scope; using it turns a support question into a redirect. See [Incremental authorization](../01-oauth/06-incremental-authorization.md).

**`invalid_token`** arrives with a `WWW-Authenticate` header naming the reason and pointing at the protected-resource metadata document, and with `data.reason` carrying the same value in the body. The reason is drawn from a closed set, and each one has a different correct response — branch on it rather than refreshing blindly:

| `data.reason`     | What happened                                                      | What to do                                                                          |
| ----------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| `token_expired`   | Normal. Access tokens are short-lived                              | Refresh. This is routine and not an error worth alerting on                         |
| `token_revoked`   | This access token was killed, but the grant may still be live      | Refresh once. If that fails, re-authorize                                           |
| `grant_suspended` | The host's connection is paused                                    | Stop. Prompt the host to reconnect from their Haven account                         |
| `grant_revoked`   | The host disconnected you                                          | Stop, and delete your stored tokens. Send them through the authorization flow again |
| `seat_lost`       | The person who authorized you no longer has access to that account | Stop. Another person on the account must authorize you                              |
| `unknown_token`   | No `Authorization` header, or a credential Haven did not issue     | Fix the request. Never retry this                                                   |

Only `token_expired` and `token_revoked` are worth a refresh. Retrying the other four is how an integration produces a loop that generates 401s forever against an authorization that is genuinely gone.

A bare `WWW-Authenticate: Bearer realm="Haven", resource_metadata="…"` with no `error` parameter means you sent no credential at all. The `resource_metadata` URL is on every challenge, so a client that has been configured with nothing but a base URL can discover the authorization server from one unauthenticated request.

**`conflict`** means two different things and the `data.reason` field separates them. `domain_locked` means an external system owns that data for that listing, which is permanent until the host changes their setup, so do not retry. An idempotency conflict means you reused a key with a different payload, which is a bug in your client.

**`rate_limited`** carries which limit you hit. See [Rate limits](07-rate-limits.md).

## Uncaught failures

A `500` may occasionally arrive in a different shape:

```json
{ "error": "internal_error", "correlationId": "0f7e4c1a-…" }
```

That is Haven's outermost handler catching something the envelope never saw. Your client should tolerate both shapes on a 5xx rather than failing to parse. Report the `correlationId`.

## What to log

Log `X-Haven-Request-Id` on every non-2xx, alongside the operation and the account. It is the single field that lets Haven find your exact request, and a support conversation that starts with one is usually a short conversation.

## Retrying

Retry `429` after `Retry-After`, and `500`, `502`, `503` and `504` with exponential backoff. Add jitter of at least 25 percent; a fleet that retries on a synchronized schedule reconstructs the spike that caused the failure.

Never retry a `400`, `403` or `404`. The request will not become valid.

Retry a `409` only after determining which kind it is.

For writes, retry with the **same** `Idempotency-Key`. That is what the key is for, and it is what makes a retry safe against an operation that already partly succeeded.
