public-logo

36Blocks Docs

Welcome to 36Blocks
Getting Started
Widgets
Sessions & Tokens
Companies & Roles
Security & Access
Webhooks
OAuth 2.0 Module
API Reference
Built with DocStar
  1. /
  2. OAuth 2.0 Module
  3. /
  4. Token endpoint

Token endpoint

This is the one endpoint used for both getting an access token and refreshing it later. It's called directly by the connecting app's own backend, never from a browser, and always returns plain JSON (no envelope wrapper).

POST /oauth/token
#

Exchange a code for a token (grant_type=authorization_code)

Called once, right after the consent redirect lands on the connecting app's redirect_uri with a code in the query string.

Field

Required

Notes

grant_type

yes

literal authorization_code

code

yes

from the consent redirect

redirect_uri

yes

must exactly match the one used when the user was sent to the consent screen

client_id

yes

 

client_secret

confidential clients

 

code_verifier

public clients, or any confidential client with PKCE enabled

the original value from Consent screen

curl -X POST https://your-domain.example/api/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "<code from the redirect>",
    "redirect_uri": "https://theirapp.example.com/callback",
    "client_id": "aZ9k...",
    "client_secret": "<confidential client secret>"
  }'
{...}{
  "access_token": "<opaque bearer token>",
  "token_type": "Bearer",
  "expires_in": 86400,
  "refresh_token": "<opaque, long-lived>",
  "scope": "profile email tickets:read"
}

The code is single-use. Presenting an already-used code a second time doesn't just fail. It revokes the whole connection, since it's treated as a sign the code may have leaked. The connecting app would need the user to go through consent again from the start.

#

Refresh an access token (grant_type=refresh_token)

Field

Required

Notes

grant_type

yes

literal refresh_token

refresh_token

yes

from a previous call to this same endpoint

scope

no

if sent, must be a subset of the already-granted scopes; omit to keep the full set

curl -X POST https://your-domain.example/api/oauth/token \
  -H "Content-Type: application/json" \
  -d '{"grant_type": "refresh_token", "refresh_token": "<refresh token>"}'

Response shape is identical to the code exchange above: a brand-new access_token and a brand-new refresh_token every time. Refresh tokens rotate on every use: the one just used stops working the instant the new one is issued, and presenting an already-rotated-out token again, like a replayed code, revokes the whole connection rather than just failing quietly. A refresh token itself never expires on a timer; it only stops working if the connection is revoked (by the user, by you disabling the integration, or by either of the leak-detection cases above).

#

Errors

Always this shape, with an appropriate 4xx status:

{ "error": "invalid_grant", "error_description": "..." }

error

When

invalid_grant

The most common one. Covers an unknown/expired/already-used code, a replayed refresh token, a redirect_uri/client_id mismatch, a wrong client_secret, or a failed PKCE check. Deliberately generic: a secret mismatch and a PKCE mismatch return the identical error, so neither response reveals which check actually failed.

invalid_scope

A refresh request asked for a scope outside what was already granted.

unsupported_grant_type

grant_type wasn't authorization_code or refresh_token.

 

Was this helpful?
Built with DocStar
On this page
Exchange a code for a token (grant_type=authorization_code)
Refresh an access token (grant_type=refresh_token)
Errors
Was this helpful?