SeloraXDEVELOPERS

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
}
FieldRequiredDescription
grant_typeYesMust be authorization_code
client_idYesYour client ID
client_secretConditionalRequired for confidential clients. Omit for public clients using PKCE.
codeYesThe authorization code received in the callback
redirect_uriRecommendedMust match the redirect_uri used in the authorize request
code_verifierConditionalRequired 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"
}
FieldTypeDescription
access_tokenstringUse this to call the UserInfo endpoint
refresh_tokenstringUse this to obtain new access tokens
token_typestringAlways Bearer
expires_inintegerAccess token lifetime in seconds (3600 = 1 hour)
scopestringSpace-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_..."
}
FieldRequiredDescription
grant_typeYesMust be refresh_token
client_idYesYour client ID
client_secretConditionalRequired for confidential clients
refresh_tokenYesThe 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

ErrorHTTP StatusDescription
invalid_request400Missing required fields
invalid_client400/401Client ID not found or secret is wrong
invalid_grant400Code is expired, already used, redirect_uri mismatch, or PKCE verification failed
unsupported_grant_type400Only authorization_code and refresh_token are supported

Example Error Response

{
  "message": "Invalid or expired authorization code.",
  "error": "invalid_grant",
  "status": 400
}