Refreshing and rotation
Access tokens last an hour. Refresh tokens last sixty days and rotate on every use, which means the token you just spent is dead and the response carries its replacement.
POST /api/public/oauth/token
grant_type=refresh_token
refresh_token=hvrt_current
The response is a full token pair. Persist the new refresh token before you do anything else with the response; losing it means re-authorizing the host.
Why rotation
A stolen refresh token is a sixty-day credential. Rotation turns theft into something detectable: if both you and an attacker hold the same token, one of you spends it, and the other's attempt is a signal that would not exist otherwise.
Concurrency, and the grace window
A fleet of workers will eventually refresh the same token twice at once. Haven handles this rather than punishing it.
For sixty seconds after a token rotates, a repeat presentation of the old token succeeds and returns a fresh pair. Both callers end up with working credentials. Beyond that window the same presentation is treated as theft.
Haven cannot re-issue the first caller's exact tokens to the second, because tokens are stored hashed and the plaintext is gone the moment the response is written. Minting a second pair is what makes the race survivable without keeping plaintext credentials in a database, which is the tradeoff this design will not make.
You should still hold a mutex around refresh in your own process. It is cheaper than the extra token pair, and it is what the grace window exists to forgive rather than to replace.
Reuse detection
Presenting a rotated refresh token after the grace window has passed revokes every access and refresh token on the grant, suspends the host's authorization, and alerts both your security contact and Haven.
{
"error": "invalid_grant",
"error_description": "This refresh token was already rotated. The authorization has been suspended and the host must re-approve."
}
The message is explicit because your on-call needs to know the token chain forked rather than spending a day hunting a client bug, and because it tells an attacker nothing they do not already have.
The grant is suspended, not revoked. The host sees the application paused with a reconnect prompt, which reads very differently from an application that silently stopped working, and reconnecting restores the same grant rather than creating a second one.
The realistic cause is not theft. It is a client that persisted the old token, or two deployments sharing credentials from one database row and racing past sixty seconds. Both are worth finding.
Narrowing on refresh
You may request a subset of your current scopes:
grant_type=refresh_token
refresh_token=hvrt_current
scope=listings:read
A refresh never widens. The maximum is the scope set frozen on the token you are presenting, re-intersected with the host's grant and your approved ceiling.
One consequence to design around: the refresh token returned alongside a narrowed access token inherits the narrower set. A least-privilege token cannot be refreshed back up to full scope, because a down-scoping that could be undone by a refresh would be decorative. If you want both a wide and a narrow credential, keep two chains.
When a refresh fails
invalid_grant on a refresh means the host's authorization is no longer usable: revoked, suspended, or the person who granted it lost the seat they granted it from. It does not mean your client is broken.
The remedy is to send the host through authorization again. Treat it as a normal lifecycle event, surface it in your own UI as "reconnect Haven", and do not retry: no amount of retrying will make a revoked grant work, and the attempts count against your limit.