Custom age gate
This guide walks you through implementing a custom age gate by using the k-ID API directly, without pre-built widgets. This approach gives you full control over the user interface while k-ID handles the compliance logic.
This guide demonstrates a simplified integration pattern where all permissions are configured as essential in Compliance Studio. With this configuration, once a session is created (either directly or after parental consent), all features are immediately available. No session upgrade flows are required.
If your product has Automatic age assurance enabled for the target jurisdiction, /age-gate/check can return a CHALLENGE_AGE_GATE_AGE_ASSURANCE challenge for players who claim an age old enough to skip parental consent, even with all permissions configured as essential. Session creation is deferred until the player proves the claimed age. The feature is gated by an organization-level setting that only k-ID can grant; if it isn't enabled for your product, you can skip Step 4b.
What's a custom age gate?
A custom age gate is an age verification interface that you build and control, while k-ID's API handles the compliance logic behind the scenes. This approach is ideal when you need:
- Full UI Control: Design an age gate that matches your brand and user experience
- Platform Integration: Build native experiences for mobile apps or game engines
- Custom Workflows: Implement age verification as part of a larger registration flow
Prerequisites
Before you begin, you'll need:
- A k-ID Product: Create and configure your product in the k-ID Compliance Studio
- Essential Permissions Configuration: Configure all permissions as "essential" in your product's permissions settings
- API Key: Generate your API key from the Developer Settings page of your product in the Compliance Studio
- Webhook Endpoint (recommended): Set up a secure HTTPS endpoint to receive challenge and session events. For more detail, see Webhooks.
Step 1: Get age gate requirements
Before displaying your age gate, call /age-gate/get-requirements to determine what you need to show based on the user's jurisdiction.
For your implementation, all API calls should be server-to-server to protect your API key from being exposed in client-side code.
Example request
GET /api/v1/age-gate/get-requirements?jurisdiction=US-CA
Authorization: Bearer your-api-key
Example response
{
"shouldDisplay": true,
"ageAssuranceRequired": false,
"digitalConsentAge": 13,
"civilAge": 18,
"minimumAge": 0,
"approvedAgeCollectionMethods": [
"date-of-birth",
"age-slider",
"platform-account"
]
}
Response fields
| Field | Description |
|---|---|
shouldDisplay | Whether an age gate should be displayed |
ageAssuranceRequired | Whether age verification is required for this jurisdiction |
digitalConsentAge | Minimum age for digital consent (users below this need parental consent) |
civilAge | Age at which users are considered legal adults |
minimumAge | Minimum age to access your product (configured in Compliance Studio) |
approvedAgeCollectionMethods | Allowed methods for collecting age in this jurisdiction |
If shouldDisplay is false, skip the age gate and call /age-gate/get-default-permissions to get the default session permissions for that jurisdiction.
If your game has a platform-reported age signal (Apple iOS, Google Play, Xbox, Meta Horizon, or k-ID), include it as query parameters (platformName, platformAgeLow, platformAgeHigh, platformCategory, platformDeclarationType, platformVerificationId) so a verified adult signal can flip shouldDisplay to false and skip the age gate entirely. See Platform age signals.
Step 2: Build your age gate UI
Based on the response, build your age gate interface by using the approved collection methods:
date-of-birth: Full date of birth input (YYYY-MM-DD)age-slider: Age range or slider selectionplatform-account: Use existing platform account age data
For detailed design recommendations, including age slider behavior, date picker requirements, and accessibility considerations, see the UX guidelines.
Example age gate UI
<div id="age-gate">
<h2>Please enter your date of birth</h2>
<form id="age-gate-form">
<label for="dob">Date of Birth</label>
<input type="date" id="dob" name="dob" required />
<button type="submit">Continue</button>
</form>
</div>
Step 3: Check age with the API
When the user submits their age, call /age-gate/check with the collected age information and jurisdiction. You can pass either dateOfBirth or age depending on which age collection method you used.
Example request with date of birth
If you used a date picker to collect a full date of birth:
POST /api/v1/age-gate/check
Content-Type: application/json
Authorization: Bearer your-api-key
{
"jurisdiction": "US-CA",
"dateOfBirth": "2015-04-15"
}
Example request with age
If you used an age slider to collect the user's age:
POST /api/v1/age-gate/check
Content-Type: application/json
Authorization: Bearer your-api-key
{
"jurisdiction": "US-CA",
"age": 9
}
Example request with a platform age signal
You can also include a platformAgeSignal, either on its own or alongside dateOfBirth/age. A verified signal satisfies verified-age permissions without an extra verification step; an unverified one still feeds age-conflict detection. See Platform age signals.
POST /api/v1/age-gate/check
Content-Type: application/json
Authorization: Bearer your-api-key
{
"jurisdiction": "US-CA",
"dateOfBirth": "2005-04-15",
"platformAgeSignal": {
"name": "apple-ios",
"ageLow": 18,
"ageHigh": 25,
"declarationType": "governmentIDChecked"
}
}
Possible responses
The API returns one of three statuses: PASS, PROHIBITED, or CHALLENGE. A session is created immediately on PASS. A CHALLENGE response must be resolved before a session exists, and you must branch on challenge.type to decide how to present it:
CHALLENGE_PARENTAL_CONSENT: the claimed age is too young to proceed without parental consent; a trusted adult must approve (Step 4).CHALLENGE_AGE_GATE_AGE_ASSURANCE: the claimed age is old enough to skip parental consent, but your product has Automatic age assurance enabled and the player must prove the claim (Step 4b).
PASS: User can proceed
The user's age allows immediate access. A session is created with all permissions. No challenge is created since parental consent isn't required.
{
"status": "PASS",
"session": {
"sessionId": "608616da-4fd2-4742-82bf-ec1d4ffd8187",
"ageStatus": "LEGAL_ADULT",
"dateOfBirth": "2005-04-15",
"jurisdiction": "US-CA",
"permissions": [...],
"status": "ACTIVE"
}
}
Action: store the sessionId and allow the user to proceed.
PROHIBITED: User is blocked
The user's age is below the minimum age configured for your product.
{
"status": "PROHIBITED"
}
Action: display an age-appropriate message and prevent access.
CHALLENGE with CHALLENGE_PARENTAL_CONSENT: Parental consent required
The user's age requires Verifiable Parental Consent (VPC). A challenge is created for a trusted adult to approve. Once the challenge is approved, a session is created with the granted permissions.
{
"status": "CHALLENGE",
"challenge": {
"challengeId": "683409f1-2930-4132-89ad-827462eed9af",
"oneTimePassword": "PP5BUS",
"type": "CHALLENGE_PARENTAL_CONSENT",
"url": "https://family.k-id.com/authorize?otp=PP5BUS"
}
}
Action: store the challengeId and display the trusted adult challenge screen (see Step 4).
CHALLENGE with CHALLENGE_AGE_GATE_AGE_ASSURANCE: Automatic age assurance
Returned when the player claims an age old enough to skip parental consent and your product has Automatic age assurance enabled for the jurisdiction. The player must prove the claim (facial age estimation or ID document) before a session is created. No trusted adult is involved and no OTP is issued.
{
"status": "CHALLENGE",
"challenge": {
"challengeId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "CHALLENGE_AGE_GATE_AGE_ASSURANCE",
"url": "https://family.k-id.com/age-gate/verify?token=..."
}
}
Action: store the challengeId and embed challenge.url in an iframe (see Step 4b).
Step 4: Build the trusted adult challenge screen
When you receive a CHALLENGE response, display a screen that allows the user to contact a trusted adult for consent. The challenge response provides everything you need.
For detailed design recommendations for the trusted adult consent flow, including layout examples and implementation tips, see Trusted adult consent in the UX guidelines.
Challenge screen options
Your challenge screen should offer three options for the trusted adult to complete the consent:
Option 1: Email notification
Allow the user to enter a trusted adult's email address. When submitted, call /challenge/send-email to send a consent request email.
POST /api/v1/challenge/send-email
Content-Type: application/json
Authorization: Bearer your-api-key
{
"challengeId": "683409f1-2930-4132-89ad-827462eed9af",
"email": "parent@example.com"
}
Option 2: QR code
Display a QR code generated from the challenge.url field. The trusted adult can scan this with their phone to access the consent portal directly.
// Generate QR code from the challenge URL
const qrCodeUrl = challenge.url;
// Use a QR code library to render: "https://family.k-id.com/authorize?otp=PP5BUS"
Option 3: Manual code entry
Display the challenge.oneTimePassword and instruct the trusted adult to visit asktoplay.com and enter the code.
Example challenge screen implementation
<div id="challenge-screen">
<h2>Ask a trusted adult to help you</h2>
<p>
Please ask a trusted adult to complete setup using one of the following
methods.
</p>
<div class="options-container">
<!-- Option 1: Email -->
<div class="option">
<h3>Option 1</h3>
<p>Enter a trusted adult's email</p>
<form id="email-form">
<label for="email">Email</label>
<input type="email" id="email" placeholder="Email address" required />
<button type="submit">Submit</button>
</form>
</div>
<!-- Option 2: QR Code -->
<div class="option">
<h3>Option 2</h3>
<p>Ask a trusted adult to scan or click the QR code.</p>
<div id="qr-code">
<!-- Render QR code from challenge.url -->
</div>
</div>
<!-- Option 3: Manual Code -->
<div class="option">
<h3>Option 3</h3>
<p>
Go to <a href="https://asktoplay.com">asktoplay.com</a> and enter the
code below.
</p>
<div class="code-display">
<span id="otp-code">PP5BUS</span>
<button onclick="copyCode()">Copy</button>
</div>
</div>
</div>
<button id="do-later">Do This Later</button>
</div>
Storing the challenge
Store the challengeId while waiting for consent. You can store it in local storage, a database associated with the user's account, or any other persistent storage appropriate for your platform. If the user returns to your app before consent is granted, retrieve the challenge by using /challenge/get to restore the challenge screen.
// Store challenge when created (example using localStorage)
localStorage.setItem("pendingChallenge", challengeId);
// On app restart, check for pending challenge
const pendingChallenge = localStorage.getItem("pendingChallenge");
if (pendingChallenge) {
// Fetch challenge details and show challenge screen
const challenge = await fetchChallenge(pendingChallenge);
showChallengeScreen(challenge);
}
Step 4b: Handle age-assurance challenges
Skip this step if your product doesn't have Automatic age assurance enabled. When challenge.type is CHALLENGE_AGE_GATE_AGE_ASSURANCE, the player verifies themselves; don't show the trusted-adult screen from Step 4.
Embed the verification iframe
Render challenge.url directly in an iframe. The iframe handles facial age estimation or ID document verification, depending on jurisdiction and product configuration.
<iframe
id="age-assurance-widget"
src="CHALLENGE_URL"
width="100%"
height="600"
frameborder="0"
allow="camera;payment;publickey-credentials-get;publickey-credentials-create">
</iframe>
The allow attribute is required:
camera: facial age estimationpayment: credit-card based verificationpublickey-credentials-get/publickey-credentials-create: WebAuthn / AgeKey
Listen for Verification.Result DOM events
The iframe posts a Verification.Result message when the player finishes. Use this for responsive UI updates only; for data integrity, wait for the webhook in Step 5 or poll /challenge/get-status.
window.addEventListener("message", (event) => {
if (!event.origin.endsWith(".k-id.com")) {
return;
}
const message = event.data;
if (message?.eventType === "Verification.Result") {
if (message.data.status === "PASS") {
// Player verified successfully.
// A session is being created - wait for the Challenge.StateChange webhook
// or poll /challenge/get-status to retrieve the sessionId.
closeAgeAssuranceIframe();
} else if (message.data.status === "FAIL") {
// Player didn't meet the age criteria. No session is created.
closeAgeAssuranceIframe();
showVerificationFailedMessage();
}
}
});
For detailed event structure, see Verification.Result.
How this differs from CHALLENGE_PARENTAL_CONSENT
| Aspect | CHALLENGE_PARENTAL_CONSENT (Step 4) | CHALLENGE_AGE_GATE_AGE_ASSURANCE (Step 4b) |
|---|---|---|
| Who verifies | A trusted adult | The player |
oneTimePassword | Present | Not present |
challenge.url | https://family.k-id.com/authorize?otp=... | https://family.k-id.com/age-gate/verify?token=... |
| UI | Email / QR code / OTP entry | iframe with camera access |
/challenge/send-email | Applicable | Not applicable |
| Session timing | Created when the adult approves | Created after the player passes verification |
Step 5: Handle webhook events
Configure your webhook endpoint to receive events when the challenge status changes or when sessions are deleted. Note: the Challenge.StateChange webhook event is only sent when a challenge exists. If the age gate completes with a PASS status (no challenge required), no Challenge.StateChange event is sent.
Challenge.StateChange event
This event fires whenever a challenge resolves, whether that challenge was for parental consent or auto age-assurance. The event is sent whenever a challenge was created (that is, when the /age-gate/check response had status: "CHALLENGE").
{
"eventType": "Challenge.StateChange",
"data": {
"id": "683409f1-2930-4132-89ad-827462eed9af",
"productId": 42,
"status": "PASS",
"dob": "2015-04-15",
"sessionId": "0ad1641f-c154-4c2-8bb2-74dbd0de7723",
"approverEmail": "parent@example.com",
"kuid": "7a1f2c3d-4e5f-6789-abcd-ef0123456789"
}
}
Fields such as approverEmail, dob, and kuid are optional; the set of fields you see depends on the flow that produced the challenge. Key off challengeId to correlate the event back to the challenge you issued. For the complete field contract, see Challenge.StateChange.
| Status | Description | Action |
|---|---|---|
PASS | Consent granted | Store the sessionId and allow access |
FAIL | Consent denied | Show appropriate message, user can't proceed |
IN_PROGRESS | Challenge is still pending | Continue waiting |
Handling successful consent
When you receive a PASS status (via webhook or polling), the response includes a sessionId. You should:
- Get the session permissions: Call
/session/getwith thesessionIdto retrieve the full session details including permissions.
GET /api/v1/session/get?id=0ad1641f-c154-4c2-8bb2-74dbd0de7723
Authorization: Bearer your-api-key
Example response:
{
"session": {
"ageStatus": "DIGITAL_MINOR",
"dateOfBirth": "2015-04-15",
"etag": "6d9d24fccd428f845b355122799948dd0a52fc5d",
"jurisdiction": "US-CA",
"kuid": "7a1f2c3d-4e5f-6789-abcd-ef0123456789",
"permissions": [
{
"enabled": true,
"managedBy": "GUARDIAN",
"name": "text-chat-private"
},
{
"enabled": false,
"managedBy": "GUARDIAN",
"name": "voice-chat"
}
],
"sessionId": "0ad1641f-c154-4c2-8bb2-74dbd0de7723",
"status": "ACTIVE"
},
"status": "PASS"
}
- Replace the stored challenge with the session: Clear the
challengeIdand store thesessionIdinstead. This indicates the user now has an active session with granted permissions.
// Clear the pending challenge and store the active session
user.challengeId = null;
user.sessionId = data.sessionId;
- Apply permissions: Use the permissions from the session response to enable or disable features in your application.
Session.Delete event
This event fires when a session is deleted (for example, when a parent revokes access through Family Connect).
{
"eventType": "Session.Delete",
"data": {
"id": "0ad1641f-c154-4c2-8bb2-74dbd0de7723",
"productId": 42
}
}
Action: remove the stored session and require the user to complete the age gate flow again.
Example webhook handler
app.post("/webhook/k-id", (req, res) => {
const { eventType, data } = req.body;
switch (eventType) {
case "Challenge.StateChange":
if (data.status === "PASS") {
// Grant access - store sessionId for the user
grantAccess(data.sessionId, data.kuid);
} else if (data.status === "FAIL") {
// Deny access
denyAccess(data.id);
}
break;
case "Session.Delete":
// Revoke access - user must complete age gate again
revokeSession(data.id);
break;
}
res.status(200).send("OK");
});
Polling as fallback
If webhooks aren't available, you can poll /challenge/get-status to check challenge status:
GET /api/v1/challenge/get-status?id=683409f1-2930-4132-89ad-827462eed9af
Authorization: Bearer your-api-key
When polling, wait at least 5 seconds between requests. The API might return HTTP 429 if you poll too frequently.
Webhook configuration
For the custom age gate flow, ensure your webhook endpoint is configured to receive:
Challenge.StateChange: Notifies when parental consent is approved or deniedSession.Delete: Notifies when a session is deleted by a parent
For information on validating webhook signatures, see Webhooks.
What's next?
Now that you've implemented the custom age gate, explore these resources to enhance your integration:
- UX Guidelines: Design recommendations for age sliders, date pickers, and consent flows
- API Reference Documentation: Detailed documentation of all age gate and challenge APIs
- Webhooks Setup: Implement robust webhook handling for production systems
- Best Practices: Implement best practices to ensure security and a reliable user experience
- Testing: Test your integration with test mode APIs
- Pre-launch Checklist: Review requirements before going live
With k-ID's custom age gate integration, you can build a fully branded compliance experience while k-ID handles the complex jurisdictional logic and parental consent flows.