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

Body

{
  "client_id": "cl_your_client_id",
  "client_secret": "cs_your_client_secret",
  "code": "authorization_code_from_callback",
  "redirect_uri": "https://example.com/callback"
}

Parameters

ParameterTypeRequiredDescription
client_idstringYesYour Client ID
client_secretstringYesYour Client Secret
codestringYesAuthorization code from callback
redirect_uristringYesSame redirect_uri used during authorization

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

curl -X POST https://app.ageonce.com/api/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "cl_abc123",
    "client_secret": "cs_secret456",
    "code": "auth_code_xyz",
    "redirect_uri": "https://example.com/callback"
  }'
const response = await fetch('https://app.ageonce.com/api/oauth/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    client_id: process.env.AGEONCE_CLIENT_ID,
    client_secret: process.env.AGEONCE_CLIENT_SECRET,
    code: authorizationCode,
    redirect_uri: process.env.AGEONCE_REDIRECT_URI,
  }),
});

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 requests

response = requests.post(
    'https://app.ageonce.com/api/oauth/token',
    json={
        'client_id': os.environ['AGEONCE_CLIENT_ID'],
        'client_secret': os.environ['AGEONCE_CLIENT_SECRET'],
        'code': authorization_code,
        'redirect_uri': os.environ['AGEONCE_REDIRECT_URI'],
    }
)

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
$response = file_get_contents('https://app.ageonce.com/api/oauth/token', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' => 'Content-Type: application/json',
        'content' => json_encode([
            'client_id' => getenv('AGEONCE_CLIENT_ID'),
            'client_secret' => getenv('AGEONCE_CLIENT_SECRET'),
            'code' => $authorizationCode,
            'redirect_uri' => getenv('AGEONCE_REDIRECT_URI'),
        ]),
    ],
]));

$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