The authorization flow
Haven implements OAuth 2.1 authorization code with PKCE. If you have integrated with a modern OAuth provider this will hold no surprises, and a standard client library will work. The details that differ are collected at the end.
The shape
A host connects your application in five steps.
You send them to /oauth/authorize with your client id, a redirect URI, the scopes you want and a PKCE challenge. Haven authenticates them, shows what you are asking for, and they approve or decline. On approval Haven redirects back to you with a single-use authorization code. You exchange that code, plus the PKCE verifier, for an access token and a refresh token. From then on you call the API with the access token and rotate it with the refresh token.
Discovery
Haven publishes RFC 8414 metadata, so a library can configure itself:
GET https://www.bookwithhaven.com/.well-known/oauth-authorization-server
It also publishes RFC 9728 protected-resource metadata, at the root and at the path-inserted form, which is what lets an MCP client discover the authorization server from nothing but a 401:
GET https://www.bookwithhaven.com/.well-known/oauth-protected-resource
GET https://www.bookwithhaven.com/.well-known/oauth-protected-resource/api/public/v1
Two absences in the metadata are deliberate and worth reading as answers rather than omissions. There is no registration_endpoint, because Haven has no dynamic client registration: every client is created by a person after review. There is no jwks_uri, because tokens are opaque rather than signed.
Endpoints
| Purpose | Endpoint |
|---|---|
| Authorize | GET /oauth/authorize |
| Token | POST /api/public/oauth/token |
| Revoke | POST /api/public/oauth/revoke |
| Introspect | POST /api/public/oauth/introspect |
Tokens are opaque
An access token is hvat_ followed by random bytes. It is not a JWT, carries no claims, and cannot be inspected offline.
The reason is revocation. Haven recomputes what a token may do on every single request, intersecting the token's scopes with the host's current grant and with the ceiling Haven staff set on your application. A host narrowing your access, an agency link being severed, a seat being revoked, or Haven reducing your approved scopes all take effect on your very next call, with no reissue and no propagation delay. A signed token would need a revocation list to achieve the same thing, which is the database lookup this design already performs.
The practical consequence for you: do not cache authorization decisions, and read the scope field on every token response.
What differs from a typical provider
PKCE is mandatory for every client, including confidential ones holding a secret. S256 only. plain is rejected, and code_challenge_method is required rather than defaulted.
state is required. Formally it is optional when PKCE is present. Haven requires it because it is the only CSRF binding your own client has for its session, and every real library sends it.
Authorization codes live sixty seconds and are consumed on first presentation, before the PKCE check runs. A failed exchange cannot be retried; re-run the authorization instead. This bounds a stolen code to a single verifier attempt.
Refresh tokens rotate. Every refresh returns a new refresh token and invalidates the old one, with a sixty-second grace window for concurrent callers. Presenting a rotated token after that window is treated as theft and suspends the grant. See Refreshing and rotation.
redirect_uri is matched by exact string equality. No normalization, no trailing-slash tolerance, no wildcards, no subdomain matching. The single exception is the port on a loopback address.
Errors from the OAuth endpoints use the RFC shape, {"error": "...", "error_description": "..."}, not Haven's envelope. Everything under /api/public/v1 uses the envelope. The boundary is exactly where a generic OAuth library stops reading and your own code starts.
Sequence
Your app Host's browser Haven
| | |
|-- authorize URL ------------>| |
| |-- GET /oauth/authorize->|
| |<-- consent screen ------|
| |-- approve ------------->|
|<-- 302 ?code=&state=&iss= ---| |
| |
|-- POST /token (code + verifier + client auth) -------->|
|<-- access_token, refresh_token, scope ----------------|
| |
|-- GET /api/public/v1/... (Bearer access_token) ------->|
|<-- {success, value, error} ---------------------------|
Check state against what you sent before you use the code. Check iss equals https://www.bookwithhaven.com if your library supports RFC 9207; it is how you detect a mix-up attack when your client talks to more than one provider.