Skip to main content

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.

Simplified Integration with Essential Permissions

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.

Automatic age assurance

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:

  1. A k-ID Product: Create and configure your product in the k-ID Compliance Studio
  2. Essential Permissions Configuration: Configure all permissions as "essential" in your product's permissions settings
  3. API Key: Generate your API key from the Developer Settings page of your product in the Compliance Studio
  4. 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.

Important

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

FieldDescription
shouldDisplayWhether an age gate should be displayed
ageAssuranceRequiredWhether age verification is required for this jurisdiction
digitalConsentAgeMinimum age for digital consent (users below this need parental consent)
civilAgeAge at which users are considered legal adults
minimumAgeMinimum age to access your product (configured in Compliance Studio)
approvedAgeCollectionMethodsAllowed methods for collecting age in this jurisdiction
No Age Gate Required

If shouldDisplay is false, skip the age gate and call /age-gate/get-default-permissions to get the default session permissions for that jurisdiction.

Platform age signals

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 selection
  • platform-account: Use existing platform account age data
UX Guidelines

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.

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.

UX Guidelines

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 estimation
  • payment: credit-card based verification
  • publickey-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.

AspectCHALLENGE_PARENTAL_CONSENT (Step 4)CHALLENGE_AGE_GATE_AGE_ASSURANCE (Step 4b)
Who verifiesA trusted adultThe player
oneTimePasswordPresentNot present
challenge.urlhttps://family.k-id.com/authorize?otp=...https://family.k-id.com/age-gate/verify?token=...
UIEmail / QR code / OTP entryiframe with camera access
/challenge/send-emailApplicableNot applicable
Session timingCreated when the adult approvesCreated 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.

StatusDescriptionAction
PASSConsent grantedStore the sessionId and allow access
FAILConsent deniedShow appropriate message, user can't proceed
IN_PROGRESSChallenge is still pendingContinue waiting

When you receive a PASS status (via webhook or polling), the response includes a sessionId. You should:

  1. Get the session permissions: Call /session/get with the sessionId to 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"
}
  1. Replace the stored challenge with the session: Clear the challengeId and store the sessionId instead. 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;
  1. 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
Polling limits

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:

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:

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.