Skip to content

Exchange a code, refresh token or device code for tokens

POST
/oauth/token
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=example

Accepts 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.

object
grant_type
required
string
Allowed values: authorization_code refresh_token urn:ietf:params:oauth:grant-type:device_code
client_id
required
string
code

Authorization_code grant.

string
redirect_uri

Authorization_code grant; must match the authorize request.

string
code_verifier

Authorization_code grant; PKCE verifier.

string
resource

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.

string
refresh_token

Refresh_token grant.

string
scope

Refresh_token grant, optional; must be mcp:card-writer if present.

string
device_code

Device code grant: the device_code from /oauth/device_authorization.

string

Token response. Sent with Cache-Control: no-store.

Media typeapplication/json
object
access_token
required

Opaque, prefixed mcp_at_.

string
token_type
required
string
Allowed values: Bearer
expires_in
required

Seconds: 28800 normally; 2592000 for fallback clients.

integer
scope
required

mcp:card-writer.

string
resource
required

The resource the token is bound to.

string
refresh_token

Opaque, prefixed mcp_rt_; single-use. Absent for clients without the refresh_token grant and for fallback clients.

string
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.

Media typeapplication/json
object
error
required

OAuth error code.

string
redirect_url

Only on the internal consent endpoints when the request expired.

string
retry_after

Seconds to wait before trying again; only with rate_limited.

integer
Examplegenerated
{
"error": "example",
"redirect_url": "example",
"retry_after": 1
}

temporarily_unavailable — token store failure.

Media typeapplication/json
object
error
required

OAuth error code.

string
redirect_url

Only on the internal consent endpoints when the request expired.

string
retry_after

Seconds to wait before trying again; only with rate_limited.

integer
Examplegenerated
{
"error": "example",
"redirect_url": "example",
"retry_after": 1
}