Notes for coding agents
If you are a model building an integration against this API, this page is the short version. It assumes you will read a few pages rather than all of them and tries to name the things that are expensive to get wrong.
If your client speaks MCP, use it
POST https://www.bookwithhaven.com/api/public/mcp
Every write operation as a tool, dispatched through the same code as the REST write — same scopes, same limits, same idempotency ledger, same two-step confirmation on the two tools that move money. See MCP endpoint, and read the idempotency section there before you retry anything.
Reads are still REST. Fetch the listing or the reservation before you change it.
Load the whole contract in one request
GET https://www.bookwithhaven.com/developers/llms.txt
Every page, concatenated, each preceded by the path it came from. Individual pages are also fetchable as raw Markdown under /developer-docs/<path>.md.
The eight things that cause the most rework
A token is bound to one account. Fixed when the host approved you. Not a header, not switchable. An agency with forty clients is forty authorizations.
Read the scope field on every token response. Effective permission is recomputed per request and can be narrower than what you asked for. Do not cache authorization decisions.
items.length < limit does not mean the last page. Rows you cannot see are filtered after the page is drawn. Stop only when nextCursor is null. This is the single most common bug in a first integration.
Stay dates are YYYY-MM-DD and are not instants. new Date('2026-09-14') produces a UTC midnight that formats as the 13th in any timezone behind UTC, which moves the booking a night. Keep the string.
Money is an integer in minor units with a sibling currency code. Never a float. Do not compute totals; use the ones returned.
A granted scope is not sufficient for a write. If the host's channel manager owns that domain for that listing, the write is refused with domain_locked. Read lockedDomains first.
Every write needs an Idempotency-Key, generated once per logical operation and reused across retries. Generating it inside the retry loop defeats the mechanism entirely.
Webhooks are signals, not state. Thin payloads, at-least-once, out of order. Re-read the resource; never reconstruct state from event sequence.
Errors are structured, so branch on them
Switch on error.detail, never on error.message. insufficient_scope carries missingScope and a prebuilt reauthorizeUrl. conflict carries a reason distinguishing a permanent source-of-truth lock from an idempotency mistake.
Retry 429 after Retry-After with jitter, and 5xx with backoff. Never retry 400, 403 or 404.
Ordering
Call /me first, always. It tells you the account, the effective scopes and whether writes will be permitted at all. Re-read it after any authorization change and after any insufficient_scope.
Backfill by walking REST once. Existing data is not replayed as webhooks when a host connects.
Do not
Do not construct reference codes, or assume they are unguessable, or sort by them.
Do not send X-Haven-Client-Id or X-Haven-Workspace-Context. Both are rejected with a 400, and both attempt to assert an identity claim a partner token has no standing to make.
Do not omit User-Agent. It is enforced and must carry a contact address.
Do not poll where a webhook would do, and do not retry without jitter. A synchronized fleet reconstructs the burst that caused the limit.
Scope selection
Ask for the narrowest set that does the job. Narrow requests are approved faster, and Incremental authorization means widening later costs one redirect rather than a new application.
Note in particular that guests:read is separate from reservations:read. If your product does not need the guest's name and email, do not request it; the reservation payload is otherwise identical.