Integrate age verification in under 10 minutes
Secret keys on your server. Public keys in the browser. Domain allowlisting built in. First signed webhook in under 10 minutes from signup.
Integration pattern: your backend calls
POST https://api.ageverifyeu.com/v1/verification-sessions with a secret key,
receives a verification_url, redirects (or opens a popup) to the hosted UI,
and your webhook endpoint receives a signed verification.completed event
with age_over_threshold: true|false.
Create a free sandbox account to get API keys.
1. Create a session (server-side)
Call the sessions endpoint from your backend with your secret key. Never use a secret key in browser code.
curl -X POST https://api.ageverifyeu.com/v1/verification-sessions \
-H "Authorization: Bearer sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"country_code": "DE",
"language": "de",
"age_threshold": 18
}'
# Response:
# {
# "session_id": "avs_2nLz7X5",
# "verification_url": "https://verify.ageverifyeu.com/session/avs_2nLz7X5",
# "status": "created",
# "expires_at": "2026-06-18T14:00:00Z"
# }const res = await fetch(
"https://api.ageverifyeu.com/v1/verification-sessions",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.AGEVERIFY_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
country_code: "DE",
language: "de",
age_threshold: 18,
}),
}
);
const { session_id, verification_url } = await res.json();
// Redirect user to verification_urlimport httpx, os
resp = httpx.post(
"https://api.ageverifyeu.com/v1/verification-sessions",
headers={
"Authorization": f"Bearer {os.environ['AGEVERIFY_SECRET_KEY']}",
"Content-Type": "application/json",
},
json={
"country_code": "DE",
"language": "de",
"age_threshold": 18,
},
)
data = resp.json()
session_id = data["session_id"]
verification_url = data["verification_url"]
# Redirect user to verification_url2. Launch the hosted UI
Redirect the user to the verification_url, or use the JavaScript SDK to launch in popup, embed, or wallet mode. The SDK validates your domain allowlist on init.
// Simplest integration: full-page redirect // verification_url returned by POST /v1/verification-sessions window.location.href = verification_url;
import { AgeVerify } from "@ageverify/sdk";
// Init once per page (validates domain allowlist)
await AgeVerify.init({ publicKey: "pk_live_YOUR_KEY" });
// Launch popup when user triggers age-gated action
await AgeVerify.start({
sessionId: "avs_2nLz7X5",
mode: "popup",
language: "de",
onComplete: (result) => {
// result.verified — wait for webhook for authoritative result
console.log("Flow complete:", result);
},
});// EU Wallet deep-link mode — opens wallet app directly
await AgeVerify.start({
sessionId: "avs_2nLz7X5",
mode: "wallet",
// Platform detects wallet availability automatically
});3. Receive and verify the webhook
Configure your webhook endpoint in the dashboard. Every event is signed — always verify before processing.
Webhook payload
{
"event_type": "verification.completed",
"session_id": "avs_2nLz7X5",
"status": "verified",
"age_over_threshold": true,
"age_threshold": 18,
"country_code": "DE",
"method": "eudi_wallet",
"assurance_level": "high",
"policy_version": "DE-AGE-2026-001",
"completed_at": "2026-06-18T12:34:56Z"
}Signature verification (Node.js)
import crypto from "crypto";
app.post("/webhook", express.raw({ type: "*/*" }), (req, res) => {
const sig = req.headers["x-ageverify-signature"];
const ts = req.headers["x-ageverify-timestamp"];
// Reject replays older than 5 minutes
if (Date.now() / 1000 - parseInt(ts) > 300) {
return res.status(400).send("Timestamp too old");
}
const expected = crypto
.createHmac("sha256", process.env.WEBHOOK_SIGNING_SECRET)
.update(`${ts}.${req.body.toString()}`)
.digest("hex");
if (!crypto.timingSafeEqual(
Buffer.from(sig), Buffer.from(expected)
)) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body);
if (event.age_over_threshold) {
// Grant access
}
res.sendStatus(200);
});We request only what is strictly necessary
AgeVerify EU requests only boolean age attributes from wallets and eID providers. Identity fields are never requested, transmitted, or stored.
| Data field | Requested by AgeVerify EU | Never requested |
|---|---|---|
age_over_18 / age_over_16 / age_over_21 |
✓ Requested | — |
| Issuer trust metadata (for validation) | ✓ Validated | — |
given_name, family_name |
— | ✗ Never requested |
birth_date, birth_place |
— | ✗ Never requested |
| Document images (passport, ID card scans) | — | ✗ Never requested or stored |
| Address, phone number, email | — | ✗ Never requested |
Test every outcome without real identity documents
Sandbox mode simulates the full verification flow. Use it to test your webhook handling, error paths, and UI states before going live.
test_over_18
Simulates a successful verification where the user is over the configured age threshold. age_over_threshold: true.
test_under_18
Simulates a successful verification where the user is under the threshold. age_over_threshold: false.
test_failed
Simulates a verification failure (e.g., provider error, user abort). Status: failed. Tests your error handling path.
test_expired
Simulates a session that expires before the user completes verification. Status: expired. Tests your timeout handling.
Programmatic sandbox simulation: POST to /v1/provider-callbacks/sandbox with {"session_id": "…", "method": "test_over_18"} to trigger an outcome without opening the hosted UI — useful for automated integration tests and CI pipelines.
Developer FAQ
What API authentication does AgeVerify EU use?
Secret keys (sk_test_… / sk_live_…) are used for server-side API calls via the Authorization: Bearer header. They must never appear in browser code. Public keys (pk_test_… / pk_live_…) are used for JavaScript SDK initialization and are validated against a per-app domain allowlist. Both key types can be rotated in the developer dashboard without downtime.
How do I verify webhook signatures?
Each webhook includes X-AgeVerify-Signature (HMAC-SHA256 hex of timestamp + "." + raw request body, signed with your webhook signing secret) and X-AgeVerify-Timestamp (Unix epoch seconds). Always verify both and reject payloads with timestamps older than 5 minutes to prevent replay attacks. Use crypto.timingSafeEqual (or equivalent) to prevent timing attacks.
Should I use webhooks or poll the session endpoint?
Webhooks are strongly recommended for production. They receive the result immediately after verification without polling delay, and include the HMAC signature for security. Polling GET /v1/verification-sessions/{session_id} is supported for environments where incoming webhooks are not possible. In sandbox, use the programmatic callback endpoint to trigger results instantly.
How do I test without real wallets or identity documents?
Create an app in sandbox mode and generate test API keys (sk_test_…). The hosted verify UI shows sandbox methods (test_over_18, test_under_18, test_failed, test_expired) that simulate each outcome without connecting to real identity providers. For automated testing, POST directly to /v1/provider-callbacks/sandbox with the desired method.
What SDK launch modes are available?
Four modes: redirect — full-page redirect (no SDK needed); popup — centred popup window; embed — iframe within your page; wallet — deep link to the user's EUDI Wallet app. All modes deliver results via the same signed webhook.
What HTTP status codes should I handle?
201 Created — session created. 400 Bad Request — invalid parameters. 401 Unauthorized — missing or invalid API key. 402 Payment Required — monthly quota exceeded. 422 Unprocessable Entity — validation error. 429 Too Many Requests — rate limit exceeded. Non-2xx responses from your webhook endpoint trigger automatic retries with exponential back-off.
Ready to integrate?
Free sandbox account. No credit card. Full API access from day one.
Create free account Try sandbox demo