AgeOnce Docs
API Reference

Token Exchange

Exchange authorization code for age token

Token Exchange

Exchange authorization code for age token.

Endpoint

POST https://app.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
statestringOptionalSame state you sent (for validation)

Response

Success (200)

{
  "age_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFnZW9uY2UtcHVibGljLWtleSJ9...",
  "token_type": "Bearer",
  "expires_in": 600,
  "transaction_id": "550e8400-e29b-41d4-a716-446655440000"
}
FieldTypeDescription
age_tokenstringJWT token with age information
token_typestringAlways "Bearer"
expires_innumberToken lifetime in seconds
transaction_idstringUnique Audit ID for this verification (same value as verification_id in the JWT). We recommend storing it (e.g. in your order or database) if you need to track when and how access was granted — you can then search by this ID in Dashboard Audit Logs to see verification details.

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": "invalid_grant",
  "error_description": "Authorization code has expired"
}
ErrorDescription
invalid_requestMissing required parameters
invalid_clientInvalid client_id or client_secret
invalid_grantInvalid or expired code
unauthorized_clientredirect_uri does not match

Examples

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

curl -X POST https://app.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://app.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 { age_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://app.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()
age_token = data['age_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://app.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']);
}

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

Age Token Structure

JWT token contains the following claims:

{
  "sub": "anonymous",
  "age_verified": true,
  "min_age": 18,
  "age_over": 18,
  "verification_id": "550e8400-e29b-41d4-a716-446655440000",
  "verified_at": "2026-02-11T12:00:00Z",
  "client_id": "cl_abc123",
  "iat": 1739275200,
  "exp": 1739361600,
  "iss": "ageonce"
}
ClaimTypeDescription
substringAlways "anonymous" (privacy)
age_verifiedbooleanWhether age is verified
min_agenumberMinimum verified age (alias for age_over)
age_overnumberAge threshold verified (e.g. 16, 18, 21)
verification_idstringTransaction ID — same as transaction_id from token exchange. Use for audit trail and Dashboard Audit Logs.
verified_atstringISO 8601 verification date
client_idstringClient ID
iatnumberUnix timestamp of creation
expnumberUnix timestamp of expiration
issstringAlways "ageonce"

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