Errors
Reading this page once will remove most of the reasons you would otherwise write to support.
Shape
{
"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.
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.
Uncaught failures
A 500 may occasionally arrive in a different shape:
{ "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.