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 code from Step 2: Receive the code, used within 10 minutes.
  • Your client_id and client_secret from the dashboard's API Keys page. Keep the secret on your server.
  • The same redirect_uri you sent in Step 1.

Base URL

EnvironmentURL
Productionhttps://production-api.shoutaboutus.com
Developmenthttps://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_id and client_secret in 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.

HeaderSentMeaning
X-RateLimit-LimitEvery responseThe limit for this endpoint (60).
X-RateLimit-RemainingEvery responseRequests left in the current window.
Retry-After429 onlySeconds until you can retry.
X-RateLimit-Reset429 onlyUnix 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:

FieldTypeRequiredNotes
grant_typestringYesauthorization_code.
client_idstringYesYour client ID.
client_secretstringYesYour client secret.
redirect_uristringYesExactly the value sent in Step 1.
codestringYesThe code from Step 2. It works once.
code_verifierstringConditionalRequired only if Step 1 sent a code_challenge.

Refreshing a token:

FieldTypeRequiredNotes
grant_typestringYesrefresh_token.
refresh_tokenstringYesA refresh token from an earlier response. Each refresh returns a new refresh token; use the new one next time.
client_idstringYesYour client ID.
client_secretstringYesYour 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.

FieldTypeDescription
token_typestringAlways Bearer.
expires_inintegerSeconds until the access token expires (about 86400, which is 24 hours).
access_tokenstringSend it as Authorization: Bearer <access-token>.
refresh_tokenstringUse 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."
}
StatusMeaning
200Tokens issued.
400invalid_grant, invalid_request or unsupported_grant_type. Read hint for the cause.
401invalid_client: the client ID or secret is wrong.
429Rate limit exceeded.
500Unexpected 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>'

Did this page help you?