AgeOnce Docs
API Reference

Token Exchange

Exchange authorization code for age token

Token Exchange

Exchange authorization code for age token.

Endpoint

POST https://api.ageonce.com/api/oauth/token

Request

Headers

Content-Type: application/json
Authorization: Basic <base64(client_id:client_secret)>

Client credentials are sent via HTTP Basic Authentication: encode client_id:client_secret in Base64 and set the Authorization header to Basic <encoded_string>.

Body

{
  "grant_type": "authorization_code",
  "code": "authorization_code_from_callback",
  "redirect_uri": "https://example.com/callback",
  "state": "same_state_you_sent"
}

Parameters

ParameterTypeRequiredDescription
(Header) AuthorizationstringYesBasic + Base64 of client_id:client_secret
grant_typestringYesMust be authorization_code
codestringYesAuthorization code from callback
redirect_uristringYesSame redirect_uri used during authorization
statestringRequired if you sent itSame state from the verification URL. If a state was stored with the code, a mismatch returns invalid_state.

Response

Success (200)

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 600,
  "transaction_id": "550e8400-e29b-41d4-a716-446655440000"
}
FieldTypeDescription
access_tokenstringJWT token with age information
token_typestringAlways "Bearer"
expires_innumberToken lifetime in seconds
transaction_idstringAudit ID for this verification (same value as verification_id in the JWT). Included when an approved audit log exists. Search it in Dashboard Audit Logs.

Audit: Save transaction_id when you grant access (e.g. after token exchange). Later you can look up the verification in the Dashboard to see when and how it was completed.

Error (400/401)

{
  "error": "expired_code",
  "error_description": "Authorization code has expired. Please start a new verification."
}
ErrorDescription
invalid_requestMissing required parameters
invalid_clientInvalid client_id or client_secret
invalid_grantCode is invalid
expired_codeCode has expired (60 seconds)
code_already_usedCode was already exchanged
invalid_redirect_uriredirect_uri is not allowed for this client
invalid_statestate does not match the value stored with the code
unsupported_grant_typegrant_type is not authorization_code
age_requirement_not_metVerified age is below age_required
client_suspendedClient is not active
payment_requiredPlan verification limit reached

Examples

# Encode client_id:client_secret in Base64
AUTH=$(echo -n "cl_abc123:cs_secret456" | base64)

curl -X POST https://api.ageonce.com/api/oauth/token \
  -H "Content-Type: application/json" \
  -H "Authorization: Basic $AUTH" \
  -d '{
    "grant_type": "authorization_code",
    "code": "auth_code_xyz",
    "redirect_uri": "https://example.com/callback",
    "state": "your_state_value"
  }'
const credentials = Buffer.from(
  `${process.env.AGEONCE_CLIENT_ID}:${process.env.AGEONCE_CLIENT_SECRET}`
).toString('base64');

const response = await fetch('https://api.ageonce.com/api/oauth/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Basic ${credentials}`,
  },
  body: JSON.stringify({
    grant_type: 'authorization_code',
    code: authorizationCode,
    redirect_uri: process.env.AGEONCE_REDIRECT_URI,
    state: stateFromCallback,
  }),
});

if (!response.ok) {
  const error = await response.json();
  throw new Error(error.error_description);
}

const { access_token, expires_in, transaction_id } = await response.json();
import os
import base64
import requests

credentials = base64.b64encode(
    f"{os.environ['AGEONCE_CLIENT_ID']}:{os.environ['AGEONCE_CLIENT_SECRET']}".encode()
).decode()

response = requests.post(
    'https://api.ageonce.com/api/oauth/token',
    headers={
        'Content-Type': 'application/json',
        'Authorization': f'Basic {credentials}',
    },
    json={
        'grant_type': 'authorization_code',
        'code': authorization_code,
        'redirect_uri': os.environ['AGEONCE_REDIRECT_URI'],
        'state': state_from_callback,
    }
)

if response.status_code != 200:
    error = response.json()
    raise Exception(error.get('error_description'))

data = response.json()
access_token = data['access_token']
transaction_id = data.get('transaction_id')  # Audit ID for compliance
$credentials = base64_encode(
    getenv('AGEONCE_CLIENT_ID') . ':' . getenv('AGEONCE_CLIENT_SECRET')
);

$response = file_get_contents('https://api.ageonce.com/api/oauth/token', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' => "Content-Type: application/json\r\nAuthorization: Basic " . $credentials,
        'content' => json_encode([
            'grant_type' => 'authorization_code',
            'code' => $authorizationCode,
            'redirect_uri' => getenv('AGEONCE_REDIRECT_URI'),
            'state' => $stateFromCallback,
        ]),
    ],
]));

$data = json_decode($response, true);

if (isset($data['error'])) {
    throw new Exception($data['error_description']);
}

$accessToken = $data['access_token'];
$transactionId = $data['transaction_id'] ?? null;  // Audit ID for compliance

Age Token Structure

JWT token contains the following claims:

{
  "iss": "https://ageonce.io",
  "aud": "ap_live_your_client_id",
  "sub": "user-id",
  "age_verified": true,
  "age_over": 18,
  "verification_level": "first_verification",
  "verification_id": "550e8400-e29b-41d4-a716-446655440000",
  "reverification": false,
  "iat": 1739275200,
  "exp": 1739275800
}
ClaimTypeDescription
issstringIssuer. Default is https://ageonce.io
audstringYour client ID
substringAgeOnce user id for this verification
age_verifiedbooleanWhether age is verified
age_overnumberAge threshold verified (for example 16, 18, or 21)
verification_levelstringfirst_verification or re_verification
verification_idstringTransaction ID — same as transaction_id from token exchange. Use for audit trail and Dashboard Audit Logs.
reverificationbooleanWhether this token is from a repeat check
iatnumberUnix timestamp of creation
expnumberUnix timestamp of expiration (iat + 600)

Token is signed with RS256 algorithm. Public key is available via JWKS endpoint.

Limitations

  • Authorization code can be used only once
  • Code is valid for 1 minute from creation
  • redirect_uri must exactly match the one used during authorization

On this page