Guides Integration
Exchange Code for Tokens
The authorization code is single-use and short-lived. Exchange it from your server for an ID token carrying the age result.
https://app.agewallet.io/user/tokenParameters are sent in the request body as form fields, with Content-Type: application/x-www-form-urlencoded. Sending JSON returns invalid_request.
| Parameter | Required | Purpose |
|---|---|---|
grant_type | Yes | Always authorization_code. |
code | Yes | The one-time code from the callback. |
redirect_uri | Yes | Your callback URL. Must match the /authorize request exactly. |
client_id | Yes | Your registered client ID. |
client_secret | Confidential clients | Your client secret. Omit for public clients, which rely on PKCE instead. |
code_verifier | Yes | The original, unhashed verifier you generated before /authorize. |
curl https://app.agewallet.io/user/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code \
-d code="$AUTH_CODE" \
-d redirect_uri="https://yourapp.com/callback" \
-d client_id="$AGEWALLET_CLIENT_ID" \
-d client_secret="$AGEWALLET_CLIENT_SECRET" \
-d code_verifier="$CODE_VERIFIER"const response = await fetch('https://app.agewallet.io/user/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: 'https://yourapp.com/callback',
client_id: process.env.AGEWALLET_CLIENT_ID,
client_secret: process.env.AGEWALLET_CLIENT_SECRET,
code_verifier: req.session.agewalletVerifier
})
});
if ( ! response.ok ) {
const error = await response.json();
throw new Error( `Token exchange failed: ${ error.error }` );
}
const tokens = await response.json();$response = wp_remote_post( 'https://app.agewallet.io/user/token', array(
'headers' => array( 'Content-Type' => 'application/x-www-form-urlencoded' ),
'body' => array(
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => 'https://yourapp.com/callback',
'client_id' => AGEWALLET_CLIENT_ID,
'client_secret' => AGEWALLET_CLIENT_SECRET,
'code_verifier' => $_SESSION['agewallet_verifier'],
),
) );
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
// Handle the failure; do not retry with the same code.
}
$tokens = json_decode( wp_remote_retrieve_body( $response ), true );import requests
response = requests.post(
"https://app.agewallet.io/user/token",
data={
"grant_type": "authorization_code",
"code": code,
"redirect_uri": "https://yourapp.com/callback",
"client_id": AGEWALLET_CLIENT_ID,
"client_secret": AGEWALLET_CLIENT_SECRET,
"code_verifier": session["agewallet_verifier"],
},
timeout=10,
)
response.raise_for_status()
tokens = response.json()Success Response
{
"access_token": "eyJ0eXAiOiJKV1Qi...[truncated]",
"id_token": "eyJ0eXAiOiJKV1Qi...[truncated]",
"token_type": "Bearer",
"expires_in": 900
}| Field | What is it for |
|---|---|
id_token | Proves the user authenticated. Validate it, but don’t read the age status from it. |
access_token | Bearer token for /userinfo. This is the call that returns the age result. |
token_type | Always Bearer. |
expires_in | Lifetime of the access token in seconds. 900 is 15 minutes. |
No refresh token is issued. A verification is a one-time event: if you need a fresh result, start a new authorization request.
When the exchange fails
Failures return HTTP 400 with an error field. invalid_grant covers most of them and is worth checking against all four causes before opening a ticket.
| Error | Usual cause |
|---|---|
invalid_grant | The code expired, was already used, the redirect_uri does not match the /authorize request, or the code_verifier does not match the challenge you sent. |
invalid_client | Wrong client_id, or a missing or incorrect client_secret. |
invalid_request | A required parameter is missing, or the body was sent as JSON rather than form-encoded. |
unsupported_grant_type | grant_type is not authorization_code. |
Next: validate the ID token
Receiving an id_token is not the same as trusting it. Before you act on the age result, verify its signature against our JWKS, and check that iss, aud, exp, and the nonce you sent all match.