Guides Integration
Exchange Code for Tokens
The authorization code is single-use and short-lived. Exchange it from your server for an ID token proving authentication and an access token you use to fetch the age result from /userinfo
POST
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 unsupported_grant_type.
| 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();$ch = curl_init( 'https://app.agewallet.io/user/token' );
curl_setopt_array( $ch, array(
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query( 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'],
) ),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
) );
$body = curl_exec( $ch );
$status = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
curl_close( $ch );
if ( $status !== 200 ) {
// Handle the failure; do not retry with the same code.
}
$tokens = json_decode( $body, 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_client is returned as HTTP 401). 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 redirect_uri or code_verifier is missing. |
unsupported_grant_type | grant_type is not authorization_code — including when the body is sent as JSON rather than form-encoded, which makes all parameters unreadable. |