Token Exchange
Token Exchange
After receiving an authorization code from the authorization flow, exchange it for an access token and refresh token on your server.
POST /api/oauth/token
This endpoint is rate limited to 10 requests per minute.
Authorization Code Grant
Exchange an authorization code for tokens.
Request Body
{
"grant_type": "authorization_code",
"client_id": "sx_oc_...",
"client_secret": "sx_os_...",
"code": "sx_ic_...",
"redirect_uri": "https://example.com/auth/callback",
"code_verifier": null
}| Field | Required | Description |
|---|---|---|
grant_type | Yes | Must be authorization_code |
client_id | Yes | Your client ID |
client_secret | Conditional | Required for confidential clients. Omit for public clients using PKCE. |
code | Yes | The authorization code received in the callback |
redirect_uri | Recommended | Must match the redirect_uri used in the authorize request |
code_verifier | Conditional | Required if code_challenge was provided during authorization (PKCE) |
Response
{
"access_token": "sx_it_...",
"refresh_token": "sx_ir_...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile email"
}| Field | Type | Description |
|---|---|---|
access_token | string | Use this to call the UserInfo endpoint |
refresh_token | string | Use this to obtain new access tokens |
token_type | string | Always Bearer |
expires_in | integer | Access token lifetime in seconds (3600 = 1 hour) |
scope | string | Space-separated list of granted scopes |
Example: Server-Side Token Exchange (Node.js)
const response = await fetch('https://api.selorax.io/api/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
client_id: process.env.SELORAX_CLIENT_ID,
client_secret: process.env.SELORAX_CLIENT_SECRET,
code: req.query.code,
redirect_uri: 'https://example.com/auth/callback',
}),
});
const tokens = await response.json();
// Store tokens.access_token and tokens.refresh_token securely:::danger Never expose client_secret to the browser
The token exchange must happen on your server. Never include client_secret in client-side JavaScript. For browser-only apps, use PKCE with a public client instead.
:::
Refresh Tokens
Access tokens expire after 1 hour. Use the refresh token to obtain a new token pair without re-prompting the user.
Request Body
{
"grant_type": "refresh_token",
"client_id": "sx_oc_...",
"client_secret": "sx_os_...",
"refresh_token": "sx_ir_..."
}| Field | Required | Description |
|---|---|---|
grant_type | Yes | Must be refresh_token |
client_id | Yes | Your client ID |
client_secret | Conditional | Required for confidential clients |
refresh_token | Yes | The refresh token from a previous token response |
Response
{
"access_token": "sx_it_...",
"refresh_token": "sx_ir_...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile email"
}:::warning Token rotation When you refresh, the old access token and old refresh token are both revoked. A new pair is issued. Store the new tokens immediately. :::
Refresh Token Lifetime
Refresh tokens are valid for 30 days. After that, the user must go through the full authorization flow again.
:::tip Implement proactive refresh — when the access token has less than 5 minutes remaining, refresh it before making API calls. This avoids failed requests. :::
Error Codes
| Error | HTTP Status | Description |
|---|---|---|
invalid_request | 400 | Missing required fields |
invalid_client | 400/401 | Client ID not found or secret is wrong |
invalid_grant | 400 | Code is expired, already used, redirect_uri mismatch, or PKCE verification failed |
unsupported_grant_type | 400 | Only authorization_code and refresh_token are supported |
Example Error Response
{
"message": "Invalid or expired authorization code.",
"error": "invalid_grant",
"status": 400
}