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/tokenRequest
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
| Parameter | Type | Required | Description |
|---|---|---|---|
(Header) Authorization | string | Yes | Basic + Base64 of client_id:client_secret |
grant_type | string | Yes | Must be authorization_code |
code | string | Yes | Authorization code from callback |
redirect_uri | string | Yes | Same redirect_uri used during authorization |
state | string | Optional | Same state you sent (for validation) |
Response
Success (200)
{
"age_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFnZW9uY2UtcHVibGljLWtleSJ9...",
"token_type": "Bearer",
"expires_in": 600,
"transaction_id": "550e8400-e29b-41d4-a716-446655440000"
}| Field | Type | Description |
|---|---|---|
age_token | string | JWT token with age information |
token_type | string | Always "Bearer" |
expires_in | number | Token lifetime in seconds |
transaction_id | string | Unique 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"
}| Error | Description |
|---|---|
invalid_request | Missing required parameters |
invalid_client | Invalid client_id or client_secret |
invalid_grant | Invalid or expired code |
unauthorized_client | redirect_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 complianceAge 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"
}| Claim | Type | Description |
|---|---|---|
sub | string | Always "anonymous" (privacy) |
age_verified | boolean | Whether age is verified |
min_age | number | Minimum verified age (alias for age_over) |
age_over | number | Age threshold verified (e.g. 16, 18, 21) |
verification_id | string | Transaction ID — same as transaction_id from token exchange. Use for audit trail and Dashboard Audit Logs. |
verified_at | string | ISO 8601 verification date |
client_id | string | Client ID |
iat | number | Unix timestamp of creation |
exp | number | Unix timestamp of expiration |
iss | string | Always "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