Step 3: Exchange the code for a token
Overview
From your server, exchange the one-time code from Step 2 for an access token and a refresh token that act as the signed-in user. The access token lasts 24 hours and the refresh token 30 days.
Use the same endpoint later with grant_type=refresh_token to get a new access token without asking the user to sign in again.
Prerequisites
- The
codefrom Step 2: Receive the code, used within 10 minutes. - Your
client_idandclient_secretfrom the dashboard's API Keys page. Keep the secret on your server. - The same
redirect_uriyou sent in Step 1.
Base URL
| Environment | URL |
|---|---|
| Production | https://production-api.shoutaboutus.com |
| Development | https://development-api.shoutaboutus.com |
Endpoint
POST /oauth/token
This path is at the server root, not under /api/v1.
Authentication
Public (no token)
- No bearer token. Your app authenticates with
client_idandclient_secretin the request body. - A wrong or unknown client returns
401 invalid_client.
Rate limit
60 requests per minute, counted per client IP address.
Requests to all rate-limited endpoints count toward the same per-IP total, and this endpoint rejects a request once that total reaches 60 within the window.
| Header | Sent | Meaning |
|---|---|---|
X-RateLimit-Limit | Every response | The limit for this endpoint (60). |
X-RateLimit-Remaining | Every response | Requests left in the current window. |
Retry-After | 429 only | Seconds until you can retry. |
X-RateLimit-Reset | 429 only | Unix timestamp when the window resets. |
Over the limit, the API returns 429 with {"message": "Too Many Attempts."}.
Request parameters
None.
Request body
Send the body form-encoded (Content-Type: application/x-www-form-urlencoded).
Exchanging a code:
| Field | Type | Required | Notes |
|---|---|---|---|
grant_type | string | Yes | authorization_code. |
client_id | string | Yes | Your client ID. |
client_secret | string | Yes | Your client secret. |
redirect_uri | string | Yes | Exactly the value sent in Step 1. |
code | string | Yes | The code from Step 2. It works once. |
code_verifier | string | Conditional | Required only if Step 1 sent a code_challenge. |
Refreshing a token:
| Field | Type | Required | Notes |
|---|---|---|---|
grant_type | string | Yes | refresh_token. |
refresh_token | string | Yes | A refresh token from an earlier response. Each refresh returns a new refresh token; use the new one next time. |
client_id | string | Yes | Your client ID. |
client_secret | string | Yes | Your client secret. |
Request payload
grant_type=authorization_code&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&redirect_uri=https%3A%2F%2Fyourapp.com%2Fcallback&code=<authorization-code>
Response body
The response is not wrapped in data.
| Field | Type | Description |
|---|---|---|
token_type | string | Always Bearer. |
expires_in | integer | Seconds until the access token expires (about 86400, which is 24 hours). |
access_token | string | Send it as Authorization: Bearer <access-token>. |
refresh_token | string | Use it with grant_type=refresh_token. Valid for 30 days. |
Response payload
{
"token_type": "Bearer",
"expires_in": 86400,
"access_token": "<access-token>",
"refresh_token": "<refresh-token>"
}Examples
Success payload
A refresh returns the same shape, with a new access_token and a new refresh_token.
Error payload
400 invalid_grant
The code expired, was already used, or was issued to another client. A refresh fails the same way when the refresh token has expired or been used. The hint says which.
{
"error": "invalid_grant",
"error_description": "The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.",
"hint": "Authorization code has expired"
}400 invalid_request
A required field is missing, or redirect_uri doesn't match Step 1. The hint names the field.
{
"error": "invalid_request",
"error_description": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.",
"hint": "Check the `redirect_uri` parameter"
}400 unsupported_grant_type
{
"error": "unsupported_grant_type",
"error_description": "The authorization grant type is not supported by the authorization server.",
"hint": "Check that all required parameters have been provided"
}401 invalid_client
{
"error": "invalid_client",
"error_description": "Client authentication failed"
}429 Too many requests
{
"message": "Too Many Attempts."
}| Status | Meaning |
|---|---|
200 | Tokens issued. |
400 | invalid_grant, invalid_request or unsupported_grant_type. Read hint for the cause. |
401 | invalid_client: the client ID or secret is wrong. |
429 | Rate limit exceeded. |
500 | Unexpected server error. |
Example request
curl --request POST \
--url "https://production-api.shoutaboutus.com/oauth/token" \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'client_secret=YOUR_CLIENT_SECRET' \
--data-urlencode 'redirect_uri=https://yourapp.com/callback' \
--data-urlencode 'code=<authorization-code>'Updated 19 days ago
