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.

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.

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
}

Possible responses

The API returns one of three statuses:

PASS: User can proceed

The user's age allows immediate access. A session is created with all permissions.

{
"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.

{
"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).

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 5: Handle webhook events

Configure your webhook endpoint to receive events when the challenge status changes or when sessions are deleted.

Challenge.StateChange event

This event fires when a trusted adult approves or denies the consent request.

{
"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"
}
}
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.