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/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 | Required if you sent it | Same 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"
}| Field | Type | Description |
|---|---|---|
access_token | string | JWT token with age information |
token_type | string | Always "Bearer" |
expires_in | number | Token lifetime in seconds |
transaction_id | string | Audit 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."
}| Error | Description |
|---|---|
invalid_request | Missing required parameters |
invalid_client | Invalid client_id or client_secret |
invalid_grant | Code is invalid |
expired_code | Code has expired (60 seconds) |
code_already_used | Code was already exchanged |
invalid_redirect_uri | redirect_uri is not allowed for this client |
invalid_state | state does not match the value stored with the code |
unsupported_grant_type | grant_type is not authorization_code |
age_requirement_not_met | Verified age is below age_required |
client_suspended | Client is not active |
payment_required | Plan 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 complianceAge 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
}| Claim | Type | Description |
|---|---|---|
iss | string | Issuer. Default is https://ageonce.io |
aud | string | Your client ID |
sub | string | AgeOnce user id for this verification |
age_verified | boolean | Whether age is verified |
age_over | number | Age threshold verified (for example 16, 18, or 21) |
verification_level | string | first_verification or re_verification |
verification_id | string | Transaction ID — same as transaction_id from token exchange. Use for audit trail and Dashboard Audit Logs. |
reverification | boolean | Whether this token is from a repeat check |
iat | number | Unix timestamp of creation |
exp | number | Unix 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