Skip to main content

Creating an AgeKey

Overview

The Create AgeKey flow allows users to save their age verification results as an AgeKey for future use. This guide walks through implementing this flow step-by-step.

Already familiar with OAuth/OIDC?

This guide uses OAuth 2.0 Pushed Authorization Request (PAR) plus an OpenID Connect authorization request with response_type=none (no ID token is returned). If you already know these flows, you mainly need the AgeKey-specific piece: the authorization_details payload format (see the Create AgeKey PAR API Reference). The rest follows the standard pattern.

Time to implement: ~3 hours


High-level flow overview

Before diving into code, it's important to understand what happens during the Create AgeKey flow from start to finish.

The user's journey

1. User completes age verification A user has just completed age verification by using your existing verification method - maybe they scanned their driver's license, verified their credit card, or completed a facial age estimation. Your system now has proof of their age.

2. Offer to save as AgeKey Instead of making them repeat this verification process every time they need to prove their age, you offer them the option: "Save as AgeKey for quick verification next time?" This is completely optional - the user can decline and continue normally.

3. Your server prepares the data If the user wants to create an AgeKey, your server securely sends the verification details to AgeKey's service through a server-to-server API call. This includes:

  • What type of verification was performed (ID scan, credit card)
  • The age information obtained (date of birth or age threshold)
  • When the verification happened
  • A unique ID for audit purposes

4. AgeKey acknowledges receipt AgeKey receives your verification data and generates a special temporary token (called a request_uri). This token is similar to a claim ticket - it proves you've submitted valid verification data and is needed for the next step. It expires in 90 seconds.

5. Redirect to AgeKey service Your app redirects the user's browser to AgeKey, passing along that temporary token. The user now sees AgeKey's interface for creating their passkey.

6. User creates their passkey The AgeKey service prompts the user to create a passkey (using Touch ID, Face ID, Windows Hello, or similar). This passkey is stored on their device and associated with the age information you verified. The actual creation happens entirely on their device - no sensitive biometric data is uploaded.

7. Return to Your app Once the passkey is created (or if the user decides not to create one), their browser is redirected back to your app. Unlike the Use AgeKey flow, no verification token comes back - the redirect simply confirms whether or not the AgeKey was created.

8. Confirm success Your app checks whether the creation was successful and shows the user an appropriate message: "AgeKey created. You can now use it for quick age verification" or "You can create an AgeKey later from your account settings."

Visual flow diagram

Key technical concepts

Pushed Authorization Request (PAR) This flow uses a special OAuth 2.0 extension called PAR. Instead of putting all the verification details in a URL (which could be visible in browser history), you send them securely from your server to AgeKey's server. You get back a short request_uri token to use instead.

Why PAR?
  • Privacy: Sensitive verification details never appear in URLs or browser history
  • Security: Your client_secret stays on your server, never exposed to browsers
  • Size: Verification data can be large; PAR avoids URL length limits

Server-to-server communication The most sensitive part of this flow (submitting verification data) happens entirely server-to-server. The user's browser is never involved in transmitting verification details.

No ID token response Unlike the Use AgeKey flow, you don't receive an id_token back. The redirect simply confirms whether the user completed the flow. The AgeKey itself is stored on the user's device - you don't receive it or manage it.

Time sensitivity The request_uri expires in 90 seconds. The user needs to create their passkey within this window, or you'll need to generate a new one. This prevents stale verification data from being reused.

What you'll need to build

  1. Verification Data Formatting: Logic to structure your verification results in the format AgeKey expects
  2. PAR Request Handler: Server-side code to submit verification data to AgeKey's API
  3. Authorization URL Builder: Code to construct the URL for redirecting users to AgeKey
  4. Callback Handler: A page that receives users when they return from AgeKey
  5. User Experience Flow: UI to offer AgeKey creation and show confirmation messages

When to offer AgeKey creation

Best Practices
  • After successful verification: Offer immediately when verification is fresh
  • Make it optional: Never force users to create an AgeKey
  • Explain the benefit: "Save time on future verifications"
  • Handle expiration gracefully: If the 90-second window passes, offer to try again
  • Don't offer for failed verifications: Only offer after successful age verification

Now you can implement each piece step by step.


Step 1: Complete age verification

Before creating an AgeKey, the user must complete an age verification by using your existing verification vendor (ID scan, credit card, facial estimation).

After successful verification, you should have:

  • Verification method used (for example, id_doc_scan, credit_card)
  • Age information (date of birth, age in years, or minimum age)
  • Timestamp of verification
  • Unique verification ID (you generate this)

Step 2: Build authorization details

The authorization details is a JSON array containing your verification result. This is what you'll send to AgeKey. For complete details on the authorization details structure, see the Create AgeKey PAR API Reference.

Structure

[
{
"type": "age_verification",
"method": "<verification_method>",
"age": { <age_object> },
"verified_at": "<timestamp>",
"verification_id": "<your_unique_id>",
"attributes": { <optional_method_specific_data> }
}
]

Age object formats

Choose one format based on what your verification provides:

Most common format when you have the user's exact date of birth.

"age": {
"date_of_birth": "2000-01-02"
}

Supported verification methods

MethodWhen to UseAge FormatRequired Attributes
id_doc_scanID document verificationdate_of_birth preferredNone
payment_card_networkCredit/debit card verificationat_least_yearscard_type (required: credit, debit, or unknown)
facial_age_estimationFace scan age estimationat_least_yearsNone
email_age_estimationemail-based age inferenceat_least_yearsNone
bank_accountBank account verificationdate_of_birth preferredNone

Complete examples

[
{
"type": "age_verification",
"method": "id_doc_scan",
"age": {
"date_of_birth": "2000-01-02"
},
"verified_at": "2025-10-07T12:34:56Z",
"verification_id": "80760254-837a-4008-87f2-6071648a4893",
"attributes": {
"face_match_performed": true
}
}
]

Step 3: Generate security token

Generate a random state value for CSRF protection. Store it in your session.

Code Example (JavaScript):

function generateState() {
const array = new Uint8Array(16);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}

const state = generateState();
// Store in session for later verification
sessionStorage.setItem('agekey_create_state', state);

Code Example (Python):

import secrets

state = secrets.token_urlsafe(16)
# Store in session
session['agekey_create_state'] = state

Step 4: Send pushed authorization request (PAR)

Now make a server-to-server POST request to submit your verification data.

Endpoint

POST https://api.agekey.org/v1/oidc/create/par

Headers

Content-Type: application/x-www-form-urlencoded

Form fields

FieldValue
client_idYour AgeKey client ID
client_secretYour AgeKey client secret
scopeopenid
response_typenone
typeage_verification
redirect_uriYour registered redirect URI
stateThe state token you generated
authorization_detailsURL-encoded JSON array from Step 2
const axios = require('axios');
const qs = require('querystring');

async function createPushedAuthorizationRequest(verificationData, state, redirectUri) {
// Build authorization details
const authDetails = buildAuthorizationDetails(verificationData);

// Prepare form data
const formData = {
client_id: process.env.AGEKEY_CLIENT_ID,
client_secret: process.env.AGEKEY_CLIENT_SECRET,
scope: 'openid',
response_type: 'none',
type: 'age_verification',
redirect_uri: redirectUri,
state: state,
authorization_details: JSON.stringify(authDetails)
};

try {
const response = await axios.post(
'https://api.agekey.org/v1/oidc/create/par',
qs.stringify(formData),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);

return response.data;
// Returns: { request_uri: "urn:agekey:request:...", expires_in: 90 }

} catch (error) {
console.error('PAR request failed:', error.response?.data);
throw error;
}
}

Response

On success, you'll receive:

{
"request_uri": "urn:agekey:request:AjcP1Yt7Np0",
"expires_in": 90
}
warning

Important: The request_uri expires in 90 seconds. The user must complete the next step within this time.


Step 5: Build the authorization URL

Now construct the URL to redirect the user to create their AgeKey.

Using an iframe?

If you're embedding the Create AgeKey flow in an iframe, you must include the publickey-credentials-create permission in the allow attribute:

<iframe src="https://api.agekey.org/v1/oidc/create" 
allow="publickey-credentials-create">
</iframe>

Required parameters

ParameterValue
client_idYour AgeKey client ID
scopeopenid
response_typenone
redirect_uriSame URI used in PAR request
request_uriThe request_uri from PAR response

Base URL:

https://api.agekey.org/v1/oidc/create
function buildCreateAgeKeyUrl(requestUri, redirectUri) {
const params = new URLSearchParams({
scope: 'openid',
response_type: 'none',
client_id: process.env.AGEKEY_CLIENT_ID,
redirect_uri: redirectUri,
request_uri: requestUri
});

return `https://api.agekey.org/v1/oidc/create?${params.toString()}`;
}

// Usage
app.post('/api/create-agekey', async (req, res) => {
const verificationData = req.body;
const state = generateState();
const redirectUri = 'https://yourapp.com/agekey/create-callback';

// Store state in session
req.session.agekey_create_state = state;

// Send PAR request
const parResponse = await createPushedAuthorizationRequest(
verificationData,
state,
redirectUri
);

// Build authorization URL
const authUrl = buildCreateAgeKeyUrl(parResponse.request_uri, redirectUri);

// Return URL to frontend for redirect
res.json({ redirect_url: authUrl });
});

Step 6: Redirect User to AgeKey

Redirect the user's browser to the authorization URL you built in Step 5.

Front end Code Example:

async function offerCreateAgeKey(verificationData) {
// Show "Save as AgeKey?" prompt to user
const userWantsToSave = await showSaveAgeKeyPrompt();

if (!userWantsToSave) {
return; // User declined
}

try {
// Call backend to get redirect URL
const response = await fetch('/api/create-agekey', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(verificationData)
});

const data = await response.json();

// Redirect user to AgeKey service
window.location.href = data.redirect_url;

} catch (error) {
console.error('Failed to initiate AgeKey creation:', error);
showErrorMessage('Unable to create AgeKey. Please try again.');
}
}

Step 7: Handle the Callback

After the user creates (or declines to create) their AgeKey, they'll be redirected back to your redirect_uri.

Success response

If the user created an AgeKey:

https://yourapp.com/agekey/create-callback?state=abc123xyz789

No id_token or other data is returned. Success is indicated by the absence of an error parameter.

Declined response

If the user declined to create an AgeKey:

https://yourapp.com/agekey/create-callback?
state=abc123xyz789&
error=access_denied

Code example (server)

app.get('/agekey/create-callback', (req, res) => {
const { state, error } = req.query;

// Verify state
const storedState = req.session.agekey_create_state;
if (state !== storedState) {
console.error('State mismatch - possible CSRF attack');
return res.status(400).send('Invalid state parameter');
}

// Clear state from session
delete req.session.agekey_create_state;

if (error) {
if (error === 'access_denied') {
// User declined to create AgeKey
console.log('User declined to create AgeKey');
return res.redirect('/dashboard?agekey_declined=true');
}

// Other error
console.error('AgeKey creation error:', error);
return res.redirect('/dashboard?agekey_error=true');
}

// Success! AgeKey was created
console.log('AgeKey created successfully');
res.redirect('/dashboard?agekey_created=true');
});

Step 8: Show confirmation to user

After handling the callback, show an appropriate message to the user.

Front end Code Example:

// On your dashboard/confirmation page
function showAgeKeyStatus() {
const params = new URLSearchParams(window.location.search);

if (params.get('agekey_created') === 'true') {
showSuccessMessage(
'AgeKey Created!',
'You can now use your AgeKey for quick age verification in the future.'
);
} else if (params.get('agekey_declined') === 'true') {
// User chose not to create AgeKey - this is fine
console.log('User declined AgeKey creation');
} else if (params.get('agekey_error') === 'true') {
showErrorMessage(
'Unable to create AgeKey',
'You can try again later from your account settings.'
);
}
}

This completes the Create AgeKey flow. Users now have a reusable, privacy-preserving credential they can use for quick age verification across your app.