Exchange a code, refresh token or device code for tokens
const url = 'https://api.harperharbor.com/oauth/token';const options = { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: new URLSearchParams({ grant_type: 'authorization_code', client_id: 'example', code: 'example', redirect_uri: 'example', code_verifier: 'example', resource: 'example', refresh_token: 'example', scope: 'example', device_code: 'example' })};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://api.harperharbor.com/oauth/token \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data grant_type=authorization_code \ --data client_id=example \ --data code=example \ --data redirect_uri=example \ --data code_verifier=example \ --data resource=example \ --data refresh_token=example \ --data scope=example \ --data device_code=exampleAccepts application/x-www-form-urlencoded (or multipart form) fields only — a JSON body is NOT parsed and results in unsupported_grant_type. No client authentication (public clients). authorization_code: code, client_id, redirect_uri (must equal the one used at authorize), code_verifier (PKCE), resource (must equal the one used at authorize); the code is single-use and must be presented with exactly the same client_id, redirect_uri and resource as the authorize request and a code_verifier whose S256 challenge equals the registered code_challenge; any mismatch is invalid_grant. refresh_token: refresh_token, client_id, resource (must match the session), optional scope (must be mcp:card-writer if given). Refresh tokens are single-use and rotated: each successful refresh returns a new access token and a new refresh token; presenting an already-used refresh token revokes the whole session (all its tokens) and returns invalid_grant. urn:ietf:params:oauth:grant-type:device_code (RFC 8628): device_code, client_id (the client that requested the code; any other is invalid_grant); poll no faster than the interval from /oauth/device_authorization. Until the person decides, the answer is authorization_pending (keep polling); polling too soon is slow_down (add 5 seconds to the interval and continue); access_denied means the person denied; expired_token means the code expired, so request a new one. The first poll after approval returns the tokens and uses up the device code; presenting it again is invalid_grant and revokes the tokens it issued. The tokens carry the scopes and resource of the device request. A server that does not offer device sign-in answers this grant with unsupported_grant_type. Access tokens live 8 hours (expires_in 28800); the session/refresh token lives 30 days from the original grant. Clients registered without the refresh_token grant, and server-configured long-access fallback clients, receive no refresh_token (fallback clients get a 30-day access token, expires_in 2592000). Successful responses are sent with Cache-Control: no-store.
Request Bodyrequired
Section titled “Request Bodyrequired”object
Authorization_code grant.
Authorization_code grant; must match the authorize request.
Authorization_code grant; PKCE verifier.
Authorization_code and refresh_token grants; must match the authorized resource. The device code grant does not send it: the token is bound to the resource of the device request.
Refresh_token grant.
Refresh_token grant, optional; must be mcp:card-writer if present.
Device code grant: the device_code from /oauth/device_authorization.
object
Authorization_code grant.
Authorization_code grant; must match the authorize request.
Authorization_code grant; PKCE verifier.
Authorization_code and refresh_token grants; must match the authorized resource. The device code grant does not send it: the token is bound to the resource of the device request.
Refresh_token grant.
Refresh_token grant, optional; must be mcp:card-writer if present.
Device code grant: the device_code from /oauth/device_authorization.
Responses
Section titled “Responses”Token response. Sent with Cache-Control: no-store.
object
Opaque, prefixed mcp_at_.
Seconds: 28800 normally; 2592000 for fallback clients.
mcp:card-writer.
The resource the token is bound to.
Opaque, prefixed mcp_rt_; single-use. Absent for clients without the refresh_token grant and for fallback clients.
Example
{ "token_type": "Bearer"}unsupported_grant_type, invalid_client, client_not_active, invalid_grant; for the device code grant also authorization_pending, slow_down, access_denied, expired_token.
object
OAuth error code.
Only on the internal consent endpoints when the request expired.
Seconds to wait before trying again; only with rate_limited.
Examplegenerated
{ "error": "example", "redirect_url": "example", "retry_after": 1}temporarily_unavailable — token store failure.
object
OAuth error code.
Only on the internal consent endpoints when the request expired.
Seconds to wait before trying again; only with rate_limited.
Examplegenerated
{ "error": "example", "redirect_url": "example", "retry_after": 1}