Requests and responses
Every endpoint under /api/public/v1 shares the conventions on this page.
Base URL
https://www.bookwithhaven.com/api/public/v1
Required headers
Authorization: Bearer hvat_...
User-Agent: YourProduct/1.2.0 (you@example.com)
The User-Agent must name your product and carry a contact address. When an integration is responsible for a traffic spike or a wave of errors, this is how Haven reaches you before taking a blunter action.
It is checked, not merely logged. Haven records a product token against your application at approval — the name you gave on your application form, e.g. YourProduct — and once one is recorded, a request whose User-Agent does not contain it is refused with 400. Send the same User-Agent from every process in your fleet.
Writes additionally require Idempotency-Key and Content-Type: application/json.
Headers you must not send
X-Haven-Client-Id and X-Haven-Workspace-Context are rejected with 400 on this surface, and error.data.rejectedHeaders names the ones you sent. Both are first-party headers, and both assert something about identity that a partner token has no standing to assert. Which account your token acts inside is a property of the grant, decided when the host approved you, and it cannot be switched at request time.
Haven strips both at the edge before any handler runs, so the 400 is a courtesy rather than the control — but it is a 400 precisely so that an integration expecting to switch accounts finds out on its first call rather than quietly reading the wrong account's data forever. If you manage several hosts, you hold several grants and several tokens, one per host. See How it fits together.
X-Request-Id is accepted and ignored. Haven mints its own request id; see X-Haven-Request-Id below.
The response envelope
Every response, success or failure, has the same three keys.
{ "success": true, "value": {}, "error": null }
{
"success": false,
"value": null,
"error": {
"detail": "not_found",
"message": "No reservation with that code.",
"requestId": "0f7e4c1a-2b8d-4a3f-9c11-6e5b0d7a2f34"
}
}
One shape means one decoder. success is authoritative; do not infer it from the presence of value, because a successful call can legitimately return null.
The OAuth endpoints are the exception. /oauth/token, /oauth/revoke and /oauth/introspect return the RFC shape so that standard OAuth libraries can parse them. The boundary is exact: everything under /api/public/v1 uses the envelope, everything under /api/public/oauth does not.
Response headers
| Header | Meaning |
|---|---|
X-Haven-Request-Id | Identifies this request in Haven's logs. Log it. |
X-Haven-Api-Version | The major version serving the request |
X-Haven-Route | The endpoint that served it, e.g. properties.list |
X-Response-Time | Haven's own processing time for the request |
X-RateLimit-Limit-* | Your ceiling for each active window |
X-RateLimit-Remaining-* | What is left |
X-RateLimit-Reset-* | When each window resets, epoch seconds |
Retry-After | On a 429 only |
X-Haven-Request-Id is the same string as error.requestId in a failure body, the same string in Haven's application logs, and the same string tagged in Haven's error tracking. It is a UUID Haven mints; if you send an X-Request-Id of your own it is not used for this. Quote it in any support request and the answer arrives materially faster.
A 500 may additionally carry X-Correlation-Id, which comes from Haven's outermost error handler and is a different string. Quote both if you have both; X-Haven-Request-Id is the one that is always present.
Status codes
| Code | Meaning |
|---|---|
| 200 | Success |
| 400 | Malformed request, or a header you may not send |
| 401 | Credential missing, expired or revoked |
| 403 | Valid credential, insufficient permission |
| 404 | Not found, or not yours |
| 409 | Conflict, including a source-of-truth lock |
| 426 | Not used on this surface |
| 429 | Rate limited |
| 500 | Haven's fault |
404 rather than 403 for an object outside your grant is deliberate. Distinguishing "does not exist" from "exists but is not yours" would let a caller enumerate other accounts' objects.
This API is not callable from a browser
There is no CORS on /api/public/v1. No Access-Control-Allow-Origin is returned and a preflight OPTIONS is refused, so a browser cannot call these endpoints from a page — deliberately, because it means a leaked access token cannot be spent from a victim's browser at all.
Call Haven from your own server. If your product is a single-page app, proxy through your backend, which is where your refresh token has to live anyway.
Requests are strict
Unknown fields in a request body are an error, not silently dropped. A typo in an optional field name would otherwise look like a successful call that did nothing, and you would find out much later.
Writes
Every write is POST /api/public/v1/operations/{operation}, with the operation's input as a JSON body. Reads are conventional REST. The asymmetry is explained in How it fits together.
Source-of-truth locks
A write into a domain owned by an external system is refused:
{
"success": false,
"value": null,
"error": {
"detail": "conflict",
"message": "Availability for this listing is managed by the host's channel manager.",
"requestId": "0f7e4c1a-2b8d-…",
"data": {
"reason": "domain_locked",
"lockedDomain": "AVAILABILITY",
"pmsOwner": "hospitable",
"propertyCode": "a1b2c3d4"
}
}
}
Refusing is the point. Accepting the write would let the next inbound sync overwrite it, and the host would report that your integration silently stopped working. Read lockedDomains on the listing first.