// File: webhooks
# Webhooks
Webhooks let you subscribe to events happening in the k-ID Engine as they happen, as opposed to polling an API to see if data is available.
## What are webhooks?
Webhooks can be used for a variety of purposes, such as:
1. Handling challenge completion results
2. Handling age verification results
3. Handling changes in the k-ID Session
## Setting up webhooks
Webhooks are configured in the [Compliance Studio](/compliance-studio/creating-product), by specifying a URL that the k-ID Engine calls when an event occurs. The URL must be a secure HTTPS URL. The k-ID Engine sends a POST request to the URL with a JSON payload that contains the event data.
:::info
Webhooks are associated with individual Products. You can use the same endpoint for all of your k-ID Products if you have more than one, but it's important to note that you must retrieve the correct Product-specific k-ID API Key to make API calls (for example [`/session/get`](/api/endpoints/get-session)).
:::
Webhooks can be configured in the Developer Settings section of your product in the Compliance Studio.

## Webhook event structure
The JSON payload sent to the webhook URL contains the following fields:
- `eventType` - The type of event that occurred.
- `data` - The data associated with the event.
An `X-Event-Type` header is also sent with the event type.
## Validating webhook requests
Webhooks are sent over the public internet, so it's important to validate that the requests are coming from k-ID. This is done by verifying the event payload signature by using the configured webhook secret.
All requests include the following headers:
- `X-Signature-Timestamp` - The timestamp of the request, in UNIX epoch seconds.
- `X-Signature-Hmac-Sha256` - The HMAC SHA-256 keyed-hash of the UTF-8 encoded timestamp and request body concatenated together, using the webhook secret as the key, encoded as a lowercase hexadecimal string.
If the signature is invalid, the request should be rejected with a 401 status code. Webhook requests with validated signatures can be processed and accepted with a 200 status code.
### Example validation code
```javascript
const crypto = require("crypto");
// Your webhook secret, configured in the [Compliance Studio](/compliance-studio/creating-product).
const SECRET = "your-secret";
const timestamp = req.get("X-Signature-Timestamp");
const signature = req.get("X-Signature-Hmac-Sha256");
const body = req.rawBody; // Raw request body, as a string.
// Compute the expected signature.
const hmac = crypto.createHmac("sha256", SECRET);
hmac.update(timestamp + body);
const expectedSignature = hmac.digest("hex");
// Compare signatures securely.
if (
!crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expectedSignature, "hex")
)
) {
return res.status(401).end("Unauthorized");
}
```
## Event types
| Event Type | Description |
|------------|-------------|
| [`Challenge.StateChange`](/events/webhooks/event-types/challenge-statechange) | Emitted when a parental consent challenge changes state |
| [`Verification.Result`](/events/webhooks/event-types/verification-result) | Emitted with the result of a verification attempt |
| [`Account.Delete`](/events/webhooks/event-types/account-delete) | Emitted when an account is deleted |
| [`AgeAssurance.Result`](/events/webhooks/event-types/ageassurance-result) | Emitted with the result of an Age Assurance evaluation (deprecated, replaced by `Verification.Result`) |
| [`Session.ChangePermissions`](/events/webhooks/event-types/session-changepermissions) | Emitted when session permissions are modified by a parent |
| [`Session.Delete`](/events/webhooks/event-types/session-delete) | Emitted when a session is deleted |
| [`Test`](/events/webhooks/event-types/test) | Used to verify that the webhook is working correctly |
---
// File: agekit-plus/overview
# AgeKit+
AgeKit+ provides comprehensive age verification capabilities for your application, allowing users to prove their age without revealing personal information. AgeKit+ offers flexible solutions to meet your age verification requirements.
## What's AgeKit+?
AgeKit+ is k-ID's age verification solution that provides:
- **Privacy-preserving verification**: Users can prove their age without sharing personal information
- **Multiple verification methods**: Support for facial scanning, ID verification, AgeKey, and more
- **Jurisdiction-aware**: Automatically adapts to local regulations and requirements
- **Flexible integration**: Choose between product-configured methods or dynamically selected methods
## Integration approaches
AgeKit+ supports two integration approaches:
### Waterfall flow
AgeKit+ acts as a single-point orchestrator for age checks, automatically cascading through a waterfall of verification providers to confirm a user's age. In practice, one API call to k-ID presents the configured methods in sequence. For example, starting with email inference or a facial age estimation and then falling back to an ID document scan or other methods as needed, until the user's age is verified or all options are exhausted. This means developers integrate once with k-ID's API, and the platform handles trying multiple verification techniques behind the scenes, combining methods to maximize the chances of a successful verification.
- Single API integration point
- Automatic waterfall through configured verification methods
- Maximizes verification success rates
- Jurisdiction-aware compliance
- See [Waterfall flow](/agekit-plus/waterfall-flow) for details
### Single method flow
When you need to choose verification methods dynamically through API calls rather than using your product's static configuration, use method-specific endpoints to create a custom UI for selecting verification methods. This gives you full control over which methods are presented and how users select them.
- Complete UI/UX control
- Method-specific endpoints
- Custom method selection logic
- See [Single method flow](/agekit-plus/single-method-flow) for details
## Verification methods
AgeKit+ supports multiple verification methods that balance security, user experience, and regulatory compliance:
- **Facial age estimation**: Privacy-preserving AI-based age estimation
- **ID document verification**: Government-issued ID verification
- **AgeKey**: Reusable age credential for repeat users
- **Credit card verification**: Age verification through payment processing
- **Email age estimation**: Age estimation using email address
- **Regional methods**: ConnectID (Australia), and more
For detailed information about all available verification methods, see [Verification methods](/concepts/verification-methods).
:::important Create verifications when users start the flow
Call verification creation endpoints (for example [Perform access age verification](/api/endpoints/perform-access-age-verification)) only after the user takes an action to begin verification. Don't pre-generate verifications or widget URLs for flows they might never start. See [Best practices](/agekit-plus/best-practices#when-to-create-verifications) for more detail.
:::
## Getting started
- **Quick start**: See the [Age verification quick start guide](/get-started/quickstart-guides/age-verification)
- **Waterfall flow**: Learn about [waterfall verification with product configuration](/agekit-plus/waterfall-flow)
- **Single method flow**: Learn about [dynamically selecting verification methods through API calls](/agekit-plus/single-method-flow)
- **API reference**: Explore the [Age verification API endpoints](/api/endpoints/perform-access-age-verification)
---
// File: agekit-plus/waterfall-flow
# Waterfall flow
Age verification with k-ID is a privacy-preserving process that allows users to prove their age without revealing personal information. This approach uses a **Waterfall flow** model.
## Waterfall flow
AgeKit+ acts as a single-point orchestrator for age checks, automatically cascading through a waterfall of verification providers to confirm a user's age. In practice, one API call to k-ID presents the configured methods in sequence. For example, starting with email inference or a facial age estimation and then falling back to an ID document scan or other methods as needed, until the user's age is verified or all options are exhausted. This means developers integrate once with k-ID's API, and the platform handles trying multiple verification techniques behind the scenes, combining methods to maximize the chances of a successful verification.
The verification flow is initiated with an API call that returns a URL where users complete the verification process. You can present this URL across different surfaces (see [Presenting the verification URL](#presenting-the-verification-url)). The available verification methods are determined by your product configuration in the Compliance Studio, ensuring compliance with jurisdiction requirements.
:::important Create verifications when users start the flow
Call verification creation endpoints (for example [Perform access age verification](/api/endpoints/perform-access-age-verification)) only after the user takes an action to begin verification. Don't pre-generate verifications or widget URLs for flows they might never start. See [Best practices](/agekit-plus/best-practices#when-to-create-verifications) for more detail.
:::
| API | Scenario |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| [`/age-verification/perform-access-age-verification`](/api/endpoints/perform-access-age-verification) | To verify the age of a user before getting access to a feature, mature content, or the product itself. |
| [`/age-verification/perform-trusted-adult-verification`](/api/endpoints/perform-trusted-adult-verification) | To perform trusted adult (parent or guardian) verification. |
| [`/age-verification/perform-age-appeal`](/api/endpoints/perform-age-appeal) | For users who have failed age verification but want to appeal the decision. |
The Age Verification APIs are standardized in terms of the request & response format.
### Request body
| Property | Description | Required? |
| ------------- | ------------------------------------------------------------ | --------- |
| `jurisdiction` | The jurisdiction in which the age verification should happen | Yes |
| `criteria` | The criteria for age verification | Yes |
| `subject.email` | If the user verified their age with k-ID in any other context with an email address, then the original age is returned instead of asking the user to estimate or prove their age again. | No |
| `subject.claimedAge` | If a user was asked for their age in an age gate, used to inform the age estimation process | No |
| `subject.id` | An identifier used across multiple verification methods to report multiple failed attempts. This can be a temporary session ID, or hashed user ID. | No |
| `options.facialAgeEstimation.passIfOver` | The estimated age threshold required to automatically pass facial age estimation. If the estimated age is at or greater than this value, the verification passes. | No |
| `options.facialAgeEstimation.failIfUnder` | The estimated age threshold below which the verification fails. If the estimated age is below this value, the verification fails. Defaults to the verification criteria age when omitted. | No |
| `options.locale` | Optional IETF BCP 47 tag for the verification UI. If omitted, language follows the visitor's browser preferences; set it when your app already exposes a user-selected language and verification should match that choice. | No |
| `options.redirectUrl` | The URL to redirect to after verification completes. Supports HTTP/HTTPS URLs or mobile deeplinks with custom protocol schemes. The redirect only occurs when the verification URL is opened directly in a browser or webview (not embedded in an iframe). When a redirect occurs, the URL includes `verificationId` and `result` (PASS or FAIL) as query string parameters. | No |
The `passIfOver` and `failIfUnder` parameters give you control over the variance allowed in facial age estimation results. When a facial age estimation scan is performed:
- If the estimated age is **at or greater than** `passIfOver`, the verification **PASSES** and an age signal is determined.
- If the estimated age is **below** `failIfUnder`, the verification **FAILS** and an age signal is determined.
- If the estimated age is greater than `failIfUnder` and less than `passIfOver`, the result is considered **inconclusive**. Facial age estimation is disabled for the rest of that verification, and the user can continue with any of the remaining verification methods. The verification fails with `max-attempts-exceeded` only once every method has been exhausted.
This allows you to set a confidence range where results are clear enough to make a determination, while falling back to your other verification methods when the estimation lands in an uncertain range. For example, if you need to verify users are 18+, you might set `passIfOver` to 25 and `failIfUnder` to 12. This means users estimated to be 25 or older pass immediately, users estimated to be below 12 fail immediately, and users estimated to be 12-24 can continue with another verification method (facial age estimation itself isn't retried).
Sample:
```jsx
{
"jurisdiction": "US-CA",
"criteria": {
"ageCategory": "ADULT"
},
"options": {
"facialAgeEstimation": {
"passIfOver": 25,
"failIfUnder": 12
},
"locale": "en-US",
"redirectUrl": "https://example.com/verification-complete"
}
}
```
#### Redirect URL
The `redirectUrl` parameter allows you to specify where users should be redirected after completing verification. This is useful for:
- **Browser-based flows**: Redirecting to another web page after verification completes
- **Custom success screens**: Displaying your own custom success or failure page
- **Mobile app deeplinks**: Using custom protocol schemes (for example, `myapp://verification-complete`) to return control to your mobile app
:::important
The redirect only occurs when the verification URL is opened directly in a browser or webview (not embedded in an iframe). When embedded in an iframe, verification results are delivered via DOM events instead.
:::
When a redirect occurs, the redirect URL includes the following query string parameters:
- `verificationId`: The unique verification ID
- `result`: The verification result, either `PASS` or `FAIL`
Example redirect URL:
```
https://example.com/verification-complete?verificationId=7854909b-9124-4bed-9282-24b44c4a3c97&result=PASS
```
### Response body
A successful request to the Age Verification API returns the following response.
| Property | Description |
| -------- | ------------------------------------------------------------------------------------------- |
| `id` | A unique verification ID generated by Age Verification Service |
| `url` | The hosted age verification URL presented to the user for them to verify themselves. Embed it in an iframe or open it in a browser or webview. See [Presenting the verification URL](#presenting-the-verification-url). |
| `shortUrl` | A short URL suitable for QR codes that redirects to the full verification URL. |
Sample:
```jsx
{
"id": "7854909b-9124-4bed-9282-24b44c4a3c97",
"url": "https://family.k-id.com/verify?token=eyJ...",
"shortUrl": "https://family.k-id.com/v/7854909b-9124-4bed-9282-24b44c4a3c97?pid=42&s=qr"
}
```
On some platforms, such as living-room consoles, you can choose not to run the full verification flow inside an embedded browser. In those situations you can show a QR code built from `shortUrl` so the user completes verification on a phone or another device. The full `url` carries the session as a JWT in its `token` query parameter. Don't try to shorten or reconstruct it yourself. Treat `shortUrl` as an opaque value. Display or encode it exactly as returned, and don't depend on its path or query layout, which might change over time.
#### Verification URL validity
The verification URL is valid for **2 weeks** after creation. The expiry is encoded in the JWT in the URL's `token` query parameter. To determine whether a previously generated URL is still valid without opening it, decode the JWT and inspect the standard `exp` claim, which contains the expiration time.
If a user returns after the URL has expired, use the saved verification ID to call [`/age-verification/get-status`](/api/endpoints/get-age-verification-status). If the verification no longer exists (for example, it was never completed and has been removed), create a new verification by calling the perform endpoint again.
### Presenting the verification URL
The verification URL is a hosted web page. Present it in the surface that fits your application:
- **Web app**: embed it in an iframe (example below), open it as a pop-up, or redirect to it as a full page.
- **Mobile app**: open the verification URL in a system browser component (Android Custom Tabs, iOS ASWebAuthenticationSession) and receive the result through the `redirectUrl` callback. See the [Mobile apps quick start](/get-started/quickstart-guides/mobile-apps) for the recommended platform components, the AgeKey support matrix, and device orientation guidance.
- **Console** (Switch, PlayStation, Xbox): console browsers are typically restricted or absent. Display the `shortUrl` as a QR code so the player completes verification on a paired mobile device, then confirm the result via webhook or [`/age-verification/get-status`](/api/endpoints/get-age-verification-status) polling, since the mobile-device redirect can't return to the console.
The available methods inside the verification interface automatically adapt to jurisdictional requirements regardless of host.
#### Web example
Use the returned URL to create an iframe in your website or app. Users complete their verification through this interface, with available methods automatically adapting to jurisdictional requirements.

```html
```
The `allow` attribute is required to enable the following features:
- `camera`: Required for facial age estimation
- `payment`: Required for credit card verification
- `publickey-credentials-get` and `publickey-credentials-create`: Required for WebAuthn-based verification methods
### Verification result
Once the user has successfully completed the age verification, or the user has retried the maximum number of times and hasn't succeeded, the Age Verification Result is delivered through both client-side and server-side channels. **Implementations should use a combination of both**: client-side events are best for controlling UI elements, while for data integrity, the actual results should come from either a webhook or a call to [`/age-verification/get-status`](/api/endpoints/get-age-verification-status).
For detailed information about analyzing verification results, including field presence rules, status types, and implementation guidance, see the [Verification Event Contract](/events/webhooks/event-types/verification-result#verification-event-contract).
**Client-side (DOM events)** - If the URL from the response body is included in an iframe, it's sent to the parent frame as a window message (`MessageEvent`) with a [`Verification.Result`](/events/dom-events/event-structures/verification-result) structure.
**Server-side (webhooks)** - An event is sent to the registered webhook in the form of a [`Verification.Result`](/events/webhooks/event-types/verification-result) event.
Example of accessing the window message:
```ts
const handleMessage = (event: MessageEvent) => {
const message = event.data;
if (message.eventType === "Verification.Result") {
// Use DOM Events for immediate UI updates
updateUI(message);
}
};
window.addEventListener("message", handleMessage);
```
:::important
For data integrity, always verify results with events from webhooks or by calling [`/age-verification/get-status`](/api/endpoints/get-age-verification-status) rather than relying solely on DOM Events. DOM Events are best suited for responsive UI updates.
:::
The data element of the window event and the webhook event contains the following properties.
| Property | Description |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | The verification ID for which this is the result. |
| `status` | Indicates a `PASS` or `FAIL` status, based on whether the user met the age criteria or not. |
| `ageCategory` | Indicates the age category the user belongs to in the jurisdiction specified in the request. Supported values are `adult`, `digital-youth` or `digital-minor` |
| `method` | Indicates the method used for the verification. Supported values are `id-document`, `age-estimation`, `age-attestation`, `credit-card`, `social-security-number` |
| `failureReason` | The reason the verification failed. Supported values are `age-criteria-not-met`, `max-attempts-exceeded`, or `fraudulent-activity-detected`. This is only set if `status` is `FAIL` |
| `age` | Returns the lower bound and higher bound of the estimated or verified age as `low` and `high`. |
Sample:
```jsx
{
"eventType": "Verification.Result",
"data": {
"id": "5a58e98a-e477-484b-b36a-3857ea9daaba",
"status": "PASS",
"ageCategory": "adult",
"method": "id-document",
"age": {
"low": 25,
"high": 25,
}
}
}
```
Handling a window event:
```ts
const handleMessage = (event: MessageEvent) => {
const message = event.data;
if (message.eventType === "Verification.Result") {
if (message.data.status === "PASS") {
window.location.href = `https://www.example.com/success?verificationId=${message.data.id}`
}
if (message.data.status === "FAIL") {
window.location.href = `https://www.example.com/fail?verificationId=${message.data.id}`
}
}
};
window.addEventListener("message", handleMessage);
```
### Verification error
If an unexpected error has occurred, a JavaScript event fires so that your implementation can gracefully handle the error.
For detailed information about the event structure, see [`Verification.Error`](/events/dom-events/event-structures/verification-error).
Sample Message:
```jsx
{
"eventType": "Verification.Error",
"method": "credit-card",
"status": "ERROR"
}
```
## Checking verification status
You can get the status of a verification **independent of the verification URL** by calling [`/age-verification/get-status`](/api/endpoints/get-age-verification-status) with the verification ID. This is useful when:
- The registered webhook was unreachable and the status event was never received
- You need to check status without redirecting the user to the verification URL
- A previously generated URL has expired and you want to see if the verification was already completed
The data structure returned follows the same contract as the [`Verification.Result`](/events/webhooks/event-types/verification-result) webhook event, but there are some differences in structure and field presence. For detailed information about analyzing verification results, including field presence rules, status types, differences between webhook and API endpoint responses, and implementation guidance, see the [Verification Event Contract](/events/webhooks/event-types/verification-result#verification-event-contract). For more information about webhook events, see [Webhooks](/webhooks).
### Handling expired verification URLs
If a user has an expired verification URL (valid for 2 weeks after creation):
1. Call [`/age-verification/get-status`](/api/endpoints/get-age-verification-status) with the saved verification ID.
2. If the verification exists, use the returned status (for example, `PASS` or `FAIL`).
3. If get-status returns 400 with error code `INVALID_INPUT`, create a new verification by calling the perform endpoint again and present the new URL to the user.
### Verification retention
Verifications that remain in `PENDING` status for more than 2 weeks are deleted from the system. After deletion, [`/age-verification/get-status`](/api/endpoints/get-age-verification-status) returns 400 with error code `INVALID_INPUT`; callers should treat that response as requiring a new verification to be created.
## Edge case handling
When processing verification results, you must handle variations in the response structure depending on the verification outcome and failure reason. The following examples demonstrate correct and incorrect handling patterns for different response variations. For complete field presence rules, see the [Verification Event Contract](/events/webhooks/event-types/verification-result#verification-event-contract).
### Partial attempt (max attempts exceeded)
When a user exhausts all verification attempts without a conclusive age determination, the verification fails with `max-attempts-exceeded`. This response doesn't include `method`, `age`, or `ageCategory` fields.
**Example payload:**
```json
{
"eventType": "Verification.Result",
"data": {
"id": "123e4567-e89b-12d3-a456-426614174002",
"status": "FAIL",
"failureReason": "max-attempts-exceeded"
}
}
```
**Example payload for `age-criteria-not-met` (for comparison):**
```json
{
"eventType": "Verification.Result",
"data": {
"id": "123e4567-e89b-12d3-a456-426614174001",
"status": "FAIL",
"method": "age-estimation-scan",
"failureReason": "age-criteria-not-met",
"age": {
"low": 16,
"high": 17
}
}
}
```
**Incorrect handling:**
```ts
// ❌ WRONG: Assumes method and age are always present for FAIL
const handleVerification = (result: VerificationResult) => {
if (result.data.status === "FAIL") {
// These fields are undefined when failureReason is max-attempts-exceeded
logFailedMethod(result.data.method);
recordFailedAge(result.data.age.low);
showRetryWithMethod(result.data.method);
}
};
```
**Correct handling:**
```ts
// ✅ CORRECT: Handles different failure scenarios appropriately
const handleVerification = (result: VerificationResult) => {
if (result.data.status === "FAIL") {
denyAccess();
switch (result.data.failureReason) {
case "max-attempts-exceeded":
// No age determination was made - offer alternative options
showMaxAttemptsMessage();
offerSupportContact();
// Consider implementing rate limiting for future attempts
break;
case "age-criteria-not-met":
// Age was determined but didn't meet criteria
// method and age fields are available
if (result.data.age) {
logDeterminedAge(result.data.age.low);
}
showAgeCriteriaNotMetMessage();
break;
case "fraudulent-activity-detected":
// Handle suspicious activity (see next section)
handleSuspiciousActivity(result.data.id);
break;
default:
// Handle unknown failure reasons gracefully
logUnknownFailure(result.data.failureReason);
showGenericFailureMessage();
}
}
};
```
### Suspicious activity detected
When the system detects potentially fraudulent behavior, the verification fails with `fraudulent-activity-detected`. This response excludes age data and doesn't include the `method` field.
**Example payload:**
```json
{
"eventType": "Verification.Result",
"data": {
"id": "123e4567-e89b-12d3-a456-426614174003",
"status": "FAIL",
"failureReason": "fraudulent-activity-detected"
}
}
```
**Incorrect handling:**
```ts
// ❌ WRONG: Treats fraudulent activity like a normal failure
const handleVerification = (result: VerificationResult) => {
if (result.data.status === "FAIL") {
// Allowing immediate retry could enable continued abuse
showRetryButton();
// Logging age data that doesn't exist
analytics.track("verification_failed", {
age: result.data.age?.low // undefined for fraudulent activity
});
}
};
```
**Correct handling:**
```ts
// ✅ CORRECT: Implements appropriate security measures
const handleVerification = (result: VerificationResult) => {
if (result.data.status === "FAIL") {
denyAccess();
if (result.data.failureReason === "fraudulent-activity-detected") {
// Log the security event for review
securityLog.warn("Fraudulent activity detected", {
verificationId: result.data.id,
timestamp: new Date().toISOString(),
subjectId: currentSubjectId
});
// Implement stricter rate limiting or temporary blocks
applySecurityCooldown(currentSubjectId);
// Show appropriate message without revealing detection details
showVerificationUnavailableMessage();
// Don't offer immediate retry - this could enable continued abuse
hideRetryOptions();
// Optionally flag for manual review
flagForManualReview(result.data.id);
}
}
};
```
### Complete edge case handler
The following example shows a comprehensive handler that correctly processes all edge cases:
```ts
interface VerificationData {
id: string;
status: "PASS" | "FAIL";
method?: string;
ageCategory?: "adult" | "digital-youth" | "digital-minor"; // Always present for PASS status
age?: { low: number; high: number }; // Always present for PASS status
dob?: string;
failureReason?: string;
}
const handleVerificationResult = (data: VerificationData) => {
// Always log the verification attempt
logVerificationAttempt(data.id, data.status);
if (data.status === "PASS") {
// Grant access - the user met the age criteria
grantAccess();
// age and ageCategory are always present for PASS status
storeAgeData(data.age.low, data.age.high);
applyPermissionsForCategory(data.ageCategory);
// Process optional fields only if present
if (data.dob) {
storeDateOfBirth(data.dob);
}
if (data.method) {
analytics.track("verification_passed", { method: data.method });
}
return;
}
// Handle FAIL status
denyAccess();
// failureReason is always present for FAIL status
switch (data.failureReason) {
case "age-criteria-not-met":
// method and age are available for this failure reason
handleAgeCriteriaFailure(data);
break;
case "max-attempts-exceeded":
// No age determination - method and age are NOT available
handleMaxAttemptsFailure(data.id);
break;
case "fraudulent-activity-detected":
// Security event - method and age are NOT available
handleFraudulentActivity(data.id);
break;
default:
// Always handle unknown failure reasons gracefully
handleUnknownFailure(data);
}
};
```
:::tip Best practice
Always check for field presence before accessing optional fields. The [Verification Event Contract](/events/webhooks/event-types/verification-result#verification-event-contract) provides complete field presence rules for each status and failure reason combination.
:::
## Limiting verification attempts
Each verification request allows users three attempts per available verification method. A verification fails if:
- All available verification methods have been exhausted and an age can't be determined
- An age is determined but falls below the required threshold for your criteria
When a verification fails, you can allow users to initiate a new verification attempt. However, to prevent misuse and abuse of the verification system, you should implement rate limiting on additional verification attempts. For example, you might limit users to three verification attempts within a 24-hour period.
Use the `subject.id` field in the verification request to track attempts across multiple verification requests. This field should contain a consistent identifier for the user (such as a temporary session ID or hashed user ID), allowing you to:
- Track the number of verification attempts per user
- Implement time-based rate limiting (for example, 3 attempts per 24 hours)
- Prevent users from bypassing limits by creating new sessions
:::tip Best practice
Implement rate limiting on your server before initiating verification requests. This prevents unnecessary API calls and helps protect your system from abuse.
:::
## Verification methods
For detailed information about all available verification methods, see [Verification methods](/concepts/verification-methods).
---
// File: agekit-plus/single-method-flow
# Single method flow
When you need to choose verification methods dynamically through API calls rather than using your product's static configuration, you can use method-specific endpoints to create a custom UI for selecting verification methods. This approach gives you full control over which verification methods are presented and how users select them.
:::tip
For the recommended approach where verification methods are determined by your product configuration, see [Waterfall flow](/agekit-plus/waterfall-flow).
:::
:::important Create verifications when users start the flow
Call verification creation endpoints (for example [Perform access age verification](/api/endpoints/perform-access-age-verification)) only after the user takes an action to begin verification. Don't pre-generate verifications or widget URLs for flows they might never start. See [Best practices](/agekit-plus/best-practices#when-to-create-verifications) for more detail.
:::
## Method-specific endpoints
Method-specific endpoints bypass the automatic method selection and go directly into the verification process for the selected method. k-ID provides several method-specific endpoints:
| Endpoint | Verification Method |
|----------|---------------------|
| [`/age-verification/perform-facial-age-estimation`](/api/endpoints/perform-facial-age-estimation) | [Facial age estimation scan](/concepts/verification-methods#-facial-age-estimation-scan) |
| [`/age-verification/perform-id-verification`](/api/endpoints/perform-id-verification) | [ID scan verification](/concepts/verification-methods#-id-scan-verification) |
| [`/age-verification/perform-age-key-verification`](/api/endpoints/perform-age-key-verification) | [AgeKey](/concepts/verification-methods#-agekey) |
| [`/age-verification/perform-connect-id-verification`](/api/endpoints/perform-connect-id-verification) | [ConnectID (Australia)](/concepts/verification-methods#-connectid-australia) |
| [`/age-verification/perform-credit-card-verification`](/api/endpoints/perform-credit-card-verification) | [Credit card verification](/concepts/verification-methods#-credit-card-verification) |
For detailed information about verification methods, including what methods are available and how they work, see the [Verification Methods guide](/concepts/verification-methods). For a complete list of all available age verification endpoints, see the [Age verification endpoints](/api/endpoints/perform-access-age-verification) in the API Reference.
## Creating a custom verification UI
With method-specific endpoints, you can build a custom UI that:
1. Displays available verification methods to the user
2. Lets the user choose their preferred method
3. Calls the appropriate endpoint based on their selection
4. Falls back to other methods if the selected method fails
## Request format
All age verification endpoints use the same request format:
| Property | Description | Required? |
|----------|-------------|-----------|
| `jurisdiction` | The jurisdiction in which the age verification should happen | Yes |
| `criteria` | The criteria for age verification | Yes |
| `subject.email` | If the user verified their age with k-ID in any other context with an email address, the original age is returned instead of asking the user to verify again | No |
| `subject.claimedAge` | If a user was asked for their age in an age gate, used to inform the age estimation process | No |
| `subject.id` | An identifier used across multiple verification methods to report multiple failed attempts | No |
| `options.locale` | Optional IETF BCP 47 tag for the verification UI. If omitted, language follows the visitor's browser preferences; set it when your app already exposes a user-selected language and verification should match that choice. | No |
| `options.redirectUrl` | The URL to redirect to after verification completes. Supports HTTP/HTTPS URLs or mobile deeplinks with custom protocol schemes. The redirect only occurs when the verification URL is opened directly in a browser or webview (not embedded in an iframe). When a redirect occurs, the URL includes `verificationId` and `result` (PASS or FAIL) as query string parameters. | No |
### Example request
```json
POST /api/v1/age-verification/perform-facial-age-estimation
Content-Type: application/json
Authorization: Bearer your-api-key
{
"jurisdiction": "US-CA",
"criteria": {
"ageCategory": "ADULT"
},
"subject": {
"claimedAge": 23
},
"options": {
"locale": "en-US",
"redirectUrl": "https://example.com/verification-complete"
}
}
```
#### Redirect URL
The `redirectUrl` parameter allows you to specify where users should be redirected after completing verification. This is useful for:
- **Browser-based flows**: Redirecting to another web page after verification completes
- **Custom success screens**: Displaying your own custom success or failure page
- **Mobile app deeplinks**: Using custom protocol schemes (for example, `myapp://verification-complete`) to return control to your mobile app
:::important
The redirect only occurs when the verification URL is opened directly in a browser or webview (not embedded in an iframe). When embedded in an iframe, verification results are delivered via DOM events instead.
:::
When a redirect occurs, the redirect URL includes the following query string parameters:
- `verificationId`: The unique verification ID
- `result`: The verification result, either `PASS` or `FAIL`
Example redirect URL:
```
https://example.com/verification-complete?verificationId=7854909b-9124-4bed-9282-24b44c4a3c97&result=PASS
```
## Response format
All age verification endpoints return the same response format:
| Property | Description |
|----------|-------------|
| `id` | A unique verification ID |
| `url` | The age verification URL that must be presented to the user in an iframe |
| `shortUrl` | A short URL suitable for QR codes that redirects to the full verification URL |
### Example response
```json
{
"id": "7854909b-9124-4bed-9282-24b44c4a3c97",
"url": "https://family.k-id.com/verify?token=eyJhbGciOiJFUzM4NCIs...",
"shortUrl": "https://family.k-id.com/v/7854909b-9124-4bed-9282-24b44c4a3c97?pid=42&s=qr"
}
```
## Embedding the verification widget
Once you receive the verification URL, embed it in an iframe exactly as you would with the standard approach:
```html
```
## Receiving verification results
**Implementations should use a combination of client-side and server-side methods**: client-side events are best for controlling UI elements, while for data integrity, the actual results should come from either a webhook or a call to [`/age-verification/get-status`](/api/endpoints/get-age-verification-status).
For detailed information about analyzing verification results, including field presence rules, status types, and implementation guidance, see the [Verification Event Contract](/events/webhooks/event-types/verification-result#verification-event-contract).
### Client-side (DOM events)
Use DOM Events for responsive UI updates when verification completes. For detailed information about the event structure, see [`Verification.Result`](/events/dom-events/event-structures/verification-result).
```javascript
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') {
// User passed verification - update UI immediately
console.log('Age verified:', message.data.ageCategory);
updateUI();
} else if (message.data.status === 'FAIL') {
// User failed verification - update UI immediately
console.log('Verification failed:', message.data.failureReason);
updateUI();
}
}
});
```
### Server-side (webhooks, API calls)
Use webhooks or API calls for data integrity and reliable state management. For data integrity, always verify results with events from webhooks or by calling [`/age-verification/get-status`](/api/endpoints/get-age-verification-status) rather than relying solely on DOM Events.
#### Webhooks
For detailed information about the webhook event structure, see [`Verification.Result`](/events/webhooks/event-types/verification-result).
Configure a webhook endpoint to receive [`Verification.Result`](/events/webhooks/event-types/verification-result) events. For more information, see [Webhooks](/webhooks).
#### API calls
You can query the verification status by using [`/age-verification/get-status`](/api/endpoints/get-age-verification-status) with the verification ID. This is useful if your webhook was unreachable when the result was sent. For detailed information about analyzing verification results, including field presence rules, status types, and implementation guidance, see the [Verification Event Contract](/events/webhooks/event-types/verification-result#verification-event-contract).
## Age appeal
If a verification fails, you can allow users to appeal by calling the [`/age-verification/perform-age-appeal`](/api/endpoints/perform-age-appeal) endpoint. This presents the user with ID verification and Trusted Adult Attestation options (age estimation isn't available for appeals).
## Available endpoints
The following are common method-specific age verification endpoints:
| Endpoint | Description |
|----------|-------------|
| [`/age-verification/perform-facial-age-estimation`](/api/endpoints/perform-facial-age-estimation) | Only offers facial age estimation |
| [`/age-verification/perform-id-verification`](/api/endpoints/perform-id-verification) | Only offers ID document verification |
| [`/age-verification/perform-age-key-verification`](/api/endpoints/perform-age-key-verification) | Only offers AgeKey verification |
| [`/age-verification/perform-connect-id-verification`](/api/endpoints/perform-connect-id-verification) | Only offers ConnectID verification |
| [`/age-verification/perform-credit-card-verification`](/api/endpoints/perform-credit-card-verification) | Only offers credit card verification |
| [`/age-verification/perform-age-appeal`](/api/endpoints/perform-age-appeal) | For previously failed verifications (ID verification and Trusted Adult Attestation only) |
| [`/age-verification/get-status`](/api/endpoints/get-age-verification-status) | Query the status of a verification |
For a complete and up-to-date list of all available age verification endpoints, see the [Age verification endpoints](/api/endpoints/perform-access-age-verification) in the API Reference.
---
// File: agekit-plus/best-practices
# Best practices
## Age verification best practices
### When to create verifications {#when-to-create-verifications}
Call endpoints that generate a verification (for example [Perform access age verification](/api/endpoints/perform-access-age-verification)) only after a user takes an action to begin verification, such as tapping to verify before accessing a feature. Don't pre-generate verifications or widget URLs for flows the user might never start.
### Handling verification failures
When implementing access age verification, developers should check the `failureReason` field in verification results to properly handle failures:
- **Fraudulent Activity Detection**: If a verification fails and the `failureReason` is `fraudulent-activity-detected`, don't allow additional verification attempts for that user.
- **Other Failure Reasons**: For all other failure reasons (such as `age-criteria-not-met` or `max-attempts-exceeded`), implement rate limiting to prevent abuse while allowing legitimate retry attempts. Allow a maximum of **3 verification attempts per 24-hour period**. This prevents users from repeatedly attempting to bypass the verification system while providing reasonable retry opportunities for legitimate users
## Security recommendations
### Server-only API calls
:::tip Important
All widget URL generation endpoints should only be called from your server, never directly from client-side code.
:::
Your k-ID API key is a secret credential that must be protected:
- **Store API keys securely** using a secrets manager
- **Never expose API keys** in front end JavaScript, mobile app code, or any client-facing code
- **Never store API keys** on client devices or in client-side storage
### Target origins configuration
CDK offers the ability to configure target origins in the [Compliance Studio](/compliance-studio/creating-product) to control which domains can embed your widgets. This setting controls the [frame-ancestors directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors) of the Content Security Policy. As an optional security measure, it's recommended to evaluate your risk tolerance and decide whether to implement target origin restrictions based on your security requirements.
**Configuration Options:**
- **Specific domains**: Set exact domains for production use (for example, `https://yourgame.com`)
- **Wildcard subdomains**: Use wildcard patterns for subdomains (for example, `https://*.yourgame.com`)
- **Unrestricted**: Leave empty or set to `*` for unrestricted embedding (not recommended for production)
**Security Benefits:**
Target origins prevent attackers from embedding your verification flows in transparent iframes on malicious sites, where they could overlay other content and trick users into inadvertently clicking on verification elements.
:::warning Important
**Implementation Notes:**
- Configure separate target origins for each environment (test/live)
- **Ensure target origins are properly configured for all production endpoints before going live.**
- Each subdomain requires its own entry unless using wildcards
:::
:::info
Only certain k-ID pages can be embedded in iframes regardless of target origin settings (verification pages, widgets, and VPC flows). All other k-ID pages (family management, account settings) are always blocked from iframe embedding.
:::
### iframe permissions
Always include the necessary permissions in your iframe `allow` attribute:
```html
```
**Permission Breakdown**:
- `camera` - Required for facial age estimation
- `payment` - Required for credit card verification
- `publickey-credentials-create` - Required for AgeKey creation
- `publickey-credentials-get` - Required for AgeKey verification
### Origin validation
Always validate the origin of incoming messages:
```javascript
window.addEventListener('message', (event) => {
// Validate origin based on environment
const validOrigins = [
'https://family.k-id.com', // Live environment
'https://family.test.k-id.com' // Test environment
];
if (!validOrigins.includes(event.origin)) {
return; // Ignore messages from unauthorized origins
}
// Process the event
handleWidgetEvent(event.data);
});
```
---
// File: agekit-plus/prelaunch-checklist
# Prelaunch checklist
Before going live, consult this checklist as a simple resource to ensure you are ready for a successful launch.
## Prelaunch configuration
:::important Create verifications when users start the flow
Call verification creation endpoints (for example [Perform access age verification](/api/endpoints/perform-access-age-verification)) only after the user takes an action to begin verification. Don't pre-generate verifications or widget URLs for flows they might never start. See [Best practices](/agekit-plus/best-practices#when-to-create-verifications) for more detail.
:::
:::info Know your rate limits before launch
Live mode rate limits are significantly higher than test mode. Confirm your service can stay within the [default rate limits](/api/rate-limits) for both API requests and age verification / parental consent flows, or contact your k-ID representative if you need an increase.
:::
- [ ] **Product Configuration** (in the [Compliance Studio](/compliance-studio/creating-product))
- [ ] Product details and branding configured
- [ ] Permissions properly mapped to game features (when applicable) in [Permissions configuration](/compliance-studio/product-api-configuration#permissions)
- [ ] Verification methods selected and configured
- [ ] Target origins properly configured for both test and production
- [ ] **API Integration**
- [ ] Widget URLs generated correctly for all flows
- [ ] Event handlers implemented for all widget types
- [ ] Error handling implemented
- [ ] Rate limiting and HTTP 429 handling implemented. See [Rate limits](/api/rate-limits)
- [ ] Fallback flows defined for edge cases
## Security validation
- [ ] **Proper Environment Mapping**
- [ ] Test API key calling test endpoints
- [ ] Live API key calling live endpoints
- [ ] **Origin Validation**
- [ ] Event origin validation implemented
- [ ] Target origins configured in the [Compliance Studio](/compliance-studio/creating-product)
- [ ] CSP headers configured if applicable
- [ ] **iframe Security**
- [ ] Appropriate `allow` permissions set
- [ ] Sandbox attributes reviewed
- [ ] No sensitive data in URL parameters
## Final validation
- [ ] **End-to-End Testing**
- [ ] Complete user journeys tested
- [ ] Trusted Adult experience validated
- [ ] Session management working correctly
- [ ] Permission updates reflected in game
- [ ] **Compliance Verification**
- [ ] Legal review completed
- [ ] Compliance Engine up-to-date
- [ ] Privacy policy updated
- [ ] Data handling procedures verified
- [ ] Audit trail capabilities confirmed
Once all checklist items are completed, you're ready to publish your configuration to the live environment and begin serving real users with the CDK.
---
// File: api/interactive-reference
# Interactive reference
The k-ID API reference includes interactive code samples that allow you to test API endpoints directly from the documentation. Each endpoint page includes code samples and a request interface that you can use to make live API calls.

## Interactive request interface
Every endpoint page includes an interactive request interface in the "Request" section. This interface allows you to:
1. **Select the Base URL**: Hover over the Base URL field to reveal an Edit button. Click the Edit button to open a dropdown menu with the available Base URLs:
- **Live**: `https://game-api.k-id.com/api/v1/`
- **Test**: `https://game-api.test.k-id.com/api/v1/`
2. **Authenticate your request**: Paste your API key into the "Bearer Token" field
- You can get your API key from the [Compliance Studio](https://portal.k-id.com)
- Make sure you use the correct API key for the environment you've selected (test or live)
3. **Configure the request body**: The body field contains a sample request with example values
- You can modify any of the values in the request body to match your needs
- Required fields are indicated in the endpoint documentation
4. **Send the request**: Click "Send API Request" to complete the API call
5. **View the response**: The response is displayed below the request interface, showing the API's response to your request
## Code samples
Each endpoint page also includes code samples in the upper right corner showing how to make requests with various programming languages and tools. These samples are for reference and can be copied to use in your own code.
## Best practices
- **Use the test environment** when experimenting with the API
- **Never commit API keys** to version control - use environment variables instead
- **Review the response** carefully to understand the API's behavior
- **Check error responses** to understand what went wrong if a request fails
For more information about authentication and API keys, see [Authentication](/api/authentication).
---
// File: api/authentication
# Authentication
The k-ID API uses API keys for server-to-server (S2S) authentication. API keys can be generated in [Compliance Studio](https://portal.k-id.com).
## API key authentication
The API key must be included in the `Authorization` header of all requests.
```http
Authorization: Bearer {api-key}
```
### Example request
```bash
curl -X GET "https://game-api.k-id.com/api/v1/age-gate/get-requirements?jurisdiction=US-CA" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"
```
## Security best practices
**⚠️ Important Security Notes:**
- **Never expose your API key**: Keep your API key secure and never include it in client-side code or front end applications
- **Use environment variables**: Store your API key in environment variables, not in source code
- **Server-side only**: Make all API calls from your servers, not from browsers or mobile apps
- **HTTPS only**: Always use HTTPS for API requests to protect your API key in transit
- **Rotate keys regularly**: Regularly rotate your API keys for enhanced security
## Getting your API key
API keys can be generated and managed in [Compliance Studio](https://portal.k-id.com). Contact your k-ID representative or visit [Compliance Studio](https://portal.k-id.com) to obtain your API key.
---
// File: api/endpoints/k-id-api.info
import ApiLogo from "@theme/ApiLogo";
import Heading from "@theme/Heading";
import SchemaTabs from "@theme/SchemaTabs";
import TabItem from "@theme/TabItem";
import Export from "@theme/ApiExplorer/Export";
The k-ID API is an HTTP RPC-style web API for interacting with k-ID. It
Security Scheme Type:
http
HTTP Authorization Scheme:
bearer
---
// File: api/endpoints/await-challenge.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
:::caution deprecated
This endpoint has been deprecated and may be replaced or removed in future versions of the API.
:::
Use /challenge/get-status instead.
Request
---
// File: api/endpoints/check-age-category.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to check the age category of a player based on their date of birth and jurisdiction.
Request
---
// File: api/endpoints/check-age-gate.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API performs an age gate check. It checks if a player meets the age gate requirements based on their date of birth and jurisdiction.
Request
---
// File: api/endpoints/create-bulk-challenges.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to create multiple challenges at once.
Request
---
// File: api/endpoints/create-client-auth-token.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
:::caution deprecated
This endpoint has been deprecated and may be replaced or removed in future versions of the API.
:::
This API is used to create a signed access key for a game client.
Request
---
// File: api/endpoints/create-parental-consent-challenge.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to create a challenge for a custom scenario to obtain parental consent.
Request
---
// File: api/endpoints/delete-session.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
Deletes a player session by its session ID. The session must belong to
the calling product.
By default this is a soft delete: the session is marked revoked, is no
longer queryable, and its player ID becomes reusable, but the record
itself is retained.
**Irreversible.** Setting `hardDelete` to `true` instead
permanently deletes the session and its embedded consent — this
cannot be undone. Available only to developers explicitly enabled
for hard delete.
Request
---
// File: api/endpoints/generate-age-gate-url.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to generate a URL for the age gate widget.
To learn more about Verifiable Parental Consent (VPC), see the [VPC guide](/concepts/access-features-consent/vpc).
Request
---
// File: api/endpoints/generate-direct-notices-url.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to generate a URL for the direct notices widget.
To learn more about data notices, see the [Data Notices guide](/cdk/data-notices).
Request
---
// File: api/endpoints/generate-e-2-eurl.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to generate a URL for the e2e widget.
To learn more about Verifiable Parental Consent (VPC), see the [VPC guide](/concepts/access-features-consent/vpc).
Request
---
// File: api/endpoints/generate-manage-session-permissions-url.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to generate a URL for the manage permissions widget.
To learn more about sessions and permissions, see the [Sessions & permissions guide](/cdk/sessions-permissions).
Request
---
// File: api/endpoints/generate-otp.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to generate a new one-time password (OTP) for a given challenge.
Request
---
// File: api/endpoints/get-age-gate-requirements.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to fetch the requirements to display an age gate based on the player's jurisdiction.
Request
---
// File: api/endpoints/get-age-range-for-category.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API converts an age category used by a supported platform into an age range required to play in the requested jurisdiction. Supported category-based platforms are k-id, xbox, and meta-horizon.
Request
---
// File: api/endpoints/get-age-verification-status.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to get the status of an age verification request.
For detailed information about the verification event contract, including field presence rules, status types, and implementation guidance, see the [Verification Event Contract](/events/webhooks/event-types/verification-result#verification-event-contract).
Request
---
// File: api/endpoints/get-challenge-status.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to get the status of a challenge.
Request
---
// File: api/endpoints/get-challenge.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API returns the details of a previously created challenge.
Request
---
// File: api/endpoints/get-default-permissions.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to get the default permissions for a player based on their date of birth and jurisdiction.
Request
---
// File: api/endpoints/get-session.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API returns the details of a previously created session.
Request
---
// File: api/endpoints/perform-access-age-verification.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to perform access age verification for a user. It returns a URL that can be displayed in an iFrame.
To learn more about age verification, see the [AgeKit+ overview](/agekit-plus/overview).
Request
---
// File: api/endpoints/perform-age-appeal.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to perform age appeal for a user. It returns a URL that can be displayed in an iFrame.
Request
---
// File: api/endpoints/perform-age-key-verification.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to perform an AgeKey verification for a user. It returns a URL that can be displayed in an iFrame.
Request
---
// File: api/endpoints/perform-connect-id-verification.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to perform ConnectID verification for a user. It returns a URL that can be displayed in an iFrame. It is currently only available for Australia.
Request
---
// File: api/endpoints/perform-credit-card-verification.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to perform credit card verification for a user. It returns a URL that can be displayed in an iFrame.
Credit card verification is an adult (18+) assurance signal, not a precise age estimate.
To learn more about age verification, see the [AgeKit+ overview](/agekit-plus/overview).
Request
---
// File: api/endpoints/perform-custom-age-verification.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to perform custom age verification for a user. It returns a URL that can be displayed in an iFrame.
Request
---
// File: api/endpoints/perform-email-age-estimation.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to perform email age estimation for a user. It returns a URL that can be displayed in an iFrame.
Email age estimation is an 18+ signal. It can only establish that a subject is 18 or older and cannot distinguish younger age bands, so matching the request criteria to that capability is the caller's responsibility.
The subject's email is optional. If omitted, the user provides their email on the hosted verification page.
To learn more about age verification, see the [AgeKit+ overview](/agekit-plus/overview).
Request
---
// File: api/endpoints/perform-facial-age-estimation.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to perform facial age estimation for a user. It returns a URL that can be displayed in an iFrame.
To learn more about age verification, see the [AgeKit+ overview](/agekit-plus/overview).
Request
---
// File: api/endpoints/perform-id-verification.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to perform ID verification for a user. It returns a URL that can be displayed in an iFrame.
To learn more about age verification, see the [AgeKit+ overview](/agekit-plus/overview).
Request
---
// File: api/endpoints/perform-inference.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to perform email inference with background check via VerifyMy.
Request
---
// File: api/endpoints/perform-jurisdiction-appeal.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to perform a jurisdiction appeal for the user. It returns a URL that can be displayed in an iFrame.
Request
---
// File: api/endpoints/perform-trusted-adult-verification.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to perform trusted adult verification for a user. It returns a URL that can be displayed in an iFrame.
To learn more about Verifiable Parental Consent (VPC), see the [VPC guide](/concepts/access-features-consent/vpc).
Request
---
// File: api/endpoints/refresh-client-auth-token.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
:::caution deprecated
This endpoint has been deprecated and may be replaced or removed in future versions of the API.
:::
This API is used to refresh a signed access key for a game client.
Request
---
// File: api/endpoints/send-email.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to send the details of the challenge to an email address.
Request
---
// File: api/endpoints/set-age-verification-status.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API can only be used in test mode. It is used in order to assist with testing age verifications without needing to actually complete the age verification via the web flow.
Request
---
// File: api/endpoints/set-challenge-status.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to set the status of an existing challenge. This API is used for testing purposes only. It provides a convenient way to simulate the completion of a challenge, without needing to go through the entire VPC flow.
Request
---
// File: api/endpoints/set-guardian-managed-session-permissions.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to update the player permissions which can be managed by guardians.
Request
---
// File: api/endpoints/update-jurisdiction.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
This API is used to update the jurisdiction of a player session.
Request
---
// File: api/endpoints/upgrade-session.api
import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint";
import ParamsDetails from "@theme/ParamsDetails";
import RequestSchema from "@theme/RequestSchema";
import StatusCodes from "@theme/StatusCodes";
import OperationTabs from "@theme/OperationTabs";
import TabItem from "@theme/TabItem";
import Heading from "@theme/Heading";
import Translate from "@docusaurus/Translate";
Requests additional permissions for the player's session. The response depends on the type of permission requested:
- **`PLAYER`-managed permissions** are enabled immediately. The response includes the updated session with `status: "PASS"`.
- **`GUARDIAN`-managed permissions** create a `CHALLENGE_SESSION_UPGRADE` challenge for the trusted adult to complete.
- **Permissions with `verifiedAgeThreshold`** create a `CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE` challenge. The player must verify their age through AgeKit+ rather than parental consent. All requested permissions must either all have a `verifiedAgeThreshold` or none of them can — mixing the two types in a single request returns a 400 error.
Once age verification is recorded on the session (via a verified platform signal or a completed age assurance challenge), subsequent upgrade requests for permissions with the same or lower threshold can be satisfied immediately without a new challenge.
Request
---
// File: api/error-handling
# Error handling
This guide covers error handling for the k-ID API, including common error codes, error response formats, and best practices for handling errors.
## Error response format
All API errors follow a consistent response format. Error responses include an `error` field and an `errorMessage` field:
```json
{
"error": "ERROR_CODE",
"errorMessage": "Human-readable error message"
}
```
## HTTP status codes
The k-ID API uses standard HTTP status codes to indicate the result of API requests:
| Status Code | Description | When It Occurs |
|-------------|-------------|----------------|
| 200 | OK | Request successful |
| 400 | Bad Request | Invalid request parameters |
| 401 | Unauthorized | Invalid or missing API key |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server error |
## Common error codes
### Authentication errors
#### UNAUTHORIZED
**Status:** 401
**Description:** Invalid or missing API key
```json
{
"error": "UNAUTHORIZED",
"errorMessage": "Unauthorized"
}
```
**Common causes:**
- Missing Authorization header
- Invalid API key format
- Expired API key
**Resolution:**
- Verify API key is correct
- Check if key is active and not expired
- Ensure key is properly formatted in Authorization header
### Request validation errors
#### `INVALID_INPUT`
**Status:** 400
**Description:** Request validation failed
```json
{
"error": "INVALID_INPUT",
"errorMessage": "The age verification could not be found."
}
```
**Resolution:**
- Review validation error details
- Correct the invalid field values
- Refer to API documentation for valid values
### Resource errors
### Rate limiting errors
**Status:** 429
**Description:** Rate limit exceeded
**No Response Body**
**Resolution:**
- Implement exponential backoff with jitter
- Reduce request frequency and cache responses when appropriate
- See [Rate limits](/api/rate-limits) for the default per-mode limits and how to request an increase
### Server errors
**Status:** 500
**Description:** Internal server error
```json
{
"error": "INTERNAL_ERROR",
"errorMessage": "Internal server error"
}
```
**Resolution:**
- Retry the request after a delay
- Contact support if error persists
- Check service status page
## Next steps
- **[Rate limits](/api/rate-limits)** - Default per-mode rate limits and how to request an increase
- **[API endpoints](/api/endpoints/perform-access-age-verification)** - Learn about available endpoints
- **[Authentication](/api/authentication)** - Learn about API authentication
---
// File: api/overview
# API overview
The k-ID API is an HTTP RPC-style web API for interacting with k-ID. It provides methods for initiating Verifiable Parental Consent (VPC), initiating age verification, and getting enabled permissions for a player.
## API structure
The k-ID API is a collection of HTTP RPC-style methods. All URLs are in the form `https://{host}/api/v1/{method}`. While it's not a REST API, those familiar with REST should be at home with its foundations in HTTP. All API methods use the GET or POST HTTP methods, depending on whether the API has side effects.
Arguments are passed as query parameters for GET methods, and as JSON in the request body for POST methods. All responses are in JSON format.
## Base URLs
**Live Mode:**
```
https://game-api.k-id.com/api/v1/
```
**Test Mode:**
```
https://game-api.test.k-id.com/api/v1/
```
## Endpoints
### Age gate
| Endpoint | Description |
|----------|-------------|
| [`/age-gate/get-requirements`](/api/endpoints/get-age-gate-requirements) | Get age gate requirements for a jurisdiction |
| [`/age-gate/check-age-category`](/api/endpoints/check-age-category) | Check age category for a player |
| [`/age-gate/check`](/api/endpoints/check-age-gate) | Check age and create or update session |
| [`/age-gate/get-default-permissions`](/api/endpoints/get-default-permissions) | Get default permissions for a jurisdiction |
### Sessions
| Endpoint | Description |
|----------|-------------|
| [`/session/get`](/api/endpoints/get-session) | Get session by session ID or `kuid` |
| [`/session/upgrade`](/api/endpoints/upgrade-session) | Upgrade session permissions |
| [`/session/update-jurisdiction`](/api/endpoints/update-jurisdiction) | Update session jurisdiction |
| [`/session/set-guardian-managed-session-permissions`](/api/endpoints/set-guardian-managed-session-permissions) | Set guardian-managed session permissions |
### Challenges
| Endpoint | Description |
|----------|-------------|
| [`/challenge/get`](/api/endpoints/get-challenge) | Get challenge details |
| [`/challenge/get-status`](/api/endpoints/get-challenge-status) | Get challenge status |
| [`/challenge/send-email`](/api/endpoints/send-email) | Send challenge email notification |
| [`/challenge/generate-otp`](/api/endpoints/generate-otp) | Generate one-time password for challenge |
### Age verification
| Endpoint | Description |
|----------|-------------|
| [`/age-verification/perform-facial-age-estimation`](/api/endpoints/perform-facial-age-estimation) | Perform facial age estimation verification |
| [`/age-verification/perform-id-verification`](/api/endpoints/perform-id-verification) | Perform ID document verification |
| [`/age-verification/perform-age-key-verification`](/api/endpoints/perform-age-key-verification) | Perform AgeKey verification |
| [`/age-verification/perform-connect-id-verification`](/api/endpoints/perform-connect-id-verification) | Perform ConnectID verification |
| [`/age-verification/perform-credit-card-verification`](/api/endpoints/perform-credit-card-verification) | Perform credit card verification |
| [`/age-verification/perform-trusted-adult-verification`](/api/endpoints/perform-trusted-adult-verification) | Perform trusted adult verification |
| [`/age-verification/perform-access-age-verification`](/api/endpoints/perform-access-age-verification) | Perform access age verification with all methods |
| [`/age-verification/perform-age-appeal`](/api/endpoints/perform-age-appeal) | Perform age appeal verification |
| [`/age-verification/perform-custom-age-verification`](/api/endpoints/perform-custom-age-verification) | Perform custom age verification |
| [`/age-verification/get-status`](/api/endpoints/get-age-verification-status) | Get age verification status |
### Widgets
| Endpoint | Description |
|----------|-------------|
| [`/widget/generate-age-gate-url`](/api/endpoints/generate-age-gate-url) | Generate age gate widget URL |
| [`/widget/generate-direct-notices-url`](/api/endpoints/generate-direct-notices-url) | Generate data notices widget URL |
| [`/widget/generate-manage-session-permissions-url`](/api/endpoints/generate-manage-session-permissions-url) | Generate session permissions management widget URL |
| [`/widget/generate-e2e-url`](/api/endpoints/generate-e-2-eurl) | Generate end-to-end widget URL |
### Test
| Endpoint | Description |
|----------|-------------|
| [`/test/set-challenge-status`](/api/endpoints/set-challenge-status) | Set challenge status (test only) |
| [`/test/set-age-verification-status`](/api/endpoints/set-age-verification-status) | Set age verification status (test only) |
---
// File: api/rate-limits
# Rate limits
The k-ID platform enforces two independent rate limits:
- **API rate limits** apply to direct calls to the k-ID API from your servers. When exceeded, requests fail with HTTP `429 Too Many Requests`.
- **Age verification and parental consent flow rate limits** apply to user-facing flows (age verification widgets and VPC challenges). When exceeded, the user sees an in-flow error asking them to wait and try again.
Both limits differ between **live mode** and **test mode**, and both are enforced per product.
## API rate limits
Calls to the [k-ID API](/api/overview) are rate-limited per product. Requests that exceed the limit receive an HTTP `429` response with no response body.
| Mode | Default limit |
|------|---------------|
| Live mode | 500 RPS |
| Test mode | 10 RPS |
### Handling 429 responses
When you receive a `429`, your integration should:
- Stop sending new requests for a short cool-down period.
- Retry with **exponential backoff** plus jitter, rather than a tight retry loop.
- Cache responses where possible (for example, sessions and age gate requirements).
- Coalesce duplicate concurrent requests for the same resource.
See [Error handling](/api/error-handling) for the full list of HTTP status codes and error formats.
## Age verification and parental consent flow rate limits
User-facing age verification and parental consent flows, including hosted widget URLs and challenge flows, are rate-limited separately from direct API calls. These limits are also enforced per product.
| Mode | Default limit |
|------|---------------|
| Live mode | 100 RPS |
| Test mode | 20 RPS |
When this limit is exceeded, the user is shown an in-flow error asking them to wait and try again. No `429` is surfaced to your server. This limit is independent of, and additive to, the API rate limit described in the previous section: a single user journey can consume capacity from both buckets.
## Default limits and requesting an increase
:::info Need higher limits?
The numbers on this page are the **default** rate limits assigned to every product. If your product needs higher capacity (for example, for a launch, a marketing campaign, or sustained higher traffic), contact your k-ID representative to request an increase.
:::
## Next steps
- [Error handling](/api/error-handling): error response formats and status codes.
- [Authentication](/api/authentication): keep test and live API keys correctly scoped to their environments.
---
// File: cdk/custom-workflow
# Custom workflow
The k-ID custom workflow for age gate and Verifiable Parental Consent (VPC) involves multiple steps that determine whether a player can access your game and what permissions they have.
:::tip Recommended for mobile apps
The custom workflow is the recommended approach for mobile apps: building the age gate and consent UX elements natively, following the [CDK UX guidelines](/cdk/ux-guidelines), gives players the most seamless, brand-integrated experience. The [age gate and end-to-end widgets](/cdk/embedded-flow) are also fully supported on mobile if you prefer a faster integration. See the [mobile apps guide](/get-started/quickstart-guides/mobile-apps) for how to display hosted k-ID URLs and receive results.
:::
## Workflow overview
The flow chart below shows a view of the k-ID Engine workflow. API calls are identified at the points in the workflow where they're used.

> **Note**: If you have a **`kuid`** for a user you can use that to look up if they have a session for a game.
## Workflow steps
### 1. Get age gate requirements
Call [`/age-gate/get-requirements`](/api/endpoints/get-age-gate-requirements) with the player's jurisdiction to determine:
- Whether an age gate should be displayed
- What age collection methods are allowed
- Age thresholds (digital consent age, civil age, minimum age)
- Whether age assurance is required
### 2. Collect age (if required)
If an age gate is required (`shouldDisplay` = `true`), collect the player's age with the approved methods for the jurisdiction.
### 3. Check age for access
Call [`/age-gate/check`](/api/endpoints/check-age-gate) with the player's date of birth and jurisdiction to determine the next step:
- **`PROHIBITED`**: Player is below the minimum age - block access
- **`CHALLENGE`**: Player requires parental consent - create a challenge
- **`PASS`**: Player can continue - create or return a session
### 4. Handle consent challenge (if required)
If a challenge is created:
- Display the challenge to the player (QR code, OTP, email input)
- Notify the trusted adult (via email if provided)
- Wait for consent (via webhooks or polling)
- Process the result (grant access if consent is granted)
### 5. Get session
Once consent is granted or if consent isn't required, retrieve the session by calling [`/session/get`](/api/endpoints/get-session) to get the player's permissions and age status.
### 6. Use permissions
Use the session permissions to control access to features in your game based on the `enabled` and `managedBy` fields for each permission.
## Account system integration
Some studios have a common Account system that spans across multiple games. It's often desirable in these situations to integrate an age gate and to handle Verifiable Parental Consent (VPC) directly in the Account creation flow so that players are asked for their age only once for all games from the same publisher.
When doing this type of integration, there are a few important considerations:
### Product context for VPC
If the games all require an Account to play, then VPC can be triggered from the Account creation process. However, the request for consent must be specific to a game so that a parent knows what they're consenting to. The k-ID API determines what Product should be presented to the parent during the consent process based on what API key is used when triggering VPC.
In practice, the integration of k-ID into an Account system must be able to determine at the time of Account setup what game triggered the Account creation process, and map that to a k-ID API key.
### Using a `kuid` to access k-ID sessions
k-ID exposes a global identifier for all players that have had some form of trusted adult consent called `kuid` or k-ID User ID. The `kuid` is returned as a property of the `Session` object. When integrating with an Account system across multiple games, the `kuid` should be associated with the player's identity in the Account system when present in a `Session`. This allows retrieval of a k-ID Session, if it exists, for any k-ID Product by calling [`/session/get`](/api/endpoints/get-session) providing only the `kuid` as a parameter and using the appropriate API key for the correct Product.
### Caching sessions for multiple products
When integrating with an Account system, Sessions from multiple Products are cached. `Session` objects can all be cached as a map in the Account system by using the k-ID Product ID as a key. When a player attempts to play a new game, the map containing `Session` objects can be queried by Product ID, and if a `Session` is already present, the player can then be allowed to continue without further trusted adult consent.
### Account-level product
When configuring an Account System integration of k-ID, it's typical to create one k-ID Product for each game, and then a separate k-ID Product for the Account System itself in the [Compliance Studio](/compliance-studio/creating-product). This is useful to represent any permissions and disclosures that are common or global for all games, or the Account itself.
When a Product is mapped to the Account system, this means that for each game, there are two `Session` objects retrieved, one for the k-ID Product mapped to the game, and one for the k-ID Product mapped to the Account system. The k-ID API is scoped to a Product by the API key. The correct API key must be used depending on whether you are trying to access the k-ID Product mapped to the Account system or the game.
---
// File: cdk/overview
# Compliance Development Kit (CDK)
The k-ID Compliance Development Kit (CDK) is an enterprise-grade compliance framework that automatically manages regulatory logic and compliance settings. It intelligently determines which features and content are accessible to players based on their age, jurisdiction, and parental consent status. Leveraging global compliance data from k-ID's Regulatory Hub, CDK enables game and app developers to deliver compliant experiences across different ages and jurisdictions.
## What's the CDK?
The CDK is a comprehensive compliance solution that encapsulates all of the functionality needed for age gate, Verifiable Parental Consent (VPC), sessions, permissions, data notices, and trusted adult preferences. All compliance policies are configured through the [Compliance Studio](/compliance-studio/creating-product), where you define and manage your product's regulatory requirements.
The core value of CDK is its ability to:
- **Maintain compliance logic**: Automatically determine which features and content can be offered to different age groups based on jurisdiction requirements
- **Determine consent requirements**: Identify which players need parental consent based on their age and jurisdiction
- **Manage permissions**: Control which features are enabled or disabled for each player based on age, jurisdiction, and parental consent
- **Handle data notices**: Determine what data notices must be displayed based on jurisdiction requirements
All of this logic is maintained by CDK with global compliance data from k-ID's Regulatory Hub, ensuring your application stays compliant as regulations evolve.
## Integration approaches
CDK supports two integration approaches that allow you to use its compliance logic:
### Widgets
Use pre-built widgets that handle the complete compliance flow. Perfect for quick integration with minimal code changes.
- Hosted widget URL: present in a web iframe / pop-up / redirect, a mobile system browser surface, or a QR-to-mobile handoff on consoles
- Automatic flow handling based on product configuration
- Customizable branding
- See [Embedded flow](/cdk/embedded-flow) for details
### Custom UX workflows
Build completely custom compliance experiences with the k-ID API. Full control over the user interface and workflow while still leveraging CDK's compliance logic.
- Complete UI/UX control
- Custom workflow logic
- Advanced scenarios
- See [Custom workflow](/cdk/custom-workflow) for details
:::tip Mobile apps
Both approaches are fully supported on mobile. For the age gate and consent steps, we encourage the custom UX workflow: building these elements natively, following the [CDK UX guidelines](/cdk/ux-guidelines), gives players the most seamless, brand-integrated experience. The widgets, age verification URLs, and age assurance URLs all work on mobile through system browser surfaces with a `redirectUrl` callback. See the [mobile apps guide](/get-started/quickstart-guides/mobile-apps) for the display methods and result handling.
:::
## How it works
1. **Configure in Compliance Studio**: Set up your product's compliance requirements, permissions, data notices, and verification methods in the [Compliance Studio](/compliance-studio/creating-product)
2. **CDK maintains compliance logic**: CDK automatically accesses k-ID's Regulatory Hub to:
- Determine jurisdiction-specific age requirements
- Identify which players need parental consent
- Calculate which permissions can be granted based on age and jurisdiction
- Determine what data notices must be displayed
3. **Integrate into your application**: Choose either widgets or custom UX workflows to integrate CDK's compliance logic
4. **Automatic compliance**: CDK automatically determines what features and content are allowed for each player, ensuring compliance without additional code
## Available widgets and APIs
CDK provides widgets and APIs that handle specific compliance workflows:
| Flow | Description | API Endpoint |
|-----------|-------------|--------------|
| **End-to-End** | Handles the complete Verifiable Parental Consent (VPC) flow within a single widget, including age collection, verification, and consent completion. Streamlines the entire process in one embedded component. | [`POST /widget/generate-e2e-url`](/api/endpoints/generate-e-2-eurl) |
| **Age Gate** | Collects user age using jurisdiction-appropriate methods and handles the complete flow from age collection through trusted adult consent if needed. Emits events for challenges, results (PASS/FAIL/PROHIBITED), and data lite mode navigation. | [`POST /widget/generate-age-gate-url`](/api/endpoints/generate-age-gate-url) |
| **Data Notices** | Displays product data notices and essential permissions, collecting user consent. Shows jurisdiction-appropriate disclosures and handles consent acceptance workflow. | [`POST /widget/generate-direct-notices-url`](/api/endpoints/generate-direct-notices-url) |
| **Permission Management** | Allows users and parents to view and update permission settings for an active session. Enables granular control over feature permissions and data usage preferences. | [`POST /widget/generate-manage-session-permissions-url`](/api/endpoints/generate-manage-session-permissions-url) |
## Key benefits
- **Automatic compliance logic**: CDK maintains all compliance settings and logic to determine what's allowed for each player
- **Global compliance data**: Uses k-ID's Regulatory Hub to stay up-to-date with jurisdiction requirements
- **Flexible integration**: Choose between widgets or custom UX workflows
- **Centralized configuration**: All compliance settings configured in the Compliance Studio
- **Real-time updates**: Compliance logic updates automatically without code changes as regulations evolve
## Getting started
- **Quick start**: See the [VPC quick start guide](/get-started/quickstart-guides/vpc)
- **Widgets**: Learn about [VPC with widgets](/cdk/embedded-flow)
- **Custom workflows**: Learn about [custom age gate and VPC workflows](/cdk/custom-workflow)
- **API reference**: Explore the [Age gate and VPC API endpoints](/api/endpoints/get-age-gate-requirements)
---
// File: cdk/age-gate
# Age gate
An age gate is a mechanism used to collect and verify a user's age before allowing access to age-restricted content, features, or services. The k-ID API provides endpoints for managing age gates and determining what actions are required based on the player's age and jurisdiction.
:::tip Building a custom age gate UI?
For design recommendations on age sliders, date pickers, consent flows, and accessibility, see the [UX guidelines](/cdk/ux-guidelines).
:::
## Getting age gate requirements
Call [`/age-gate/get-requirements`](/api/endpoints/get-age-gate-requirements) with the player's jurisdiction to determine:
- Whether an age gate should be displayed (`shouldDisplay`)
- What age collection methods are approved (`approvedAgeCollectionMethods`)
- Age thresholds:
- `digitalConsentAge`: The minimum age at which a player can provide digital consent
- `civilAge`: The civil/contract age at which a player is considered a legal adult
- `minimumAge`: The minimum age required to access the platform/game
- Whether age assurance is required (`ageAssuranceRequired`)
### Example request
```json
GET /api/v1/age-gate/get-requirements?jurisdiction=US-CA
Authorization: Bearer your-api-key
```
### Example response
```json
{
"shouldDisplay": true,
"ageAssuranceRequired": true,
"digitalConsentAge": 13,
"civilAge": 18,
"minimumAge": 0,
"approvedAgeCollectionMethods": [
"date-of-birth",
"age-slider",
"platform-account"
]
}
```
### Including a platform age signal
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`, and `platformVerificationId`. k-ID factors the signal into `shouldDisplay` and `ageAssuranceRequired` before you collect any input: a verified adult signal lets you skip the age gate and immediately satisfy verified-age permissions. See [Platform age signals](/cdk/age-signals/overview) for the full list of supported platforms and field shapes.
```json
GET /api/v1/age-gate/get-requirements?jurisdiction=US-CA&platformName=apple-ios&platformAgeLow=18&platformAgeHigh=25&platformDeclarationType=governmentIDChecked
Authorization: Bearer your-api-key
```
## Checking age for access
After collecting the player's age, call [`/age-gate/check`](/api/endpoints/check-age-gate) with the date of birth and jurisdiction to determine the next step:
- **`PROHIBITED`**: The player's age is below the minimum age for the game. The player should be blocked from continuing.
- **`CHALLENGE`**: The player must complete a challenge before a session can be created. Inspect `challenge.type` to determine which challenge was returned:
- `CHALLENGE_PARENTAL_CONSENT`: the claimed age is too young for the player to proceed without parental consent. A trusted adult must approve before the player can get a session.
- `CHALLENGE_AGE_GATE_AGE_ASSURANCE`: the claimed age is old enough for the player to proceed without parental consent, but the product has [Automatic age assurance](#automatic-age-assurance) enabled and the player must prove the claimed age (typically with a face scan or ID document) before a session is issued.
- **`PASS`**: The player can continue into the game and a session is returned on the response.
### Example request
```json
POST /api/v1/age-gate/check
Content-Type: application/json
Authorization: Bearer your-api-key
{
"jurisdiction": "US-CA",
"dateOfBirth": "2015-04-15"
}
```
### Example `CHALLENGE` response
```json
{
"status": "CHALLENGE",
"challenge": {
"challengeId": "683409f1-2930-4132-89ad-827462eed9af",
"oneTimePassword": "ABC123",
"type": "CHALLENGE_PARENTAL_CONSENT",
"url": "https://family.k-id.com/authorize?otp=ABC123"
}
}
```
### Example `PASS` response
```json
{
"status": "PASS",
"session": {
"sessionId": "608616da-4fd2-4742-82bf-ec1d4ffd8187",
"ageStatus": "LEGAL_ADULT",
"dateOfBirth": "2005-04-15",
"jurisdiction": "US-CA",
"permissions": [...],
"status": "ACTIVE"
}
}
```
### Including a platform age signal
You can include a `platformAgeSignal` object in the request body, either on its own or alongside `dateOfBirth`/`age`/`kuid`. A verified signal can satisfy verified-age permissions without an extra verification step and records an `ageVerification` on the session; an unverified signal still feeds into age-conflict detection and conservative age resolution. See [Platform age signals](/cdk/age-signals/overview) for supported platforms and verified declaration types.
```json
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"
}
}
```
## Automatic age assurance
Automatic age assurance is an optional product configuration that verifies players whose claimed age is high enough to skip parental consent. When enabled, k-ID intercepts what would otherwise be a `PASS` response and returns a `CHALLENGE_AGE_GATE_AGE_ASSURANCE` challenge instead. The player completes facial age estimation or ID document verification themselves (no trusted adult is involved), and k-ID creates the session after verification passes.
### Enabling the feature
Enabling Automatic age assurance is a two-step process:
1. An organization-level setting (`allowAutomaticAgeAssurance`) must be granted by k-ID. This setting is off by default and can only be toggled by k-ID. Contact k-ID support to enable it for your organization.
2. Once the setting is on, a product administrator can turn Automatic age assurance on or off per jurisdiction from the product's engine overrides in the Compliance Studio.
### When it triggers
`/age-gate/check` returns a `CHALLENGE_AGE_GATE_AGE_ASSURANCE` challenge when all of the following are true:
1. Automatic age assurance is enabled for the product in the target jurisdiction.
2. The player's claimed age is at or over the age at which the player could otherwise access the product without parental consent. This is usually the jurisdiction's digital consent age, but it can be raised by a higher `minimum-age` configured on the product or by essential permission thresholds.
3. No trustworthy [platform age signal](/cdk/age-signals/overview) meeting or exceeding the digital consent age was supplied with the request. A verified platform signal (for example `apple-ios` with `declarationType: governmentIDChecked`) already provides sufficient assurance and bypasses the challenge.
For claims that would otherwise require parental consent, the existing `CHALLENGE_PARENTAL_CONSENT` flow is used instead; Automatic age assurance doesn't change the parental consent path.
### Example `CHALLENGE_AGE_GATE_AGE_ASSURANCE` response
```json
{
"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=..."
}
}
```
Unlike `CHALLENGE_PARENTAL_CONSENT`, the response doesn't include `oneTimePassword`. The `url` points to a self-service verification page rather than the trusted-adult consent portal: the player is the one completing verification, so the URL carries a signed token and can be presented directly.
### Handling the challenge
1. Detect the challenge type and present `challenge.url` in the surface appropriate for your app: a web iframe with `allow="camera;payment;publickey-credentials-get;publickey-credentials-create"`, a mobile embedded browser (see the [Mobile apps quick start](/get-started/quickstart-guides/mobile-apps)), an in-game browser surface, or a QR-to-mobile handoff on consoles.
2. For iframe / same-window hosts, listen for the [`Verification.Result`](/events/dom-events/event-structures/verification-result) DOM message for responsive UI updates. For mobile or other hosts, pass `options.redirectUrl` in the `/age-gate/check` request and handle the callback in your app.
3. Confirm the outcome server-side via the [`Challenge.StateChange`](/events/webhooks/event-types/challenge-statechange) webhook or by polling [`/challenge/get-status`](/api/endpoints/get-challenge-status). On `PASS`, `data.sessionId` contains the newly created session.
4. Call [`/session/get`](/api/endpoints/get-session) with the `sessionId` to retrieve the full permissions for the player.
For a walkthrough with code samples, see [Handle age-assurance challenges](/get-started/quickstart-guides/custom-age-gate#step-4b-handle-age-assurance-challenges) in the custom age gate quick start guide.
### Redirecting after the challenge
When the challenge runs in a top-level browser context (not an iframe), for example a mobile webview that hands off to k-ID and then back to your app, supply a `redirectUrl` and k-ID navigates the player to it once verification completes. Pass it as `options.playerAgeAssurance.redirectUrl` on `/age-gate/check`. The option is only honored for `CHALLENGE_AGE_GATE_AGE_ASSURANCE` flows and is ignored for `CHALLENGE_PARENTAL_CONSENT` (which already returns through the trusted-adult flow).
The URL accepts `http(s)` and custom-scheme mobile deeplinks (for example `myapp://age-gate/return`). Unsafe schemes are rejected and no redirect occurs. After the player completes or dismisses the challenge, k-ID appends the following query parameters to your URL and navigates to it:
| Parameter | Description |
| --- | --- |
| `challengeId` | The challenge that was completed |
| `productId` | The product the age gate was checked against |
| `sessionId` | The newly created session (present only on `PASS`) |
| `status` | `PASS` or `FAIL` |
Existing query parameters on the redirect URL are preserved.
**Example request:**
```json
POST /api/v1/age-gate/check
Content-Type: application/json
Authorization: Bearer your-api-key
{
"jurisdiction": "US-CA",
"dateOfBirth": "2008-04-15",
"options": {
"playerAgeAssurance": {
"redirectUrl": "https://mygame.com/age-gate/return"
}
}
}
```
After a successful verification the player lands on, for example:
```
https://mygame.com/age-gate/return?challengeId=a1b2c3d4-e5f6-7890-abcd-ef1234567890&productId=42&sessionId=608616da-4fd2-4742-82bf-ec1d4ffd8187&status=PASS
```
You should still confirm the outcome server-side via the [`Challenge.StateChange`](/events/webhooks/event-types/challenge-statechange) webhook or [`/challenge/get-status`](/api/endpoints/get-challenge-status) before granting access. Treat the redirect query parameters as a UX hint, not a trusted result.
## Using platform age category APIs
Some platforms provide APIs that return an age category rather than a specific age or date of birth. For example, [Meta Horizon provides a `GetAgeCategory` API](https://developers.meta.com/horizon/documentation/unity/ps-get-age-category-api/) that returns the categories `CH` (child, ages 10-12), `TN` (teen, ages 13-17), or `AD` (adult, ages 18+).
:::tip Pass the signal directly
For most integrations you can skip the conversion step and send a `platformAgeSignal` directly on `/age-gate/check` (and as query parameters on `/age-gate/get-requirements`). k-ID resolves category-to-range conversion server-side, factors in declaration types for verified signals, and runs age-conflict detection in one call. See [Platform age signals](/cdk/age-signals/overview). The conversion endpoint below is still useful when you need the age range up front to drive your own UI.
:::
When using a platform's age category API, you need to convert the category to an age range for the player's jurisdiction, then use that age range with k-ID's age gate system.
### Example: Using Meta Horizon's `GetAgeCategory` API
Here's a complete example of how to integrate Meta Horizon's age category API with k-ID:
1. **Get the age category from Meta Horizon**
```csharp
// Meta Horizon Unity SDK example
var ageCategory = PlatformService.GetAgeCategory();
// Returns: "CH" (child, ages 10-12), "TN" (teen, ages 13-17), or "AD" (adult, ages 18+)
```
2. **Convert the category to an age range**
```json
POST /api/v1/age-gate/get-platform-age-range
Content-Type: application/json
Authorization: Bearer your-api-key
{
"jurisdiction": "US-CA",
"platform": {
"name": "meta-horizon",
"category": "TN"
}
}
```
**Response:**
```json
{
"ageLow": 13,
"ageHigh": 17
}
```
3. **Use the lowest age with the age gate check**
Use the `ageLow` value (13 in this example) as the `age` parameter when calling `/age-gate/check`:
```json
POST /api/v1/age-gate/check
Content-Type: application/json
Authorization: Bearer your-api-key
{
"jurisdiction": "US-CA",
"age": 13
}
```
This ensures that the platform's verified age category is properly converted to a specific age value that can be used with k-ID's age gate system while maintaining compliance with jurisdiction-specific requirements.
## Date of birth format
For information about date of birth formats and requirements, see [Age Gate](/concepts/access-features-consent/age-gate#date-of-birth-format) in the Core concepts section.
## Default permissions
If [`/age-gate/get-requirements`](/api/endpoints/get-age-gate-requirements) responds with `shouldDisplay` = `false`, then no age gate should be shown and the player's date of birth isn't defined. In this case, the game still creates a `Session` by retrieving default permissions for the jurisdiction by calling [`/age-gate/get-default-permissions`](/api/endpoints/get-default-permissions), which means that permissions don't vary based on age in this jurisdiction. Some features in a game might be prohibited for all age audiences based on jurisdiction, so the game should still consult the `Session` permissions to check whether a feature can be enabled.
---
// File: cdk/age-assurance
# Age assurance for high-risk features
Certain permissions in certain jurisdictions require a verified age before they can be enabled. For example, `loot-boxes-paid-cosmetic-only`, `loot-boxes-paid-gameplay-impacting`, `targeted-ads`, and `profiling` in Brazil (`BR`) require a `verifiedAgeThreshold` of 18. These permissions are disabled by default and can only be unlocked through either a [platform signal](./age-signals/overview.md) considered verified for age requirements or a dedicated age assurance challenge (`CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE`).
## What this page helps you do
Use this page when you need to understand:
- Why a permission is still disabled after the player has already passed the age gate
- Whether `POST /session/upgrade` enables a permission immediately or returns a challenge
- How to handle a player who meets the threshold but hasn't verified age yet
- How to recover when a player originally claimed an age below the threshold
## Verified age threshold
A `verifiedAgeThreshold` means the permission needs a verified age, not just a claimed or inferred age.
Currently, the main example is:
- **Brazil (`BR`)**: `loot-boxes-paid-cosmetic-only`, `loot-boxes-paid-gameplay-impacting`, `targeted-ads`, and `profiling` require a verified age of `18`; `direct-marketing` requires a verified age of `12`
Permissions with a `verifiedAgeThreshold` are:
- Never `managedBy: GUARDIAN`
- `managedBy: PLAYER` when the player is old enough but still needs verification
- `managedBy: PROHIBITED` when the player is below the threshold
:::note
This currently applies to the Brazil permissions in the preceding list. As additional jurisdictions add verified-age requirements, the same permission model applies.
:::
## Permission state logic
When a permission has a `verifiedAgeThreshold`, the `Permission` object includes that field:
```json
{
"name": "loot-boxes-paid-gameplay-impacting",
"enabled": false,
"managedBy": "PLAYER",
"verifiedAgeThreshold": 18
}
```
```mermaid
flowchart TD
Start["Permission has verifiedAgeThreshold"] --> AgeCheck{"Player age below threshold?"}
AgeCheck -->|Yes| Prohibited["managedBy: PROHIBITED enabled: false"]
AgeCheck -->|No| Verified{"Verified age already satisfied?"}
Verified -->|Yes| Enabled["managedBy: PLAYER enabled: true"]
Verified -->|No| Disabled["managedBy: PLAYER enabled: false"]
```
| Player age compared to threshold | Age verified? | `managedBy` | `enabled` |
| --- | --- | --- | --- |
| Below threshold | N/A | `PROHIBITED` | `false` |
| At or exceeding the threshold | No | `PLAYER` | `false` |
| At or exceeding the threshold | Yes, via verified platform signal | `PLAYER` | `true` |
| At or exceeding the threshold | Yes, via age assurance | `PLAYER` | `true` |
Threshold permissions are never `managedBy: GUARDIAN`. Guardian consent can't unlock them.
## `ageVerification` on the session
When a verified platform signal is processed or an age assurance challenge completes, the session stores an `ageVerification` object:
```json
{
"ageVerification": {
"verifiedAge": 18,
"platformName": "apple-ios",
"declarationType": "governmentIDChecked",
"verifiedAt": "2026-03-14T00:00:00Z"
}
}
```
This verification is reusable. Once it's on the session, later `POST /session/upgrade` calls for permissions with the same or lower threshold can usually be satisfied without a new challenge.
## Session upgrade flow
`POST /session/upgrade` is the endpoint that decides whether the player can get the high-risk permission immediately or needs to verify first.
```mermaid
flowchart TD
Request["POST /session/upgrade"] --> Mix{"All requested permissions have verifiedAgeThreshold?"}
Mix -->|Mixed| Error400["Return 400"]
Mix -->|No thresholds| Standard["Use standard guardian-consent flow"]
Mix -->|All thresholds| AgeCheck{"Player age meets threshold?"}
AgeCheck -->|No| Prohibited["Leave permission PROHIBITED No challenge"]
AgeCheck -->|Yes| Platform{"Verified platform signal satisfies threshold?"}
Platform -->|Yes| Immediate["Enable permission Store ageVerification"]
Platform -->|No| Existing{"Existing ageVerification already satisfies threshold?"}
Existing -->|Yes| Immediate
Existing -->|No| Challenge["Return CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE"]
```
## Session upgrade API
**Relevant request fields:**
| Field | Type | Description |
| --- | --- | --- |
| `platformAgeSignal` | PlatformAgeSignal | A platform signal that can satisfy the threshold immediately |
| `options` | object | Options forwarded into the age-assurance experience |
| `options.facialAgeEstimation.passIfOver` | integer | Minimum age at which facial age estimation should pass |
| `options.facialAgeEstimation.failIfUnder` | integer | Maximum age at which facial age estimation should fail |
| `options.redirectUrl` | string | Redirect target after the challenge completes, when it runs in a top-level browser context (not an iframe). Accepts `http(s)` and custom-scheme mobile deeplinks (for example `myapp://upgrade/return`); unsafe schemes are rejected and no redirect occurs. On completion k-ID appends `challengeId`, `productId`, `sessionId` (present only on `PASS`), and `status` (`PASS` or `FAIL`). Confirm the outcome server-side; treat the query parameters as a UX hint. |
**Example request (verified platform signal satisfies the permission):**
```json
POST /api/v1/session/upgrade
{
"sessionId": "608616da-4fd2-4742-82bf-ec1d4ffd8187",
"requestedPermissions": [
{ "name": "loot-boxes-paid-gameplay-impacting" }
],
"platformAgeSignal": {
"name": "apple-ios",
"ageLow": 18,
"ageHigh": 25,
"declarationType": "governmentIDChecked"
}
}
```
```json
{
"session": {
"sessionId": "608616da-4fd2-4742-82bf-ec1d4ffd8187",
"permissions": [
{
"name": "loot-boxes-paid-gameplay-impacting",
"enabled": true,
"managedBy": "PLAYER",
"verifiedAgeThreshold": 18
}
],
"ageVerification": {
"verifiedAge": 18,
"platformName": "apple-ios",
"declarationType": "governmentIDChecked",
"verifiedAt": "2026-03-14T00:00:00Z"
}
}
}
```
**Example request (player meets the threshold but still needs age assurance):**
```json
POST /api/v1/session/upgrade
{
"sessionId": "608616da-4fd2-4742-82bf-ec1d4ffd8187",
"requestedPermissions": [
{ "name": "loot-boxes-paid-gameplay-impacting" }
],
"options": {
"redirectUrl": "https://mygame.com/callback"
}
}
```
```json
{
"session": {
"sessionId": "608616da-4fd2-4742-82bf-ec1d4ffd8187",
"permissions": [
{
"name": "loot-boxes-paid-gameplay-impacting",
"enabled": false,
"managedBy": "PLAYER",
"verifiedAgeThreshold": 18
}
]
},
"challenge": {
"challengeId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE",
"url": "https://family.k-id.com/session/upgrade/age-assurance?token=..."
}
}
```
**Example outcome (player is below the threshold):**
```json
{
"session": {
"sessionId": "608616da-4fd2-4742-82bf-ec1d4ffd8187",
"permissions": [
{
"name": "loot-boxes-paid-gameplay-impacting",
"enabled": false,
"managedBy": "PROHIBITED",
"verifiedAgeThreshold": 18
}
]
}
}
```
In this case, no challenge is returned because the player isn't old enough for the permission at all.
**Key behaviors:**
- All requested permissions must either all have a `verifiedAgeThreshold` or none of them can
- If the player is below the threshold, the permission stays `PROHIBITED` and no challenge is issued
- A verified platform signal can satisfy some or all requested permissions immediately
- An existing `ageVerification` on the session can also satisfy the request
- Only the remaining unresolved permissions turn into an age-assurance challenge
## Handling the challenge result
The challenge URL points to `https://family.k-id.com/session/upgrade/age-assurance?token=...`.
The URL's expiry is encoded as the `exp` claim of the JWT in the `token` query parameter. In live mode, challenge URLs are valid for **2 weeks**. In test mode, they're valid for only **7 minutes** so you can exercise expired-URL handling without waiting. For more, see [Challenge URL and email link expiration](/concepts/access-features-consent/challenges#challenge-url-and-email-link-expiration).
When the player opens this URL:
1. The token is parsed and validated.
2. If the challenge is already resolved, the player is redirected to the completion page.
3. If still pending, a verification iframe is shown with k-ID's age verification methods (such as facial age estimation or ID document scan).
4. On completion, the result is communicated back to your integration.
You can receive that result in three ways:
### Option 1: Listen for `postMessage`
For iframe or webview integrations, the family portal posts a `Challenge.StateChange` message to the parent window:
```json
{
"eventType": "Challenge.StateChange",
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"productId": "your-product-id",
"status": "PASS",
"type": "CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE"
}
}
```
:::warning
DOM `postMessage` events are useful for updating your UI, but they aren't authoritative. Use a webhook or the polling endpoint for any server-side logic.
:::
### Option 2: Poll `GET /challenge/get-status`
```text
GET /api/v1/challenge/get-status?challengeId=a1b2c3d4-e5f6-7890-abcd-ef1234567890
```
Possible statuses are `PASS`, `FAIL`, `PENDING`, and `IN_PROGRESS`.
### Option 3: Receive a `Challenge.StateChange` webhook
k-ID posts a [`Challenge.StateChange`](/events/webhooks/event-types/challenge-statechange) webhook event to your configured endpoint whenever the challenge status changes:
```json
{
"eventType": "Challenge.StateChange",
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"productId": 11472,
"status": "PASS",
"type": "CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE",
"sessionId": "608616da-4fd2-4742-82bf-ec1d4ffd8187"
}
}
```
| Field | Description |
| --- | --- |
| `data.id` | The `challengeId` returned by `POST /session/upgrade` |
| `data.status` | `PASS`, `FAIL`, or `IN_PROGRESS` |
| `data.type` | The challenge type (for example, `CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE`) |
| `data.sessionId` | Present on `PASS`: use this to fetch the refreshed session |
Webhooks are the recommended approach for server-side handling because they're delivered in real time and don't require polling. For the full field reference and status lifecycle, see [`Challenge.StateChange`](/events/webhooks/event-types/challenge-statechange).
### After the result
- After `PASS`, fetch `GET /session/get` and use the refreshed permissions. The threshold permission should now be enabled and the session should contain `ageVerification`.
- After `FAIL`, the permission remains disabled. The player can retry later by calling `POST /session/upgrade` again.
## Recovery flow for a player who entered an age below the threshold
Sometimes a player enters a claimed age below the threshold for a high-risk permission and ends up with a `PROHIBITED` permission. In that case, the normal `session/upgrade` flow can't fix the problem because the session already says the player is too young for that feature.
This recovery flow is for:
- Player-managed sessions
- Guardian-managed sessions that don't yet have an approver linked
It's **not** the right flow when the session is guardian-managed and already has an approver.
```mermaid
flowchart TB
Start["Permission is PROHIBITED because claimed age is below threshold"] --> SessionType{"Session type?"}
SessionType -->|Player-managed| Appeal["Run age appeal"]
SessionType -->|Guardian-managed, no approver| Appeal
SessionType -->|Guardian-managed, approver linked| Standard["Use the standard guardian flow"]
Appeal --> Result{"Age appeal result?"}
Result -->|PASS| Revoke["Invalidate the session"]
Revoke --> Restart["Restart age gate with a `k-id` platform signal"]
Result -->|FAIL| Denied["Permission remains prohibited Session is preserved"]
```
### Recovery steps
1. **Direct the player to Age Appeal on AgeKit+.** Call [`POST /age-verification/perform-age-appeal`](/api/endpoints/perform-age-appeal) with the appropriate `jurisdiction` and `criteria`. Present the returned URL in the surface appropriate for your app (web iframe / pop-up / redirect, mobile embedded browser, in-game browser surface, or QR-to-mobile handoff on consoles) for the player to complete age verification. See the [Mobile apps quick start](/get-started/quickstart-guides/mobile-apps) for mobile details. Don't invalidate the session yet: if the appeal fails, the player keeps their existing session and won't need to go through VPC again.
2. **Handle the age appeal result.** Listen for the result via DOM events, webhooks, or [`GET /age-verification/get-status`](/api/endpoints/get-age-verification-status).
- **On success:** The appeal produces a `verificationId` that proves the player's verified age.
- **On failure:** The feature remains prohibited. The player stays at their originally claimed age, and the existing session is preserved.
3. **Invalidate the session.** After a successful appeal, discard the `sessionId` on your side so the player can start fresh. A session invalidation endpoint is planned for a future release; for now, removing the session client-side is sufficient.
4. **Restart the age gate flow with the k-ID signal.** Send the `verificationId` from the appeal as a `k-id` platform signal.
```json
POST /api/v1/age-gate/check
{
"jurisdiction": "BR",
"platformAgeSignal": {
"name": "k-id",
"verificationId": ""
}
}
```
k-ID resolves the verified age from the `verificationId`. If the verified age meets or exceeds the `verifiedAgeThreshold`, the session is created with the high-risk permission enabled.
## Sequential permission upgrades
Each threshold permission is tracked independently:
- Upgrading one permission doesn't automatically upgrade another
- Each permission still needs to be requested through `POST /session/upgrade`
- Once the session has an `ageVerification`, later permissions with the same or lower threshold can often be satisfied without another challenge
---
// File: cdk/challenges
# Challenges
When a player's age requires Verifiable Parental Consent (VPC), k-ID creates a consent challenge that must be approved by a trusted adult before the player can access the game or feature.
## Challenge creation
Challenges are created automatically when calling [`/age-gate/check`](/api/endpoints/check-age-gate) with a date of birth that requires parental consent. The challenge information is returned in the response:
```json
{
"status": "CHALLENGE",
"challenge": {
"challengeId": "683409f1-2930-4132-89ad-827462eed9af",
"oneTimePassword": "ABC123",
"type": "CHALLENGE_PARENTAL_CONSENT",
"url": "https://family.k-id.com/authorize?otp=ABC123"
}
}
```
## Challenge information
When a challenge is created, you receive:
- **`challengeId`**: A unique identifier for the challenge (store this for later use)
- **`oneTimePassword`**: A password that can be entered by a parent to access the consent portal
- **`url`**: A URL that can be rendered as a QR code for easy mobile access
## Displaying the challenge
For information about displaying challenges and what information to show, see [Challenges](/concepts/access-features-consent/challenges#challenge-information) in the Core concepts section.
## Notifying trusted adults
There are several ways to notify trusted adults about a consent challenge. For detailed information about notification methods, see [Challenges](/concepts/access-features-consent/challenges#notifying-trusted-adults) in the Core concepts section.
### Email notification
If an email address for a parent or guardian is provided by the player, call [`/challenge/send-email`](/api/endpoints/send-email) with the challenge ID and email address:
```json
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"
}
```
Alternatively, you can call [`/challenge/send-email`](/api/endpoints/send-email) without specifying an email address, and the API sends an email to the trusted adult who most recently approved a permission for the player. If no associated email address is found, the API responds with an `INVALID_EMAIL` error code.
## Checking challenge status
After showing the consent challenge, wait for the trusted adult to complete the consent process. You can check the challenge status in two ways:
### Webhooks (recommended)
For detailed information about the webhook event structure, see [`Challenge.StateChange`](/events/webhooks/event-types/challenge-statechange).
Configure a webhook endpoint to receive [`Challenge.StateChange`](/events/webhooks/event-types/challenge-statechange) events. For more information, see [Webhooks](/webhooks).
### Polling
Poll the [`/challenge/get-status`](/api/endpoints/get-challenge-status) API periodically with the challenge ID:
```json
GET /api/v1/challenge/get-status?challengeId=683409f1-2930-4132-89ad-827462eed9af
Authorization: Bearer your-api-key
```
The challenge status can be:
- **`PASS`**: Consent has been granted by a trusted adult
- **`FAIL`**: The request was denied
- **`PENDING`**: The challenge is still waiting for a response
- **`POLL_TIMEOUT`**: The polling timeout has been reached (when using polling with timeout)
### Example `PASS` response
```json
{
"status": "PASS",
"type": "CHALLENGE_PARENTAL_CONSENT",
"sessionId": "0ad1641f-c154-4cc2-8bb2-74dbd0de7723",
"approverEmail": "parent@example.com"
}
```
## Waiting for consent
There might be many calls made to the [`/challenge/get-status`](/api/endpoints/get-challenge-status) API during this time. Between calls to [`/challenge/get-status`](/api/endpoints/get-challenge-status), there should be a minimum of 5 seconds delay. Also, [`/challenge/get-status`](/api/endpoints/get-challenge-status) can return HTTP code 429. The game should implement appropriate retry logic when handling 429 responses.
For more information about waiting for consent and how long to wait, see [Challenges](/concepts/access-features-consent/challenges#waiting-for-consent) in the Core concepts section.
## Pending consent challenges
When a status of `CHALLENGE` is returned from [`/age-gate/check`](/api/endpoints/check-age-gate), the returned challenge ID should be stored in local storage while the challenge is active. The presence of an active challenge when the game starts directs the game to show the same consent challenge window from before. After retrieving the challenge ID from local storage, the [`/challenge/get`](/api/endpoints/get-challenge) API should be invoked to retrieve information about the current challenge, including the one time password and QR code URL, and the challenge window should again be displayed to the user.
For more information, see [Challenges](/concepts/access-features-consent/challenges#pending-consent-challenges) in the Core concepts section.
## Challenge expiration
Although the consent challenge itself doesn't expire, the generated time-based authentication methods do (for example, OTP, email link). For more detail on how k-ID uses time-based authentication within user flows, see [Challenges](/concepts/access-features-consent/challenges#challenge-expiration-and-time-based-authentication).
## Getting the trusted adult email address
If consent is granted, the email address of the parent is returned in the `approverEmail` field in the response from [`/challenge/get-status`](/api/endpoints/get-challenge-status). This can be stored by the game for use in future customer service cases.
For more information about challenges, including testing challenges, see [Challenges](/concepts/access-features-consent/challenges) in the Core concepts section.
---
// File: cdk/embedded-flow
# Embedded flow
The CDK provides an End-to-End widget that handles the complete Verifiable Parental Consent (VPC) flow within a single interface, covering age gate, VPC, data notices, permissions, and preferences all in one seamless experience.
:::tip Using the widgets on mobile
The end-to-end and age gate widgets are fully supported on mobile. Display the widget URL with the same system browser surfaces used for verification URLs and receive the result through the `redirectUrl` callback. See the [mobile apps guide](/get-started/quickstart-guides/mobile-apps) for the display methods.
For the age gate and consent steps, building the UX natively with the [custom workflow](/cdk/custom-workflow) and the [CDK UX guidelines](/cdk/ux-guidelines) typically delivers the most seamless, brand-integrated player experience. The widget UI works on mobile today and its mobile UX is being continuously optimized.
:::
## What's the end-to-end widget?
The **End-to-End Widget** is a comprehensive solution that handles the complete compliance flow in a single interface, covering age gate, VPC, data notices, permissions, and preferences all in one seamless experience. This widget can be used by parents either on the child's device or on their own device, providing maximum flexibility for the consent process.
## Generating the widget URL
Call the [`/widget/generate-e2e-url`](/api/endpoints/generate-e-2-eurl) API to create an end-to-end widget URL that handles the complete VPC flow. This returns a unique URL for users to complete the age collection and parental consent process.
### Example request
```json
POST /api/v1/widget/generate-e2e-url
Content-Type: application/json
Authorization: Bearer your-api-key
{
"jurisdiction": "US-CA"
}
```
### Configuration flags
The optional `flags` parameter allows you to customize which parts of the flow to skip:
- `skipDataNotices`: Skip data notices and consent collection
- `skipVerification`: Skip verification step
- `skipPermissions`: Skip permission management
- `skipPreferences`: Skip preference settings
### Pass a platform age signal (optional)
If your game already has age data from the platform (Apple iOS, Google Play, Xbox, Meta Horizon, or a prior k-ID verification), include it as `platformAgeSignal` in the request body. The widget forwards the signal to the underlying age-gate check so it can skip the age gate when a verified signal indicates an adult, satisfy verified-age permissions without an extra verification step, and detect conflicts between the signal and the player's self-reported age.
```json
POST /api/v1/widget/generate-e2e-url
Content-Type: application/json
Authorization: Bearer your-api-key
{
"jurisdiction": "US-CA",
"platformAgeSignal": {
"name": "apple-ios",
"ageLow": 18,
"ageHigh": 25,
"declarationType": "governmentIDChecked"
}
}
```
For the supported platforms and field shapes, see [Platform age signals](./age-signals/overview.md).
### Example response
```json
{
"id": "7854909b-9124-4bed-9282-24b44c4a3c97",
"url": "https://family.k-id.com/widget?token=eyJhbGciOiJFUzM4NCIs..."
}
```
## Presenting the widget URL {#present-widget}
The widget URL is a hosted web page. Open it in the surface that fits your application:
- **Web app**: embed in an iframe (example below), open as a pop-up, or redirect to it as a full page.
- **Mobile app**: open the widget URL in a system browser surface (Custom Tabs on Android, ASWebAuthenticationSession on iOS) and pass `options.redirectUrl` to receive the result as a deep link back into your app. See the [Mobile apps quick start](/get-started/quickstart-guides/mobile-apps) for the display methods. For the most brand-integrated experience, consider building the age gate and consent UX natively with the [custom workflow](/cdk/custom-workflow) instead.
- **Console** (Switch, PlayStation, Xbox): console browsers are typically restricted or absent. Display the widget URL as a QR code so the player completes the flow on a paired mobile device. Receive results via webhook plus [`/session/get`](/api/endpoints/get-session) polling, since the mobile-device redirect can't return to the console.
The available methods inside the widget automatically adapt to jurisdictional requirements regardless of host.
### Web example
```html
```
For mobile surfaces, pass `options.redirectUrl` when calling [`/widget/generate-e2e-url`](/api/endpoints/generate-e-2-eurl) and handle the callback in your app. See the [Mobile apps quick start](/get-started/quickstart-guides/mobile-apps) for end-to-end examples.
## Handling events {#handling-events}
:::note Where DOM events reach you
The JavaScript events below (`Widget.AgeGate.Result`, `Widget.AgeGate.Challenge`, `Widget.ExitReview`) are delivered via `postMessage`. To receive them, your app needs a live JavaScript listener for the widget `window`. That covers iframes, pop-ups opened via `window.open`, and mobile `WebView` / `WKWebView` with a JS bridge (see the [Mobile apps quick start](/get-started/quickstart-guides/mobile-apps#webview-postmessage)). System browser components (`ASWebAuthenticationSession`, `SFSafariViewController`, Chrome Custom Tabs) and full-page top-level redirects don't expose a listener, so they receive results via the `redirectUrl` callback and confirm server-side with [`/session/get`](/api/endpoints/get-session) or [webhooks](/webhooks).
:::
When the age gate flow completes, a **session is created** to store the player's permissions and age status. A **challenge is created whenever the flow needs one**, either for Verifiable Parental Consent or for [Automatic age assurance](/cdk/age-gate#automatic-age-assurance) when the player claims an age old enough to skip parental consent.
The widget emits JavaScript events that you can listen to. Listen for the [`Widget.AgeGate.Result`](/events/dom-events/event-structures/widget-agegate-result) event, which includes a `sessionId` when the flow completes successfully. If a challenge was created during the flow, the event also includes the `challengeId`. For detailed information about challenge-specific events, see [`Widget.AgeGate.Challenge`](/events/dom-events/event-structures/widget-agegate-challenge).
:::important Closing the UI
Listen for the [`Widget.ExitReview`](/events/dom-events/event-structures/widget-exitreview) event to determine when to close the widget UI. This event is emitted when the user clicks the 'Done' button, indicating the flow is complete and the iframe should be closed or hidden.
:::
```javascript
window.addEventListener('message', (event) => {
if (!event.origin.endsWith('.k-id.com')) {
return;
}
const message = event.data;
if (message.eventType === 'Widget.AgeGate.Result') {
if (message.data.status === 'PASS') {
const sessionId = message.data.sessionId;
// If challengeId is present, a challenge was resolved during the flow
// (for example, parental consent or auto age-assurance).
if (message.data.challengeId) {
console.log('Challenge resolved, session issued:', sessionId);
} else {
console.log('Session created (no challenge required):', sessionId);
}
grantAccess(sessionId);
}
}
// Handle challenge-specific events if needed
if (message.eventType === 'Widget.AgeGate.Challenge') {
if (message.data.status === 'FAIL') {
// Parent denied consent - restrict access
console.log('Consent denied');
restrictAccess();
}
}
if (message.eventType === 'Widget.ExitReview') {
// Close the widget UI when the user clicks 'Done'
closeWidget();
}
});
```
## What the widget handles
The widget automatically handles:
- **Age Collection**: Jurisdiction-appropriate age collection methods
- **Data Notices**: Data notices to accept, depending on the product's configuration in the [Compliance Studio](/compliance-studio/product-notices)
- **Permissions**: Permissions to manage, depending on the product's configuration in the [Compliance Studio](/compliance-studio/product-api-configuration#permissions)
- **Parental Consent Challenge**: If the user is determined to be a minor, a challenge is created for trusted adult approval
- **Automatic age assurance**: If the product has [Automatic age assurance](/cdk/age-gate#automatic-age-assurance) enabled for the jurisdiction, players who claim an age old enough to skip parental consent are asked to prove the claim (facial age estimation or ID document) inside the widget before a session is issued.
The specific flow depends on the jurisdiction and your product's configuration in the Compliance Studio.
:::info Session creation timing with Automatic age assurance
When Automatic age assurance triggers inside the end-to-end widget, the session is created after the player passes verification rather than immediately after the age gate. The `Widget.AgeGate.Result` event is still fired once the flow completes and includes the `sessionId` on `PASS`. Until the event arrives, treat the flow as in progress.
:::
For more information on implementing VPC, see the [Quick Start Guide](/get-started/quickstart-guides/vpc).
---
// File: cdk/ux-guidelines
# UX guidelines
When building a custom age gate by using the k-ID API, follow these UX guidelines to create a compliant and user-friendly experience. These recommendations help ensure players can complete age verification smoothly while meeting regulatory requirements.
:::tip When to use these guidelines
These guidelines apply when you're building a [custom age gate](/get-started/quickstart-guides/custom-age-gate) using the k-ID API directly. If you're using the k-ID embedded widget, the UX is already handled for you.
:::
## Age input methods
Your implementation should support at least two methods of collecting a player's age: an age slider or a date picker. Check the `approvedAgeCollectionMethods` field from the [`/age-gate/get-requirements`](/api/endpoints/get-age-gate-requirements) endpoint to determine which collection methods are permitted for the player's jurisdiction.
### Age slider
An age slider provides a simple, intuitive way for players to input their age. This is the recommended approach for most jurisdictions.

**Design recommendations:**
- Set the slider range from 0 to 35+ (per ESRB guidelines)
- Start in a neutral state with the default position at 0
- Keep the continue button inactive until the player interacts with the slider
- Display the selected age clearly as the player adjusts the slider
- Support keyboard navigation for accessibility
**Mobile version:**

### Date picker
A date picker collects the player's full date of birth. This method is required in certain jurisdictions.

**Design recommendations:**
- Support day/month/year entry in the local date format
- Display clear validation messages for invalid dates
- Match the date format to local conventions (DD/MM/YYYY or MM/DD/YYYY)
## Age confirmation
After collecting the player's age, display a confirmation screen before proceeding. This step helps prevent accidental errors and reinforces the importance of accurate age information.

**Design recommendations:**
- Summarize the entered age or date of birth clearly
- Explain that the entered age affects the player's experience
- Provide clear options to go back and correct the age, or confirm and continue
## Trusted adult consent
When a player is below the digital consent age for their region, they need approval from a trusted adult. Your UI should present all three consent methods to maximize completion rates.

### Consent methods
Provide these three options for the trusted adult to complete consent:
1. **Email**: Allow the player to enter a parent or guardian's email address. Call [`/challenge/send-email`](/api/endpoints/send-email) to send the consent request.
2. **QR code**: Display a scannable QR code from the `challenge.url` field. The trusted adult scans this with their phone to access the consent portal.
3. **Manual code entry**: Display the `challenge.oneTimePassword` and direct the trusted adult to [asktoplay.com](https://asktoplay.com) to enter it.
### Design recommendations
- Display all three options with equal visibility
- Include a "Do This Later" option for players who need to return later
- Add a copy button for the one-time code to improve usability
- Store the `challengeId` so players can resume the flow if they return before consent is granted
:::warning QR codes and one-time passwords expire
The QR code and one-time password expire after 1 hour and must be refreshed using the [`/challenge/generate-otp`](/api/endpoints/generate-otp) API. Design your UI to handle expiration gracefully by providing a refresh option or automatically regenerating credentials. For more details, see [Challenge expiration and time-based authentication](/concepts/access-features-consent/challenges#challenge-expiration-and-time-based-authentication).
:::
## Data-lite mode
If your game supports [data-lite mode](/concepts/access-features-consent/data-lite-mode), players under the digital consent age can access limited features while awaiting parental approval.
**Design recommendations:**
- Clearly communicate which features are available in data-lite mode
- Explain what additional features become available after consent is granted
- Provide a way for players to check consent status or resend the consent request
## Accessibility
Ensure your custom age gate UI meets accessibility standards:
| Requirement | Implementation |
|-------------|----------------|
| Keyboard navigation | All interactive elements accessible via keyboard |
| Screen reader support | Appropriate ARIA labels and semantic HTML |
| Color contrast | Sufficient contrast for text and interactive elements |
| Tap targets | Minimum 44×44 pixel tap targets for buttons |
| Error messaging | Descriptive error messages that explain how to resolve issues |
## Responding to permission changes
After the initial consent flow, permissions can change over time. Parents might adjust settings through Family Connect, players might age up to a new category, or sessions might be deleted. Your game should handle these changes gracefully and communicate them clearly.
For implementation details on detecting permission changes, see [Managing sessions and permissions](/get-started/quickstart-guides/managing-sessions-permissions).
### Communicating permission changes
When permissions change, display a clear notification that explains what happened. Players should never be left wondering why a feature suddenly became available or unavailable.
**Design recommendations:**
- Show a dialog or notification when the game detects permission changes
- Clearly list which features were enabled or disabled
- Explain the reason when possible (parent update, birthday, and similar)
- Use neutral, non-judgmental language
- Provide a clear dismissal action
**Example messages:**
| Scenario | Example message |
|----------|-----------------|
| Parent disabled features | "Your parent has updated your permissions. Voice Chat is no longer available." |
| Parent enabled features | "Your parent has enabled new features: Voice Chat, Public Profile." |
| Player aged up | "Your permissions have been updated based on your age." |
| Mixed changes | "Your permissions have been updated. Some features are now available, and others have been turned off." |
### Displaying disabled features
When a feature is disabled, your UI should make it clear why and whether the player can take action.
**Design recommendations:**
| `managedBy` value | UI treatment |
|-------------------|--------------|
| `GUARDIAN` | Show the feature as disabled with an "Ask Parent" option to request access |
| `PLAYER` | Show the feature as disabled with an option to enable it |
| `PROHIBITED` | Hide the feature entirely, or show it as unavailable without an action option |
- Don't show error states for disabled features, as they aren't errors
- Use visual treatments (dimmed appearance, lock icon) that clearly indicate restricted access
- For guardian-managed features, provide a path to request permission upgrades
### Requesting additional permissions
When a player wants to enable a feature that requires parental consent, provide a clear path to request it.
**Design recommendations:**
- Show an "Ask Parent" or "Request Access" button for guardian-managed features
- Explain what the feature does and why it requires permission
- Present the same consent methods as the initial flow (email, QR code, manual code)
- Allow players to check the status of pending requests
- Confirm when a request has been sent successfully
### Handling session deletion
When a session is deleted, the player must complete the age gate flow again. Handle this change smoothly.
**Design recommendations:**
- Explain that their session has ended and they need to verify their age again
- Don't display error messages that suggest something went wrong
- Preserve any non-permission-related progress or settings when possible
- Provide a clear call-to-action to restart the verification flow
## Related resources
- [Custom age gate quick start](/get-started/quickstart-guides/custom-age-gate) - Step-by-step implementation guide
- [Managing sessions and permissions](/get-started/quickstart-guides/managing-sessions-permissions) - Detecting and responding to permission changes
- [Age gate](/cdk/age-gate) - API documentation for age gate endpoints
- [Best practices](/cdk/best-practices) - General best practices for CDK integration
---
// File: cdk/data-notices
# Data notices
When building custom VPC flows with the k-ID API, you can use the data notices widget to display product data notices and collect user consent. The widget shows jurisdiction-appropriate disclosures and handles the consent acceptance workflow.
For general information about data notices, including when they're required, and what data is stored, see [Data Notices](/concepts/data-notices) in the Core concepts section.
## Generating the data notices widget URL
Call the [`/widget/generate-direct-notices-url`](/api/endpoints/generate-direct-notices-url) API to create a data notices widget URL. This returns a unique URL for users to view and accept data notices.
### Example request
```json
POST /api/v1/widget/generate-direct-notices-url
Content-Type: application/json
Authorization: Bearer your-api-key
{
"jurisdiction": "US-CA",
"sessionId": "608616da-4fd2-4742-82bf-ec1d4ffd8187"
}
```
### Request parameters
| Property | Description | Required? |
|----------|-------------|-----------|
| `jurisdiction` | The jurisdiction in which the data notices should be displayed | Yes |
| `sessionId` | The session ID for the player (if available) | No |
### Example response
```json
{
"id": "7854909b-9124-4bed-9282-24b44c4a3c97",
"url": "https://family.k-id.com/widget?token=eyJhbGciOiJFUzM4NCIs..."
}
```
## Embedding the widget
Use the returned URL to create an iframe in your website or app. Users view data notices and provide consent through this interface:
```html
```
## Receiving consent results
Data notice consent is tracked as part of the session. When data notices are accepted, the consent is recorded in the session. You can check data notice consent status in the session by calling the [`/session/get`](/api/endpoints/get-session) API.
For real-time updates, you can also listen for session-related events:
### JavaScript events (client-side)
For detailed information about the event structure, see [`Widget.DataNotices.ConsentApproved`](/events/dom-events/event-structures/widget-datanotices-consentapproved).
If the data notices widget is embedded in an iframe, you can listen for window messages from the widget:
```javascript
window.addEventListener('message', (event) => {
if (!event.origin.endsWith('.k-id.com')) {
return;
}
const message = event.data;
// Handle widget completion events
if (message.eventType === 'Widget.DataNotices.ConsentApproved') {
// Data notices have been accepted
console.log('Data notices accepted:', message.data.jurisdiction);
handleConsentAccepted();
}
});
```
Example event:
```json
{
"eventType": "Widget.DataNotices.ConsentApproved",
"data": {
"jurisdiction": "US"
}
}
```
### Webhooks (server-side)
For detailed information about the webhook event structure, see [`Session.ChangePermissions`](/events/webhooks/event-types/session-changepermissions).
Configure a webhook endpoint to receive session-related events. When data notice consent is updated, session changes are reflected in [`Session.ChangePermissions`](/events/webhooks/event-types/session-changepermissions) events. For more information, see [Webhooks](/webhooks).
## Data notices and sessions
When a session is created, data notice consents are included in the session. The session tracks which data notices have been accepted and when they were accepted. You can retrieve the session by calling the [`/session/get`](/api/endpoints/get-session) API to check data notice consent status.
For more information about how data notices work with sessions, see [Data Notices](/concepts/data-notices#data-notices-and-sessions) in the Core concepts section.
---
// File: cdk/sessions-permissions
# Sessions & permissions
Sessions and permissions are core concepts in k-ID that determine what features and capabilities a player can access in your game.
## Sessions
A k-ID `Session` contains the collection of permissions and age status for the current player and location. Every player requires an active `Session`. The game should consult the active `Session` to determine whether features are allowed or disallowed in the game.
For detailed information about sessions, see [Sessions](/concepts/access-features-consent/sessions).
## Getting a session
You can retrieve a session two ways:
- **[`/session/get`](/api/endpoints/get-session)**: Get a session by `sessionId` or `kuid`
- **[`/age-gate/check`](/api/endpoints/check-age-gate)**: Creates or updates a session as part of the age gate flow
### Example request
```json
GET /api/v1/session/get?sessionId=608616da-4fd2-4742-82bf-ec1d4ffd8187&etag=6d9d24fccd428f845b355122799948dd0a52fc5d
Authorization: Bearer your-api-key
```
The [`/session/get`](/api/endpoints/get-session) API supports conditional requests by using the `etag` parameter. If the session hasn't changed since the last request, the API returns HTTP 304 (Not Modified), allowing you to avoid unnecessary data transfer.
### Example response
```json
{
"session": {
"ageStatus": "LEGAL_ADULT",
"dateOfBirth": "2005-04-15",
"etag": "6d9d24fccd428f845b355122799948dd0a52fc5d",
"jurisdiction": "US-CA",
"kuid": "123456",
"permissions": [
{
"enabled": true,
"managedBy": "PLAYER",
"name": "ai-generated-avatars"
},
{
"enabled": true,
"managedBy": "PLAYER",
"name": "text-chat-private"
}
],
"allowances": [],
"sessionId": "608616da-4fd2-4742-82bf-ec1d4ffd8187",
"status": "ACTIVE"
},
"status": "PASS"
}
```
## Permissions
Permissions represent features or capabilities in your game that could require parental consent or have age restrictions. Each permission can be allowed or disallowed for a player based on:
- Their age and jurisdiction
- Parental consent (if required)
- The permission's configuration in the [Compliance Studio](/compliance-studio/product-api-configuration#permissions)
For detailed information about permissions, see [Permissions](/concepts/access-features-consent/permissions).
## Using permissions
The game code should use each k-ID Permission to control access to the corresponding features in the game. If the `enabled` field is `true` for a permission, this means that the feature can be enabled for the player in the game. If the `enabled` field is `false`, the feature must be turned off.
The `managedBy` field indicates who can allow or disallow this permission:
- **`PLAYER`**: The player can enable/disable this permission themselves
- **`GUARDIAN`**: Only a trusted adult can enable/disable this permission
- **`PROHIBITED`**: This permission is never allowed for the current player in the current location
If a feature is never allowed for the current player in the current location regardless of their trusted adult's consent, the `managedBy` field contains the value `PROHIBITED`. In this case, it's appropriate for the game to just remove the prohibited feature entirely from the user experience rather than show it turned off.
## Upgrading permissions
After a player has received a session with permissions, they might want to allow additional permissions. Use the [`/session/upgrade`](/api/endpoints/upgrade-session) API to request additional permissions.
For more information, see [Permissions Upgrade](/concepts/access-features-consent/permissions#requesting-additional-permissions).
### Presenting legal documents and data notices
`/session/upgrade` can present updated legal documents and data notices for the trusted adult to review and accept as part of the same consent moment. Pass the documents to present in the request `materialChange`:
- `materialChange.termsOfServiceDocument`, `materialChange.privacyPolicyDocument`, and `materialChange.additionalLegalLinks`: the configured legal-document variant ids to present.
A challenge is created whenever the request asks for any of the following, so the documents can be reviewed and accepted:
- a guardian-managed permission that requires parental consent,
- one or more legal documents in `materialChange`, or
- a material-change re-consent (see below).
If none of these apply, permissions are granted directly without a challenge, exactly as before. Any data notices the session still needs consent for are included automatically, and accepting the challenge records consent for both the legal documents and the data notices.
The trusted adult reviews the exact documents on the approval page. To read the resolved documents, use the `challenge.documents` array on the upgrade response. Each entry carries the variant `id`, its localized `name`, a `url` for a legal document, and `isDataNotice`, which is `true` for a name-only data notice (a data notice has no `url`). On a re-consent chain (`priorChallengeId`) the array accumulates the prior challenge's documents, so the head challenge always carries the full set.
### Requesting re-consent with a deadline (material change)
When a document has changed and you need the trusted adult to re-accept by a deadline, add an `acceptanceDeadline` to `materialChange` on the upgrade request:
- `materialChange.acceptanceDeadline`: the RFC 3339 UTC datetime by which the adult must accept. Optional: omit it to present the documents without a deadline. When set, it must be in the future.
- `materialChange.locale`: the locale for the re-consent email (defaults to `en`).
Setting a deadline makes the upgrade an active, deadline-bound re-consent: the challenge carries the deadline (returned as `expiresAt` from [`/challenge/get-status`](/api/endpoints/get-challenge-status)), and the trusted adult is emailed a link to the same approval page. Without a deadline the documents are still presented for review on the approval page, but no acceptance deadline is enforced.
:::note Active versus passive re-consent
`materialChange` with an `acceptanceDeadline` on `/session/upgrade` is for **active** re-consent bundled with an upgrade. To send a notify-only (passive) email without gating access, use the dedicated `/session/send-material-change-notice` API instead.
:::
## Using the permissions upgrade widget
When building custom VPC flows with the k-ID API, you can use the permissions upgrade widget to allow players to request additional permissions that require parental consent. The widget handles the consent workflow for permission upgrades.
:::important Guardian-managed sessions only
The permissions upgrade widget only works for guardian-managed sessions. If a player's session is player-managed (where `managedBy` is `"PLAYER"`), permissions can be enabled directly via the [`/session/upgrade`](/api/endpoints/upgrade-session) API without creating a challenge, so the widget isn't needed.
:::
:::warning Parent authentication required
The manage session permissions widget must only be hosted in a parent-authenticated session. The widget doesn't provide its own parent authentication, so it should never be presented directly to a minor. Always ensure that the widget is only displayed to authenticated parents or trusted adults.
:::
For general information about permissions upgrades, including when they're needed and how they work, see [Permissions Upgrade](/concepts/access-features-consent/permissions#requesting-additional-permissions) in the Core concepts section.
### Generating the permissions upgrade widget URL
Call the [`/widget/generate-manage-session-permissions-url`](/api/endpoints/generate-manage-session-permissions-url) API to create a permissions upgrade widget URL. This returns a unique URL for trusted adults to review and approve additional permissions.
### Example request
```json
POST /api/v1/widget/generate-manage-session-permissions-url
Content-Type: application/json
Authorization: Bearer your-api-key
{
"sessionId": "b1a6482d-5242-4b4a-aa88-3fa52595a672",
"email": "parent@example.com"
}
```
### Request parameters
| Property | Description | Required? |
|----------|-------------|-----------|
| `sessionId` | The ID of the session | Yes |
| `email` | The email address of the trusted adult | Yes |
### Example response
```json
{
"url": "https://family.k-id.com/widget?token=eyJhbGciOiJFUzM4NCIs..."
}
```
### Embedding the widget
Use the returned URL to create an iframe in your website or app. Trusted adults can review and approve additional permissions through this interface:
```html
```
### Receiving upgrade results
Permission upgrades are tracked as part of the session. When additional permissions are approved, the session is updated with the new permissions. You can check the updated permissions in the session by calling the [`/session/get`](/api/endpoints/get-session) API.
For real-time updates, you can also listen for session-related events:
### JavaScript events (client-side)
For detailed information about the event structure, see [`Widget.ExitReview`](/events/dom-events/event-structures/widget-exitreview).
If the permissions upgrade widget is embedded in an iframe, you can listen for window messages from the widget:
```javascript
window.addEventListener('message', (event) => {
if (!event.origin.endsWith('.k-id.com')) {
return;
}
const message = event.data;
// Handle widget completion events
if (message.eventType === 'Widget.ExitReview') {
// Permissions upgrade flow completed
console.log('Permissions upgrade completed');
handleUpgradeCompleted();
}
});
```
### Webhooks (server-side)
For detailed information about the webhook event structure, see [`Session.ChangePermissions`](/events/webhooks/event-types/session-changepermissions).
Configure a webhook endpoint to receive session-related events. When permissions are upgraded, session changes are reflected in [`Session.ChangePermissions`](/events/webhooks/event-types/session-changepermissions) events. For more information, see [Webhooks](/webhooks).
### Permissions upgrade and sessions
When permissions are upgraded through the widget, the session is updated with the new permissions. The session tracks which permissions have been enabled and when they were enabled. You can retrieve the session by calling the [`/session/get`](/api/endpoints/get-session) API to check the updated permissions.
For more information about how permissions upgrades work with sessions, see [Permissions Upgrade](/concepts/access-features-consent/permissions#requesting-additional-permissions) in the Core concepts section.
## Session caching
The `Session` should be cached in local or cloud storage, and can be associated with a player's account. k-ID Sessions only change when a parent updates a permission, or a kid or teen "ages up" to the next age category, or it's deleted by the parent or player.
While it's recommended that the game refresh the Session from the [`/session/get`](/api/endpoints/get-session) API every time the game restarts, this isn't explicitly required. Additionally, k-ID Webhooks can be used to receive `Session` updates instead of calling the [`/session/get`](/api/endpoints/get-session) API.
## Session webhooks
Configure webhooks to receive session-related events:
- **[`Session.ChangePermissions`](/events/webhooks/event-types/session-changepermissions)**: Sent when permissions are modified by a parent
- **[`Session.Delete`](/events/webhooks/event-types/session-delete)**: Sent when a session is deleted
For more information, see [Webhooks](/webhooks).
---
// File: cdk/best-practices
# Best practices
## Age gate best practices
### Always check age gate requirements
Before collecting age information, always call [`/age-gate/get-requirements`](/api/endpoints/get-age-gate-requirements) with the player's jurisdiction. This ensures you:
- Display the age gate only when required (`shouldDisplay` = `true`)
- Use only approved age collection methods for the jurisdiction
- Understand the age thresholds (digital consent age, civil age, minimum age)
- Know if age assurance is required
### Handle all age gate check responses
When calling [`/age-gate/check`](/api/endpoints/check-age-gate), handle all three possible statuses:
- **`PROHIBITED`**: Player is below the minimum age - block access completely
- **`CHALLENGE`**: Player requires parental consent - create and display a challenge
- **`PASS`**: Player can continue - retrieve or create a session
Never assume a player can proceed without checking the response status.
## VPC and challenge best practices
### Store challenge IDs persistently
When a `CHALLENGE` status is returned from [`/age-gate/check`](/api/endpoints/check-age-gate), store the challenge ID in local storage or your server. This allows you to:
- Resume the consent flow if the player returns before consent is granted
- Display the same challenge information (QR code, OTP) on subsequent visits
- Retrieve challenge details using [`/challenge/get`](/api/endpoints/get-challenge)
### Use webhooks for challenge status (recommended)
Configure webhooks to receive [`Challenge.StateChange`](/events/webhooks/event-types/challenge-statechange) events instead of polling. Webhooks provide:
- Real-time updates when consent is granted or denied
- Reduced API calls and better performance
- More reliable status tracking
If you must use polling, implement proper rate limiting:
- Minimum 5 seconds between calls to [`/challenge/get-status`](/api/endpoints/get-challenge-status)
- Always check for HTTP 429 responses and implement appropriate retry logic. See [Rate limits](/api/rate-limits) for default per-mode limits
- Set appropriate timeout limits
### Notify trusted adults effectively
When a challenge is created, provide multiple ways for trusted adults to complete consent:
- **Email notification**: Call [`/challenge/send-email`](/api/endpoints/send-email) if an email address is available
- **QR code display**: Show the QR code URL returned in the challenge response
- **OTP entry**: Display the one-time password for manual entry
Store the `approverEmail` from challenge status responses for customer service purposes.
## Session management best practices
### Cache sessions appropriately
Sessions should be cached in local or cloud storage and associated with a player's account. Sessions only change when:
- A parent updates permissions
- A player "ages up" to the next age category
- The session is deleted by the parent or player
### Use ETags for efficient session retrieval
The [`/session/get`](/api/endpoints/get-session) API supports conditional requests by using the `etag` parameter. Include the ETag from your cached session:
- If the session hasn't changed, the API returns HTTP 304 (Not Modified)
- This reduces unnecessary data transfer and improves performance
- Always refresh sessions when the game restarts
### Implement session webhooks
Configure webhooks to receive session-related events:
- **[`Session.ChangePermissions`](/events/webhooks/event-types/session-changepermissions)**: Sent when permissions are modified by a parent
- **[`Session.Delete`](/events/webhooks/event-types/session-delete)**: Sent when a session is deleted
This provides real-time updates without polling and ensures your game reflects permission changes immediately.
### Handle session upgrades properly
When players request additional permissions that require parental consent:
- Use [`/session/upgrade`](/api/endpoints/upgrade-session) to create a challenge for new permissions
- Use the permissions upgrade widget to handle the consent workflow
- Monitor session changes via webhooks or polling to detect when upgrades are approved
## Permissions best practices
### Respect permission states
Always check both the `enabled` and `managedBy` fields for each permission:
- **`enabled: true`**: Feature can be enabled for the player
- **`enabled: false`**: Feature must be turned off
- **`managedBy: PLAYER`**: Player can enable/disable themselves
- **`managedBy: GUARDIAN`**: Only a trusted adult can enable/disable
- **`managedBy: PROHIBITED`**: Feature is never allowed - remove it from the UI entirely
### Map permissions to game features correctly
Ensure each k-ID permission is properly mapped to the corresponding feature in your game. Permissions should control access to:
- Age-restricted content
- Features requiring parental consent
- Data collection capabilities
- Communication features
Configure permissions in the [Compliance Studio](/compliance-studio/product-api-configuration#permissions) to match your game's feature set.
## Security recommendations
### Server-only API calls
:::tip Important
All widget URL generation endpoints should only be called from your server, never directly from client-side code.
:::
Your k-ID API key is a secret credential that must be protected:
- **Store API keys securely** using a secrets manager
- **Never expose API keys** in front end JavaScript, mobile app code, or any client-facing code
- **Never store API keys** on client devices or in client-side storage
### Target origins configuration
CDK offers the ability to configure target origins in the [Compliance Studio](/compliance-studio/creating-product) to control which domains can embed your widgets. This setting controls the [frame-ancestors directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors) of the Content Security Policy. As an optional security measure, it's recommended to evaluate your risk tolerance and decide whether to implement target origin restrictions based on your security requirements.
**Configuration Options:**
- **Specific domains**: Set exact domains for production use (for example, `https://yourgame.com`)
- **Wildcard subdomains**: Use wildcard patterns for subdomains (for example, `https://*.yourgame.com`)
- **Unrestricted**: Leave empty or set to `*` for unrestricted embedding (not recommended for production)
**Security Benefits:**
Target origins prevent attackers from embedding your compliance flows in transparent iframes on malicious sites, where they could overlay other content and trick users into inadvertently clicking on verification elements.
:::warning Important
**Implementation Notes:**
- Configure separate target origins for each environment (test/live)
- **Ensure target origins are properly configured for all production endpoints before going live.**
- Each subdomain requires its own entry unless using wildcards
:::
:::info
Only certain k-ID pages can be embedded in iframes regardless of target origin settings (verification pages, widgets, and VPC flows). All other k-ID pages (family management, account settings) are always blocked from iframe embedding.
:::
### iframe permissions
Always include the necessary permissions in your iframe `allow` attribute:
```html
```
**Permission Breakdown**:
- `camera` - Required for facial age estimation (if used)
- `payment` - Required for credit card verification (if used)
- `publickey-credentials-create` - Required for AgeKey creation (if used)
- `publickey-credentials-get` - Required for AgeKey verification (if used)
### Origin validation
Always validate the origin of incoming messages:
```javascript
window.addEventListener('message', (event) => {
// Validate origin based on environment
const validOrigins = [
'https://family.k-id.com', // Live environment
'https://family.test.k-id.com' // Test environment
];
if (!validOrigins.includes(event.origin)) {
return; // Ignore messages from unauthorized origins
}
// Process the event
handleWidgetEvent(event.data);
});
```
## Account system integration
### Product context mapping
When integrating CDK with an account system that spans multiple games:
- Map each game to its own k-ID Product in the Compliance Studio
- Create a separate k-ID Product for the Account System itself for global permissions
- Ensure the correct API key is used for each Product context
- Store the `kuid` from sessions in your account system for cross-product session retrieval
### Session caching for multiple products
Cache sessions by Product ID in your account system:
- Use k-ID Product ID as the cache key
- Check for existing sessions before triggering new VPC flows
- Allow players to continue without additional consent if a session already exists for a Product
### Jurisdiction handling
Always pass the correct jurisdiction when making API calls:
- Determine jurisdiction from player's location or IP address
- Use jurisdiction-specific age thresholds and requirements
- Handle jurisdiction changes during gameplay by calling [`/session/update-jurisdiction`](/api/endpoints/update-jurisdiction)
---
// File: cdk/trusted-adult-preferences
# Trusted adult preferences
Trusted adults can configure preferences for how they want to manage consent and permissions for their children. These preferences are configured in Family Connect and affect how consent challenges are presented and processed.
## How preferences work with the API
Trusted adult preferences are part of the session object and are accessible through the [`/session/get`](/api/endpoints/get-session) endpoint. Preferences are stored in an `allowances` array within the session response.
## Accessing allowances in sessions
When you retrieve a session by using [`/session/get`](/api/endpoints/get-session), the response includes an `allowances` array that contains the trusted adult preferences configured for that player. Each allowance represents a preference setting that the trusted adult has configured.
### Example session response with allowances
```json
{
"session": {
"sessionId": "b1a6482d-5242-4b4a-aa88-3fa52595a672",
"kuid": "12b9fa0e-6d6d-4903-a1fc-f2233027b71d",
"ageStatus": "LEGAL_ADULT",
"ageCategory": "adult",
"etag": "e889efb9e8a985308e82bed78c5aef7f37f50cf36b7337bf654980d0bab7a574",
"status": "ACTIVE",
"dateOfBirth": "2005-04-15",
"jurisdiction": "US-CA",
"managedBy": "PLAYER",
"permissions": [
{
"name": "text-chat-public",
"enabled": false,
"managedBy": "GUARDIAN"
},
{
"name": "text-chat-private",
"enabled": true,
"managedBy": "PLAYER"
},
{
"name": "forums",
"enabled": false,
"managedBy": "PROHIBITED"
}
],
"allowances": [
{
"name": "3516-7b2e",
"numericalValue": 5,
"type": "numerical"
},
{
"name": "63d3-90ac",
"selectionValue": "733c-ca11",
"type": "selection"
}
]
},
"status": "PASS"
}
```
## Allowance structure
Each allowance in the `allowances` array has the following structure:
| Field | Type | Description |
|-------|------|-------------|
| `name` | string | A unique identifier for the allowance preference (configured in Compliance Studio) |
| `type` | string | The type of allowance: `"numerical"` or `"selection"` |
| `numericalValue` | number | The numerical value (only present when `type` is `"numerical"`) |
| `selectionValue` | string | The selected value identifier (only present when `type` is `"selection"`) |
## Allowance types
### Numerical allowances
Numerical allowances represent preferences that have a numeric value, such as:
- Maximum daily playtime hours
- Maximum spending limits
- Time-based restrictions
Example:
```json
{
"name": "3516-7b2e",
"numericalValue": 5,
"type": "numerical"
}
```
### Selection allowances
Selection allowances represent preferences where the trusted adult has chosen from predefined options, such as:
- Content rating preferences
- Communication settings
- Feature access levels
Example:
```json
{
"name": "63d3-90ac",
"selectionValue": "733c-ca11",
"type": "selection"
}
```
## Using allowances in your application
You can use allowances to implement game logic that respects trusted adult preferences:
1. **Retrieve the session**: Call [`/session/get`](/api/endpoints/get-session) to get the current session with allowances
2. **Check for allowances**: Look for the `allowances` array in the session response
3. **Process each allowance**: Iterate through allowances and apply the preference values to your game logic
4. **Handle allowance types**: Check the `type` field to determine whether to use `numericalValue` or `selectionValue`
### Example implementation
```javascript
async function getPlayerAllowances(sessionId) {
const response = await fetch(`/api/v1/session/get?sessionId=${sessionId}`, {
headers: {
'Authorization': `Bearer ${apiKey}`
}
});
const data = await response.json();
const allowances = data.session?.allowances || [];
// Process allowances
const preferences = {};
allowances.forEach(allowance => {
if (allowance.type === 'numerical') {
preferences[allowance.name] = allowance.numericalValue;
} else if (allowance.type === 'selection') {
preferences[allowance.name] = allowance.selectionValue;
}
});
return preferences;
}
```
## When allowances are applied
Allowances are automatically included in the session when:
- A consent challenge is created and approved
- A session is created or updated
- Permissions are upgraded
The allowances reflect the preferences that the trusted adult has configured in Family Connect for that player.
## Configuring allowances
Allowances are configured in the [Compliance Studio](/compliance-studio/product-api-configuration#parental-preferences) where you define:
- The allowance names (identifiers)
- The allowance types (numerical or selection)
- The available options for selection-type allowances
For detailed information about trusted adult preferences, including how they're configured and how they affect your integration, see [Trusted Adult Preferences](/concepts/access-features-consent/trusted-adult-preferences) in the Core concepts section.
---
// File: cdk/prelaunch-checklist
# Prelaunch checklist
Before going live, consult this checklist as a simple resource to ensure you are ready for a successful launch with CDK.
## Prelaunch configuration
- [ ] **Product Configuration** (in the [Compliance Studio](/compliance-studio/creating-product))
- [ ] Product details and branding configured
- [ ] Permissions properly mapped to game features in [Permissions configuration](/compliance-studio/product-api-configuration#permissions)
- [ ] Data notices configured in [Data notices configuration](/compliance-studio/product-notices)
- [ ] Age gate settings configured
- [ ] Trusted adult preferences configured (if applicable)
- [ ] Target origins properly configured for both test and production
- [ ] **API Integration**
- [ ] Age gate requirements checked before collecting age
- [ ] Age gate check implemented with all status handlers (`PROHIBITED`, `CHALLENGE`, `PASS`)
- [ ] Challenge creation and display implemented
- [ ] Challenge status tracking implemented (webhooks or polling)
- [ ] Session retrieval implemented
- [ ] Session caching implemented
- [ ] Permission checks implemented for all game features
- [ ] Widget URLs generated correctly for all flows (E2E, age gate, data notices, permissions)
- [ ] Event handlers implemented for all widget types
- [ ] Error handling implemented for all API calls
- [ ] Fallback flows defined for edge cases
- [ ] **Account System Integration** (if applicable)
- [ ] Product context mapping configured correctly
- [ ] `kuid` storage implemented in account system
- [ ] Session caching by Product ID implemented
- [ ] Cross-product session retrieval working correctly
## Security validation
- [ ] **Proper Environment Mapping**
- [ ] Test API key calling test endpoints
- [ ] Live API key calling live endpoints
- [ ] API keys stored securely (never in client-side code)
- [ ] **Origin Validation**
- [ ] Event origin validation implemented for all widget event handlers
- [ ] Target origins configured in the [Compliance Studio](/compliance-studio/creating-product)
- [ ] CSP headers configured if applicable
- [ ] **iframe Security**
- [ ] Appropriate `allow` permissions set for all widgets
- [ ] Sandbox attributes reviewed
- [ ] No sensitive data in URL parameters
- [ ] Widget URLs generated server-side only
- [ ] **Session Security**
- [ ] Sessions cached securely
- [ ] Session ETags used for efficient retrieval
- [ ] Session webhooks configured and secured
## Age gate validation
- [ ] **Age Gate Flow**
- [ ] Age gate requirements checked for all jurisdictions
- [ ] Approved age collection methods used correctly
- [ ] Age thresholds (digital consent age, civil age, minimum age) handled properly
- [ ] Age assurance requirements checked (if applicable)
- [ ] `PROHIBITED` status blocks access completely
- [ ] `CHALLENGE` status triggers consent flow
- [ ] `PASS` status creates/retrieves session correctly
## VPC and challenge validation
:::info Know your rate limits before launch
Live mode rate limits are significantly higher than test mode. Confirm your service can stay within the [default rate limits](/api/rate-limits) for both API requests and age verification / parental consent flows, or contact your k-ID representative if you need an increase.
:::
- [ ] **Challenge Handling**
- [ ] Challenge IDs stored persistently
- [ ] Challenge retrieval working for pending challenges
- [ ] Challenge display (QR code, OTP, email) implemented
- [ ] Email notification sent to trusted adults (when available)
- [ ] Challenge status tracking implemented (webhooks recommended)
- [ ] Rate limiting implemented for polling (if used). See [Rate limits](/api/rate-limits)
- [ ] HTTP 429 handling implemented with retry logic. See [Rate limits](/api/rate-limits)
- [ ] `approverEmail` stored for customer service
- [ ] **Consent Flow**
- [ ] Consent approval grants access correctly
- [ ] Consent denial restricts access appropriately
- [ ] Session created/updated after consent granted
- [ ] Permissions applied correctly after consent
## Session and permissions validation
- [ ] **Session Management**
- [ ] Sessions cached appropriately
- [ ] ETags used for conditional requests
- [ ] Session refresh on game restart
- [ ] Session webhooks configured (`Session.ChangePermissions`, `Session.Delete`)
- [ ] Session upgrade flow working (if applicable)
- [ ] Jurisdiction updates handled correctly
- [ ] **Permissions Implementation**
- [ ] All permissions mapped to game features
- [ ] `enabled` field checked for all features
- [ ] `managedBy` field respected (`PLAYER`, `GUARDIAN`, `PROHIBITED`)
- [ ] Prohibited permissions removed from UI
- [ ] Permission changes reflected in game immediately
- [ ] Permission upgrade flow working (if applicable)
## Data notices validation
- [ ] **Data Notices Flow**
- [ ] Data notices widget integrated (if using custom workflow)
- [ ] Data notice consent tracked in session
- [ ] Data notice events handled correctly
- [ ] Jurisdiction-specific notices displayed
## Final validation
- [ ] **End-to-End Testing**
- [ ] Complete user journeys tested for all age categories
- [ ] Age gate flow tested for all jurisdictions
- [ ] VPC flow tested end-to-end
- [ ] Trusted adult experience validated
- [ ] Session management working correctly
- [ ] Permission updates reflected in game
- [ ] Data notices flow tested (if applicable)
- [ ] Account system integration tested (if applicable)
- [ ] **Compliance Verification**
- [ ] Legal review completed
- [ ] Compliance Engine up-to-date
- [ ] Privacy policy updated
- [ ] Data handling procedures verified
- [ ] Audit trail capabilities confirmed
- [ ] Jurisdiction requirements verified for all target markets
- [ ] **Performance Testing**
- [ ] API response times acceptable
- [ ] Session caching working efficiently
- [ ] Webhook delivery reliable
- [ ] Rate limiting handled gracefully
- [ ] Error scenarios handled gracefully
Once all checklist items are completed, you're ready to publish your configuration to the live environment and begin serving real users with the CDK.
---
// File: cdk/age-signals/overview
# Platform age signals
Game platforms (Apple iOS, Google Play, Xbox, Meta Horizon, and k-ID itself) can provide age data about a player. This data can be passed to k-ID alongside, or instead of, a self-reported age. k-ID uses it to suppress the age gate when appropriate, resolve age conflicts, and, when the signal is considered verified, satisfy age verification requirements without additional verification steps.
Platform age signals work together with [age assurance for high-risk features](../age-assurance.md). For example, a verified Apple iOS signal can both skip the age gate and unlock loot box permissions in Brazil without a separate verification step.
## Start here
Use this feature set when you want to:
- Reuse age information the platform already knows about the player
- Skip the age gate for verified adult signals
- Unlock high-risk permissions, such as Brazil loot boxes and targeted ads, without asking the player to verify again
- Detect when a platform age signal conflicts with a self-reported age
## Quick integration paths
| If your game has… | Send to k-ID | What usually happens |
| --- | --- | --- |
| A verified adult signal from Apple iOS, Google Play, or k-ID | The platform signal on both `GET /age-gate/get-requirements` and `POST /age-gate/check` | The age gate can be skipped and verified-age permissions can be enabled immediately |
| An unverified signal, such as Xbox or Meta Horizon | The platform signal on `get-requirements` and `check` | k-ID can still use it for conservative age resolution, but it won't satisfy verified-age permissions |
| A category-based platform signal | The category directly, or first convert it with `POST /age-gate/get-platform-age-range` | k-ID resolves the category into a jurisdiction-specific age range |
| No platform signal at all | Your normal age-gate inputs | The standard age gate flow applies, and high-risk permissions can require age assurance later |
## API map
These are the endpoints developers use most during integration:
| Endpoint | When to call it | Why it matters |
| --- | --- | --- |
| `GET /age-gate/get-requirements` | Before showing an age gate | Tells you whether to show the gate and whether any verified-age permissions still need assurance |
| `GET /session/get` | After `check` or a challenge completes | Lets you refresh permissions and inspect `ageVerification` |
| `POST /age-gate/check` | Always, after you have the player's age inputs | Creates or updates the session and records verified age on the session when a verified platform signal allows it |
| `POST /session/upgrade` | When the player tries to use a high-risk feature | Either enables the requested permissions immediately or creates an age-assurance challenge |
| `POST /age-gate/get-platform-age-range` | Only for category-based platform signals | Converts a platform category to `ageLow` and `ageHigh` |
## End-to-end flow
```mermaid
flowchart TB
Start["Game starts"] --> GetSignal["Get platform signal if available"]
GetSignal --> Requirements["GET /age-gate/get-requirements"]
Requirements --> ShowGate{"shouldDisplay?"}
ShowGate -->|Yes| CollectAge["Collect date of birth or age"]
ShowGate -->|No| SkipGate["Skip age gate"]
CollectAge --> Check["POST /age-gate/check"]
SkipGate --> Check
Check --> CheckStatus{"status"}
CheckStatus -->|PASS| Session["Use returned session"]
CheckStatus -->|CHALLENGE| Vpc["Run VPC / parental consent flow"]
CheckStatus -->|PROHIBITED| Block["Block product access"]
Vpc --> VpcResult{"VPC successful?"}
VpcResult -->|Yes| Session
VpcResult -->|No| Block
Session --> Feature["Player tries a feature"]
Feature --> Threshold{"Permission has verifiedAgeThreshold?"}
Threshold -->|No| NormalAccess["Use session permissions normally"]
Threshold -->|Yes| Upgrade["POST /session/upgrade"]
Upgrade --> UpgradeResult{"Challenge returned?"}
UpgradeResult -->|No| AllowHighRisk["Enable feature now"]
UpgradeResult -->|Yes| Assurance["Run age assurance"]
Assurance --> AssuranceResult{"Age assurance successful?"}
AssuranceResult -->|Yes| Refresh["GET /session/get"]
Refresh --> AllowHighRisk
AssuranceResult -->|No| DenyHighRisk["Feature remains prohibited"]
```
## Recommended request sequence
1. **Get the platform signal at launch.** If the platform exposes age data, capture it as early as possible so you can use it on both `get-requirements` and `check`.
2. **Call `GET /age-gate/get-requirements`.** This tells you whether to show the age gate and whether any permissions still need verified age.
3. **Always call `POST /age-gate/check`.** Even when the gate is skipped, you still need `check` to create or update the session.
4. **Store the `sessionId` and current permissions.** This is what your game should use to control feature access.
5. **Call `POST /session/upgrade` only when needed.** Do this when the player actively tries to use a high-risk feature that's not yet enabled.
:::tip
If `shouldDisplay` is `false` but you don't actually have a platform signal to send, call `POST /age-gate/check` with `age: 1` as a conservative fallback.
:::
## How `POST /age-gate/check` uses platform signals
```mermaid
flowchart TD
Start["POST /age-gate/check"]
HasPlatform{"platformAgeSignal provided?"}
ResolveAgeSignal["Use the age signals and resolve the effective age to use"]
StandardFlow["Use dateOfBirth/age/kuid as primary age"]
SignalVerified{"Signal verified for age requirements?"}
StoreVerification["Store AgeVerification on session"]
NoStore["No AgeVerification stored"]
Done["Return response"]
AgeConflict["Return 400 AGE_CONFLICT"]
ConflictEnabled{"Is age conflict check enabled? (contact k-ID to enable)"}
ConflictCheck{"Determine if there's age conflict"}
Start --> HasPlatform
HasPlatform -->|No| StandardFlow
HasPlatform -->|Yes| ResolveAgeSignal
ResolveAgeSignal --> ConflictEnabled
ConflictEnabled -->|No| SignalVerified
ConflictEnabled -->|Yes| ConflictCheck
ConflictCheck -->|No| SignalVerified
ConflictCheck -->|Yes| AgeConflict
SignalVerified -->|Yes| StoreVerification
SignalVerified -->|No| NoStore
StoreVerification --> Done
NoStore --> Done
StandardFlow --> Done
```
## Key concepts
### The `PlatformAgeSignal` object
A unified object describing a platform-reported age signal. Provide **either** `category` **or** both `ageLow` and `ageHigh`. Never provide both.
```json
{
"name": "apple-ios",
"ageLow": 18,
"ageHigh": 25,
"declarationType": "governmentIDChecked",
"verificationId": null
}
```
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | Platform identifier: `apple-ios`, `google-play`, `xbox`, `meta-horizon`, or `k-id` |
| `category` | string | No | Platform-specific age category (for example, `digital-youth`, `TN`, `teen`) |
| `ageLow` | integer | No | Minimum age in years from the platform. Should be paired with `ageHigh`. |
| `ageHigh` | integer | No | Maximum age in years from the platform. Should be paired with `ageLow`. |
| `declarationType` | string | No | How the platform determined the age (for example, `governmentIDChecked`, `VERIFIED`) |
| `verificationId` | string (UUID) | No | For `k-id` signals only: references a completed k-ID verification record |
### Supported platforms
| Platform | Accepted input | Verified declaration types | Notes |
| --- | --- | --- | --- |
| **`apple-ios`** | `ageLow` + `ageHigh` only | `paymentChecked`, `governmentIDChecked`, `guardianPaymentChecked`, `guardianGovernmentIDChecked`, `confirmed` | Category input returns 400 |
| **`google-play`** | `ageLow` + `ageHigh` only | `VERIFIED`, `SUPERVISED` | Category input returns 400 |
| **Xbox** | `category` (`child`, `teen`, `adult`) | None (unverified for age requirements) | Ranges are jurisdiction-dependent |
| **meta-horizon** | `category` (`CH`, `TN`, `AD`) | None (unverified for age requirements) | Ranges are jurisdiction-dependent (see [Meta Horizon](./platform-details.md#meta-horizon)) |
| **k-ID** | `category` (`digital-minor`, `digital-youth`, `adult`) | `KIDVerified` (server-resolved) | A verified declaration requires valid `verificationId` from the same org within Compliance Studio |
:::note Apple declaration types
As of iOS 26.5, Apple returns `confirmed` for scrutinized checks (credit card or government ID) and deprecated the older `paymentChecked` / `governmentIDChecked` / `guardian*` types. k-ID still accepts the deprecated types for backwards compatibility. `selfDeclared` and `guardianDeclared` are treated as unverified. See [Platform signal details](./platform-details.md#apple-ios).
:::
### Verified and unverified signals
A platform signal is **considered verified** only when the `declarationType` is in the verified set for that platform. Verified signals can:
- **Suppress the age gate** (`shouldDisplay = false`) when the signal indicates an adult (`ageLow >= civilAge`)
- **Satisfy age verification thresholds** on permissions (for example, enabling loot boxes without a separate verification step)
- **Record an `ageVerification`** on the session for future permission checks
Unverified signals (for example, Xbox, Meta Horizon, or self-declared Apple or Google signals) are still useful for age conflict detection and conservative age resolution, but they can't bypass verification requirements.
### Verified age threshold
Some permissions require a verified age before they can be enabled. In those cases, the permission includes a `verifiedAgeThreshold` value.
For example, in Brazil, `loot-boxes-paid-cosmetic-only`, `loot-boxes-paid-gameplay-impacting`, `targeted-ads`, and `profiling` require a verified age of `18`, and `direct-marketing` requires a verified age of `12`. If the player's effective age is below the threshold, the permission becomes `PROHIBITED`. If the player meets the threshold but hasn't verified age yet, the permission stays `enabled: false` until either:
- A verified platform signal satisfies the threshold, or
- The player completes [age assurance for high-risk features](../age-assurance.md)
### Age conflict detection
When both a primary age parameter (`dateOfBirth`, `age`, or `kuid`) and a `platformAgeSignal` are provided to `/age-gate/check`, the system compares their age categories:
- **Platform younger than self-reported** (for example, platform says "child" but player says "adult"): returns `AGE_CONFLICT` (400 error)
- **Platform older than self-reported:** permissible; the more conservative (lower) age is used
- **Same category:** no conflict; proceeds normally
:::info
Age conflict detection is a per-developer feature flag. Contact k-ID to enable it.
:::
## Use with widget URL endpoints
If you're using the embedded VPC widget instead of calling `/age-gate/check` directly, you can still pass a platform age signal. Both [`/widget/generate-age-gate-url`](/api/endpoints/generate-age-gate-url) and [`/widget/generate-e2e-url`](/api/endpoints/generate-e-2-eurl) accept a `platformAgeSignal` in the request body and forward it to the age-gate check the widget performs internally.
The widget then behaves the same way as a direct API integration: a verified signal can skip the age gate, satisfy verified-age permissions without an extra verification step, and trigger age conflict detection when it disagrees with the player's self-reported age.
```json
POST /api/v1/widget/generate-e2e-url
Content-Type: application/json
Authorization: Bearer your-api-key
{
"jurisdiction": "US-CA",
"platformAgeSignal": {
"name": "apple-ios",
"ageLow": 18,
"ageHigh": 25,
"declarationType": "governmentIDChecked"
}
}
```
For end-to-end widget usage, see the [VPC quick start](/get-started/quickstart-guides/vpc) and the [Embedded flow guide](../embedded-flow.md).
## Next steps
- For exact request shapes, platform-specific notes, and validation rules, see [Platform signal details](./platform-details.md).
- For verified-age permissions and session upgrade behavior, see [Age assurance for high-risk features](../age-assurance.md).
---
// File: cdk/age-signals/platform-details
# Platform signal details
This page is the implementation-focused companion to [Platform age signals](./overview.md). Use it when you need exact field shapes, endpoint behavior, and examples you can map directly into your client or server-side integration.
## Choose the right signal shape
| Platform | Native signal type | What to send to k-ID | Verified for age requirements? | Notes |
| --- | --- | --- | --- | --- |
| Apple iOS | Numeric age range | `ageLow`, `ageHigh`, optional `declarationType` | Yes, for verified declaration types | Don't send `category` |
| Google Play | Numeric age range | `ageLow`, `ageHigh`, optional `declarationType` | Yes, for verified declaration types | Don't send `category` |
| Xbox | Category | `category` | No | k-ID resolves the category using the player's jurisdiction |
| Meta Horizon | Category | `category` | No | k-ID resolves the category using the player's jurisdiction |
| k-ID | Prior verification | `verificationId`, plus optional `category` or range | Yes, if the verification is valid | `declarationType` is resolved server-side |
:::tip
If a platform already gives you `ageLow` and `ageHigh`, send those values directly. Only use `POST /age-gate/get-platform-age-range` for category-based platforms.
:::
## Platform-specific integration guide
### Apple iOS
**How to obtain the signal:** use the [Age Range Service](https://developer.apple.com/documentation/declaredagerange/agerangeservice) to get the player's age range and declaration type.
**What to send to k-ID:** `name`, `ageLow`, `ageHigh`, and optionally `declarationType`.
**Verified declaration types:** `confirmed`, `paymentChecked`, `governmentIDChecked`, `guardianPaymentChecked`, `guardianGovernmentIDChecked`
`confirmed` means Apple set the age range through a scrutinized method such as a credit card or government ID. k-ID doesn't treat `selfDeclared` or `guardianDeclared` as verified, so they don't bypass age verification requirements.
:::note Deprecated declaration types
Starting in iOS 26.5, Apple returns only `selfDeclared`, `guardianDeclared`, or `confirmed`, replacing the granular `*Checked` types with `confirmed`. k-ID still accepts the deprecated `paymentChecked`, `governmentIDChecked`, `guardianPaymentChecked`, and `guardianGovernmentIDChecked` types for backwards compatibility, so signals from earlier iOS versions keep working.
:::
```json
{
"name": "apple-ios",
"ageLow": 18,
"ageHigh": 25,
"declarationType": "confirmed"
}
```
### Google Play
**How to obtain the signal:** use the [Play Age Signals API](https://developer.android.com/google/play/age-signals/use-age-signals-api) to get the player's age range and `ageRangeSource`.
**What to send to k-ID:** `name`, `ageLow`, `ageHigh`, and optionally `declarationType`.
**Verified declaration types:** `TIER_C`, `TIER_D`, `VERIFIED`, `SUPERVISED`
```json
{
"name": "google-play",
"ageLow": 13,
"ageHigh": 17,
"declarationType": "TIER_C"
}
```
### Xbox
**How to obtain the signal:** use [XR-014](https://learn.microsoft.com/en-us/gaming/gdk/docs/store/policies/xr/xr014) and `XUserGetAgeGroup` to get the player's age group.
**What to send to k-ID:** `name` and `category`.
**Accepted categories:** `child`, `teen`, `adult`
**How k-ID interprets them:**
- `child` = `[0, digitalConsentAge - 1]`
- `teen` = `[digitalConsentAge, civilAge - 1]`
- `adult` = `[civilAge, 100]`
Xbox signals aren't considered verified for verified-age permissions.
```json
{
"name": "xbox",
"category": "adult"
}
```
### Meta Horizon
**How to obtain the signal:** use the [Get Age Category API](https://developers.meta.com/horizon/documentation/unity/ps-get-age-category-api/) to get `CH`, `TN`, or `AD`.
**What to send to k-ID:** `name` and `category`.
**Accepted categories:** `CH`, `TN`, `AD`
**How k-ID interprets them:**
- `CH` = `[10, digitalConsentAge - 1]` with a minimum age of 10
- `TN` = `[digitalConsentAge, civilAge - 1]`
- `AD` = `[civilAge, 100]`
For a typical Brazil configuration, that results in `CH=10-12`, `TN=13-17`, `AD=18+`.
Meta Horizon signals aren't considered verified for verified-age permissions.
```json
{
"name": "meta-horizon",
"category": "TN"
}
```
### k-ID
**How to obtain the signal:** use a previously completed k-ID verification and pass its `verificationId`.
**What to send to k-ID:** `name: "k-id"` and `verificationId`. You can also send `category` or `ageLow` and `ageHigh` if your flow needs them, but whether the signal counts as verified comes from the verification lookup.
**Server-side verification checks:** the signal is considered verified only when the verification:
1. Exists
2. Has status `PASS`
3. Belongs to the same organization as the requesting product
Caller-provided `declarationType` values are ignored for `k-id` signals.
```json
{
"name": "k-id",
"category": "adult",
"verificationId": "a50f4f73-3c0c-4720-a8b3-ec57ccb0aa34"
}
```
:::note
`verificationId` is limited to one active session per product within an organization. The same verification can be reused across different products in the same organization, but not by multiple active sessions in the same product.
:::
## API reference
The six endpoints below are the most relevant to platform age signal integration. Each entry describes when and why to call it. For the full request and response schema, follow the link to the API reference.
### [`GET /age-gate/get-requirements`](/api/endpoints/get-age-gate-requirements)
Call this before showing the age gate. Pass platform signal fields as query parameters (`platformName`, `platformAgeLow`, `platformAgeHigh`, `platformCategory`, `platformDeclarationType`, `platformVerificationId`) to let k-ID factor the signal in before you even collect input.
Key response fields to act on:
- `shouldDisplay`: `false` when a verified platform signal proves the player is an adult; skip your age gate UI when this is `false`
- `ageAssuranceRequired`: `false` only when every verified-age permission is already satisfied by the signal
- `permissions`: lists permissions with a `verifiedAgeThreshold`; useful even when all are already satisfied
### [`GET /age-gate/get-default-permissions`](/api/endpoints/get-default-permissions)
Call this when you want to preview permission states without creating a session. Useful for checking whether a verified signal would immediately satisfy a threshold permission before the full age gate flow runs.
- Permissions with `verifiedAgeThreshold` default to `enabled: false`
- A verified signal with `ageLow >= threshold` sets those permissions to `enabled: true`
- For `k-id` signals, the declaration type is resolved server-side from `platformVerificationId`
### [`GET /session/get`](/api/endpoints/get-session)
Refresh the session after `check`, VPC, or an age assurance challenge. The response includes an `ageVerification` object when a verified platform signal (or completed age assurance) has been recorded on the session. Check `ageVerification.platformName` and `ageVerification.declarationType` to understand the verification source.
### [`POST /age-gate/check`](/api/endpoints/check-age-gate)
Creates or updates the session. Pass a `platformAgeSignal` in the request body, either on its own or alongside `dateOfBirth`, `age`, or `kuid`. k-ID uses the more conservative of the two ages when both are present.
Possible outcomes relevant to platform signals:
- **Verified signal + age meets threshold:** session contains `ageVerification` and the permission is `enabled: true`
- **Signal present but not verified:** session is created without `ageVerification`; high-risk permission needs age assurance later
- **Player age below threshold:** permission is `managedBy: PROHIBITED`; the age assurance recovery flow applies
- **Age conflict:** returns `400 AGE_CONFLICT` when age-conflict detection is enabled and the platform signal indicates a younger age category than the player's self-reported age
### [`POST /age-gate/get-platform-age-range`](/api/endpoints/get-age-range-for-category)
Converts a platform category string into a concrete `ageLow`/`ageHigh` pair for a given jurisdiction. Only needed for category-based platforms (Xbox, Meta Horizon, k-ID). If the platform already gives you a numeric range, skip this call and send the values directly.
### [`POST /session/upgrade`](/api/endpoints/upgrade-session)
Requests additional permissions for a player's existing session. This is the endpoint that determines whether a high-risk permission can be unlocked immediately or needs an age assurance challenge first.
The challenge type returned depends on what's being requested:
| Scenario | `challenge.type` returned |
| --- | --- |
| Permission is `PLAYER`-managed | No challenge: permission enabled immediately |
| Permission is `GUARDIAN`-managed | `CHALLENGE_SESSION_UPGRADE` (trusted adult consent) |
| Permission has `verifiedAgeThreshold` and session has no `ageVerification` | `CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE` (player must verify age) |
| Permission has `verifiedAgeThreshold` and session already has `ageVerification` meeting the threshold | No challenge: permission enabled immediately |
**Key constraint:** all permissions in a single upgrade request must be of the same type. Mixing permissions with and without `verifiedAgeThreshold` in one call returns `400: "Can't mix permissions with and without verifiedAgeThreshold"`.
Example: request a high-risk permission (triggers age assurance):
```json
POST /api/v1/session/upgrade
Content-Type: application/json
Authorization: Bearer your-api-key
{
"sessionId": "b1a6482d-5242-4b4a-aa88-3fa52595a672",
"requestedPermissions": [
{ "name": "loot-boxes-paid-gameplay-impacting" }
]
}
```
Example: pass a k-ID signal from a completed age appeal to satisfy the threshold directly, without creating a new challenge:
```json
POST /api/v1/session/upgrade
Content-Type: application/json
Authorization: Bearer your-api-key
{
"sessionId": "b1a6482d-5242-4b4a-aa88-3fa52595a672",
"requestedPermissions": [
{ "name": "loot-boxes-paid-gameplay-impacting" }
],
"platformAgeSignal": {
"name": "k-id",
"verificationId": "a50f4f73-3c0c-4720-a8b3-ec57ccb0aa34"
}
}
```
When a `CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE` challenge is returned, direct the player to the `challenge.url` to complete age verification through AgeKit+. Once the challenge passes, retrieve the updated session: the permission is now `enabled: true` with an `ageVerification` object on the session.
## Validation and edge cases
### Input validation
| Scenario | Result |
| --- | --- |
| `platformAgeSignal` with missing `name` | 400: "Platform name must be provided" |
| Unknown platform name | 400: "Unknown platform name" |
| `apple-ios` or `google-play` with `category` | 400: "Platform must have age range specified" |
| Both `category` and `ageLow`/`ageHigh` provided | 400: "Provide either category or `ageLow` and `ageHigh`, not both" |
| Only `ageLow` without `ageHigh` (or vice versa) | 400: "`ageLow` and `ageHigh` must both be provided" |
| `ageLow` greater than `ageHigh` | 400: "Invalid range" |
| `ageLow`/`ageHigh` outside supported bounds | 400: "`ageLow` and `ageHigh` must satisfy `0 <= ageLow <= ageHigh <= 100`" |
| Mixed threshold and non-threshold permissions in `session/upgrade` | 400: "Can't mix permissions with and without verifiedAgeThreshold" |
### Age conflict matrix
When both a primary age and a platform signal are present:
| Platform age category \ Primary age category | Primary: Minor | Primary: Youth | Primary: Adult |
| --- | --- | --- | --- |
| **Platform: Minor** | No conflict | **CONFLICT** | **CONFLICT** |
| **Platform: Youth** | No conflict | No conflict | **CONFLICT** |
| **Platform: Adult** | No conflict | No conflict | No conflict |
A conflict only happens when the platform says the player is **younger** than the self-reported age category.
### `k-id` signal security rules
- `declarationType` is always resolved server-side from `verificationId`
- Caller-provided `declarationType` is ignored
- The verification must be `PASS`
- The verification must belong to the same organization as the requesting product
- Without a valid `verificationId`, the `k-id` signal is treated as unverified
### Conservative defaults
- If `shouldDisplay` is `false` and you have no platform signal to send, call `POST /age-gate/check` with `age: 1`
- If the platform signal and primary age disagree but don't conflict, k-ID uses the lower age
- Verified-age permissions default to `enabled: false` until they're actually satisfied
---
// File: compliance-guides/coppa-2026-amendment
# 2026 COPPA Rule Amendments
:::warning Legal disclaimer
The information contained within this documentation is not intended to be a substitute for legal counsel and does not constitute legal advice. Please consult with your legal counsel for any questions regarding your compliance strategy.
:::
The U.S. Children's Online Privacy Protection Act (COPPA) Rule Amendments take effect on **April 22, 2026**. This guide walks operators subject to COPPA through the configuration changes required in the k-ID Compliance Studio to meet the new obligations.
## Who this guide is for
The new COPPA Rule Amendments apply to online operators that are considered "child-directed" under the law, which includes both services that target children directly and services that don't directly target children but might appeal to them. Use this guide alongside your legal counsel to update your product's configuration.
## What's changing
Two parts of the amended Rule drive the configuration updates below:
1. **Expanded Verifiable Parental Consent (VPC) disclosures.** Operators must include, in the VPC flow, a hyperlinked reference to a document that describes the third parties to which the operator discloses personal information and the purposes for those disclosures. This applies to **all** third-party recipients, whether the disclosure is integral to the service or not.
2. **Separate consent for non-integral disclosures.** Disclosures of a child's personal information classified as **non-integral** to the service now require their own parental consent. The Federal Trade Commission (FTC) has held that disclosures of a child's personal information to third parties for monetary or other consideration, for **advertising purposes** (for example, `targeted-advertising`), or to **train or otherwise develop AI technologies**, are **non-integral** to the online service.
Whether features beyond targeted advertising and AI training, such as text chat or push notifications, are "integral" is a judgment call that each operator must make based on their compliance strategy and risk appetite. Consult your legal counsel.
## Step 1: Add a third-party disclosures link to Developer Details
The new Rule requires a hyperlinked reference to a document that describes the third parties to which you disclose personal information, and the purposes for those disclosures. The link can point to a dedicated subsection of your existing privacy policy.
1. In the **Compliance Studio**, open your product and go to **Developer Details**.
2. Click **+ Additional Legal Links** and provide:
- The **Title** of the hyperlink (for example, "Third-Party Disclosures").
- The **URL** of the document or privacy-policy section.
3. Save.
The new link is displayed alongside your Privacy Policy and Terms of Service in the parent-facing VPC flow.

For the full set of options on this tab, including localized link titles and platform-specific variants, see [Developer details](/compliance-studio/product-basic-information#developer-details).
## Step 2: Identify non-integral features
Review your product's feature configuration and identify any features that:
1. **Involve sharing a child's personal information with a third party**, and
2. **Aren't integral to the functioning of the service.**
At a minimum, treat the following as non-integral per the FTC:
- Features that use personal information for **targeted advertising** (for example, the `targeted-advertising` permission).
- Features that send personal information to **train or develop AI models**.
If no features in your product meet both criteria, no further configuration changes are required and you can stop here. Otherwise, continue with the remaining steps for each affected feature.
## Step 2(a): Remove the essential feature label
Non-integral features can't be bundled with the rest of the service as Essential; they require separate parental consent.
1. In the **Compliance Studio**, open your product and go to **Configuration → Permissions**.
2. Locate the non-integral feature and click **Customize**.
3. Remove the **Essential Feature** label from the permission.


For background on how permissions, essential features, and consent interact, see [Permissions](/concepts/access-features-consent/permissions) and [Essential features](/concepts/access-features-consent/essential-features). For the Studio tab itself, see [Permissions configuration](/compliance-studio/product-api-configuration#permissions).
### Developer impact
Once a feature is no longer marked Essential, it's gated by the per-session permission set that the k-ID API returns. Your product must check the permission on each relevant session and disable the feature when the parent hasn't granted consent for it. Review the permission flags on the session by calling [`/session/get`](/api/endpoints/get-session), and handle updates to a session's permissions via the [`session.changepermissions`](/events/webhooks/event-types/session-changepermissions) webhook. For background, see [Sessions](/concepts/access-features-consent/sessions) and [Permissions](/concepts/access-features-consent/permissions).
## Step 2(b): Amend the data notice
Update the product's Data Notice so that the data elements associated with each non-integral feature require parental approval. For example, if `targeted-advertising` relies on **Advertising Identifiers**, that data element must be marked as requiring parental approval.
1. In the **Compliance Studio**, open your product and go to **Notices → Data Notices**.
2. For each data element tied to a non-integral feature, mark the element as requiring parental approval.
3. Save.

For more detail on the Data Notice configuration, see [Data Notices](/compliance-studio/product-notices) and the [Data notices concept page](/concepts/data-notices).
## Recap
- Add a third-party disclosures hyperlink under **Developer Details → + Additional Legal Links**.
- For any feature that shares a child's personal information with a third party and isn't integral to the service:
- Remove the **Essential Feature** label under **Configuration → Permissions**.
- Update your code to read the per-session permission flags and disable the feature when consent hasn't been granted.
- Mark the related data elements as requiring parental approval under **Notices → Data Notices**.
Confirm each change with your legal counsel, validate the parent experience end to end in [Test Mode](/concepts/testing), and update your product before **April 22, 2026**.
---
// File: compliance-studio/overview
# Compliance Studio
Compliance Studio is the web portal where publishers configure age-appropriate design compliance for their digital products. It's where you manage products, configure compliance settings, and set up verification methods -- all from a single interface at [`portal.k-id.com`](https://portal.k-id.com).
## Who is Compliance Studio for?
Compliance Studio is designed for anyone involved in making digital products safe and compliant for young audiences:
- **Publishers and developers** building games, apps, and digital experiences that need age-appropriate compliance
- **Compliance and legal teams** responsible for ensuring products meet regulatory requirements across jurisdictions
- **Product managers** who need to understand how compliance rules affect feature availability
Within an organization, team members can be assigned one of several roles based on their responsibilities:
| Role | Purpose |
|------|---------|
| **Owner** | Full access to all organization settings, billing, and resources |
| **Admin** | Manages members, products, and organization settings |
| **Member** | Views and edits products |
| **Developer** | Scoped to specific products, with access to developer settings |
| **Customer Support** | Access to event logs and support tools |
| **Product Evaluation** | Read-only access for product evaluation |
## Key capabilities
- **Product configuration** -- Define your product's details, target audiences, and compliance requirements
- **Global Compliance Engine** -- Automatically applies jurisdiction-specific rules across 200+ markets based on your product's configuration
- **Verification method configuration (AgeKit+)** -- Set up and manage [age verification methods](/concepts/verification-methods) including facial age estimation, ID document verification, AgeKey, and more
- **Data notices** -- Configure and customize [data notices](/concepts/data-notices) that inform users about data collection and usage
- **Testing and publishing workflow** -- Test your compliance configuration in a sandbox environment before publishing to production
## How this guide is organized
This section covers everything you need to know about using Compliance Studio:
- **[Getting Started](getting-started)** -- Accept your invitation, log in, and navigate the portal
- **[Managing Your Organization](managing-organization)** -- Configure your organization, manage team members, and set up SSO
- **[Managing Your Account](managing-account)** -- Update your profile, security settings, and active devices
- **[Creating a Product](creating-product)** -- Set up a new product in Compliance Studio
- **Product Configuration**
- [Basic Information](product-basic-information) -- Product details and developer information
- [Notices](product-notices) -- Data notices and custom notices
- [Verification](product-verification) -- Adult age verification setup
- [API Configuration](product-api-configuration) -- Permissions, engine overrides, and multi-product setup
- [Product Policies](product-policies) -- Attach and manage compliance policies
- **[Testing & Publishing](testing-and-publishing)** -- Test in sandbox and publish to production
- **[Developer Settings](developer-settings)** -- API keys, webhooks, and environment configuration
---
// File: compliance-studio/getting-started
# Getting started with Compliance Studio
This guide walks you through accepting your invitation, logging in for the first time, and finding your way around the Compliance Studio portal. By the end, you're ready to create your first product and start configuring compliance settings.
## Getting access
- **New organizations** are created by a k-ID representative. Contact your k-ID account representative or reach out to [k-ID](https://k-id.com) to get started.
- **Additional users** are invited by an existing **Owner** or **Admin** within the organization. You'll receive an email invitation with a link to accept and set up your account. See [Inviting new members](managing-organization#inviting-new-members) for details.

## Logging in
Once you've accepted your invitation and set up your account, log in at [`portal.k-id.com`](https://portal.k-id.com) with your email and password.
If your organization has configured Single Sign-On (SSO), you can authenticate through your corporate identity provider instead. See [Configuring SSO](managing-organization#configuring-sso) for details on how SSO is set up.
## Navigating the portal
Compliance Studio uses a collapsible sidebar on the left side of the screen for navigation.

### Sidebar layout
The sidebar is organized into the following sections from top to bottom:
| Section | Description |
|---------|-------------|
| **Organization switcher** | At the top of the sidebar, displays your current organization name. Click to switch between organizations or manage organization settings. |
| **Home** | Returns you to the main dashboard |
| **Products** | Expandable section listing all products in your organization. Click the arrow to expand and see individual products, or click **All Products** to view the full list. |
| **Administration** | Access to organization management, member settings, and other administrative tools |
The bottom of the sidebar includes links to:
- **Documentation** -- Opens the k-ID Developer Hub
- **FAQ** -- Frequently asked questions
- **Glossary** -- Definitions of key terms
- **Notifications** -- View recent alerts and updates
- **User profile** -- Access your account settings and sign out
### Collapsing the sidebar
Click the collapse icon at the top of the sidebar to minimize it, giving you more screen space when working with product configurations.
## Understanding your dashboard
The dashboard is your home screen in Compliance Studio. It provides an at-a-glance view of your recent activity and quick access to common tasks.

### Recently updated products
The top of the dashboard displays your most recently updated products, making it easy to pick up where you left off.
### Quick-access cards
Below your recent products, the dashboard shows quick-access cards organized into sections. These cards provide shortcuts to commonly used features and tools. The specific cards you see depend on your role within the organization and the features enabled for your account.
## Organization context
Everything in Compliance Studio -- products, settings, members, and configurations -- is scoped to the currently selected organization.
If you belong to multiple organizations, use the **organization switcher** at the top of the sidebar to change your active organization. When you switch organizations, the product list, dashboard, and all settings update to reflect the selected organization's data.
:::warning
Make sure you've selected the correct organization before creating or modifying products. Products belong to the organization that was active when they were created.
:::
---
// File: compliance-studio/managing-organization
# Managing your organization
This guide covers how to manage your organization's profile, team members, roles, and security settings in Compliance Studio. Organization management is available to users with the **Owner** or **Admin** role. If you have a different role, contact your organization's Owner or Admin to request changes.
## Organization profile
Your organization profile defines how your organization is displayed within Compliance Studio.
### Organization name and logo
Update your organization's display name and upload a logo through the organization settings. The logo is displayed in the sidebar and in any shared contexts within Compliance Studio.
### Verified domains
Verified domains control how new members can join your organization. After adding a domain, you can choose between:
- **No automatic enrollment** -- Members can only join through a direct invitation
- **Automatic suggestions** -- Users with a matching email domain receive a suggestion to request access, but must still be approved by an administrator
Verified domains also play an important role in [SSO configuration](#configuring-sso), as they determine which users are redirected to your identity provider during login.
## Team members
### Viewing the member list
The member list displays all current members of your organization along with their names, email addresses, and assigned roles. Use the search bar to find specific members.
The members view includes three tabs:
- **Members** -- Current active members
- **Invitations** -- Pending invitations that haven't been accepted yet
- **Requests** -- Pending join requests from users with matching verified domains
### Inviting new members
1. Navigate to the organization settings and select **Members**
2. Click the **Invite** button
3. Enter one or more email addresses, separated by commas or spaces
4. Select a role from the dropdown
5. Click **Send invitations**
Invited members are shown as **(Pending)** in the member list until they accept the invitation. You can resend or delete pending invitations from the **Invitations** tab.
## Roles and permissions
Each member is assigned a role that determines what they can access and modify within Compliance Studio.
| Role | Capabilities |
|------|-------------|
| **Owner** | Full access to all organization settings, billing, and resources. Can transfer ownership and delete the organization. |
| **Admin** | Manages members, products, and organization settings. Can't manage billing or transfer ownership. |
| **Member** | Can view and edit products within the organization. Can't manage members or organization settings. |
| **Developer** | Can access developer settings and product configurations. Can be [scoped to specific products](#product-level-access-for-developers) rather than all products. |
| **Knowledge** | Access to regulatory intelligence tools (KnowledgeKit) only. Can't view or manage products. |
| **Customer Support** | Access to event logs and support tools. Can't modify product configurations. |
| **Product Evaluation** | Read-only access for evaluating products. Can't make changes to any settings. |
### Changing a member's role
1. Locate the member in the member list
2. Click the dropdown next to their current role
3. Select the new role
Role changes take effect immediately.
## Product-level access for Developers
Members with the **Developer** role can be restricted to specific products within your organization, rather than having access to all products. This provides fine-grained access control when different developers work on different products.
To assign product access:
1. Navigate to **Member Access** in the organization settings
2. Find the Developer whose access you want to configure
3. Click **Select Products** and choose the products they should have access to
:::tip
Only the Developer role supports product-level access restrictions. Owners and Admins always have access to all products.
:::
## Leaving an organization
Any member can leave an organization through the organization settings. Click **Leave organization** and confirm your decision.
:::warning
Leaving an organization immediately removes your access to all of its resources. This action can't be undone -- you'll need a new invitation to rejoin.
:::
**Owners** must transfer ownership to another member before they can leave. Every organization must have at least one Owner.
## Configuring SSO
Single Sign-On (SSO) lets your organization members authenticate using their existing corporate identity credentials. k-ID supports SAML 2.0 and EASIE protocols.
Single Sign-On setup is managed by k-ID support. Below is a summary of the process.
### Setup process
1. **Contact k-ID support** -- Reach out to your account representative or k-ID support to initiate SSO setup
2. **Configure your Identity Provider** -- Using the details provided by k-ID (ACS URL, Entity ID, and metadata URL), create a k-ID application in your IdP (Okta, Azure AD, Google Workspace, OneLogin, and others)
3. **Exchange metadata** -- Share your IdP's metadata URL or XML file with your k-ID representative
4. **Test the connection** -- k-ID configures and tests the SSO connection on their end
5. **Enable SSO** -- Once verified, SSO is activated for your organization
### Impact of enabling SSO
Once SSO is enabled, members with email addresses matching your verified domains can **no longer log in with their k-ID password**. All authentication is handled through your identity provider.
### Troubleshooting
If members experience issues logging in after SSO is enabled:
- Verify the user is assigned to the k-ID app in your identity provider
- Confirm the user's email domain matches a verified domain in k-ID
- Check that attribute mappings (`email`, `first name`, and `last name`) are configured correctly in your IdP
- Review your identity provider's logs for authentication errors
- Contact k-ID support for further assistance
---
// File: compliance-studio/managing-account
# Managing your account
This guide covers how to manage your personal account settings in Compliance Studio, including your profile, security settings, and active devices. All users have access to these settings regardless of their role within an organization.
## Profile settings
Your profile settings control how your name and avatar appear throughout Compliance Studio.
### Display name and profile picture
Update your display name and profile picture from the **Profile** section of your account settings:
1. Click your profile avatar in the lower left corner of the sidebar
2. Select **Manage account**
3. Under **Profile details**, click **Update profile**
4. Change your display name or upload a new profile picture
5. Save your changes
Your updated profile information is displayed across all organizations you belong to.
## Email
Your account settings display the email addresses associated with your account. Your primary email is marked with a **Primary** badge and is used for login and notifications.
### Enterprise accounts
If your organization uses SSO or you've connected an enterprise identity provider (such as Google Workspace), your linked enterprise accounts are displayed in this section. Enterprise accounts allow you to authenticate using your corporate credentials.
## Password and security
### Changing your password
1. Navigate to **Security** in your account settings
2. Click **Update password**
3. Enter your current password and your new password
4. Confirm the new password and save
### Multifactor authentication (MFA)
Enable two-step verification to add an extra layer of security to your account. Compliance Studio supports authenticator applications as the verification method.
To enable MFA:
1. Navigate to **Security** in your account settings
2. Locate the **Two-step verification** section
3. Click the menu icon to configure your authenticator app
4. Scan the QR code with your authenticator app (such as Google Authenticator, Authy, or 1Password)
5. Enter the verification code to complete setup
Once enabled, you'll be prompted for a verification code from your authenticator app each time you log in.
:::tip
Enabling MFA is strongly recommended for all users, especially those with Owner or Admin roles.
:::
## Active devices
The **Active devices** section shows all devices where you're currently signed in to Compliance Studio. Each entry displays:
- Device name
- Browser and version
- IP address and approximate location
- Last active time
Your current session is labeled **This device**.
### Revoking device access
If you notice an unrecognized device or want to end a session on another device, you can revoke access directly from this list. Revoking a device immediately signs out that session.
## Signing out
To sign out of your current session:
1. Click your profile avatar in the lower left corner of the sidebar
2. Select **Sign out** from the menu
To sign out of all sessions across all devices, use the active devices section to revoke access to each device individually.
:::tip
Changes to your account settings take effect immediately across all k-ID services.
:::
---
// File: compliance-studio/creating-product
# Creating a product
Every compliance configuration in Compliance Studio starts with a product. A product represents a digital product (a game, app, website, or platform) that you're configuring for age-appropriate design compliance. Before you can set up age gates, permissions, or data notices, you need to create a product and understand its lifecycle. For background on how products relate to your organization and account system, see [Account System Product](../concepts/account-system-product).
## Creating a new product
1. In the left sidebar, click **Products** to expand the products section, then click **All Products**.

2. On the Products page, click the **New Product** button in the upper-right corner.

3. Enter a name for your product.
4. Click **Create**. Your new product is created in **Draft** state and you're taken to the product editor.

## Cloning from an existing product
When creating a new product, you can start from a template by cloning the configuration of an existing product:
1. In the new product dialog, select **New Product with existing configurations**.
2. Search for and select the product you want to clone from.
3. Click **Create**.
The new product is created with the name **Copy - [original product name]** and inherits all configuration settings from the source product. You can rename it and adjust any settings as needed.
This is particularly useful when you're setting up multiple products that share similar compliance requirements, for example, a franchise of games that all target the same age groups and jurisdictions.
## Product states
Every product moves through a series of states as you configure, test, review, and publish it. Understanding these states helps you know where a product stands in the publishing workflow.
| State | Description |
|-------|-------------|
| **Draft** | The initial state when a product is first created. It has never been published to production. |
| **Has Local Changes** | A previously published product that has configuration changes that haven't been pushed. These changes exist only in the editor and haven't been pushed to any environment. |
| **Pushed to Test** | Configuration changes have been pushed to the test environment. You can now test your integration against these settings before submitting for review. |
| **In Review** | The product has been submitted for k-ID review. The k-ID team evaluates your configuration for compliance completeness. |
| **Approved** | The review has been approved. The product is ready to be published to production. |
| **Rejected** | The review has been rejected. Feedback from the k-ID team is provided so you can address any issues and resubmit. |
| **Published** | The product configuration is live in the production environment and actively serving compliance rules. |

## Product entitlements
Your organization's setup determines which features and configuration tabs are available for each product. There are two main entitlement types:
### CDK (Compliance Development Kit)
The full compliance integration suite. CDK products have access to age gate, sessions, permissions, challenges, data notices, and the complete API configuration. This is the most comprehensive option for products that need end-to-end compliance management.
### AgeKit+
Age verification and assurance features. AgeKit+ products can configure verification methods and age assurance flows without the full session and permission management that CDK provides.
The entitlement type assigned to your product determines which configuration tabs appear in the product editor. For example, the Notices and API Configuration tabs are only available for CDK products.
---
// File: compliance-studio/product-basic-information
# Product configuration: Basic information
The Basic Information section covers the foundational details of your product: its name, description, imagery, developer information, and branding. To access these settings, navigate to your product and click **Edit**.
## Product details
The product details section captures the core identifying information for your product.
| Field | Description |
|-------|-------------|
| **Product Name** | The display name of your product, shown throughout Compliance Studio and in k-ID widgets. |
| **Product Description** | A description of your product. This can be localized into multiple languages to support your target markets. |
| **Product Type** | The category of your digital product (for example, game, app, website, platform). |
| **Primary Language** | The default language for your product's compliance content. |
| **Account System Product** | Toggle this on if your product uses an account system. See [Account System Product](../concepts/account-system-product) for details on how this affects cross-product session management. |

## Product image
Two images represent your product across Compliance Studio and k-ID surfaces:
- **Logo**: 512×512 pixels. Displayed as the product icon throughout the portal.
- **Banner**: 2430×810 pixels (a 3:1 aspect ratio). Displayed at the top of the product detail page and across k-ID surfaces.
Both images can be uploaded via drag-and-drop or by using the file picker.
### Banner safe area
You upload a single 3:1 banner, but it renders at different aspect ratios depending on the surface and orientation. Each surface crops the image from the center:
| Surface | Rendered ratio | Crop behavior |
|---------|----------------|---------------|
| Mobile portrait and desktop | 2:1 | Shows the middle of the image width; the left and right edges crop off. |
| Mobile landscape | 4:1 | Keeps the full width and shows the middle of the image height; the top and bottom edges crop off. |
Because the asset is cropped tall on some surfaces and wide on others, the only region guaranteed to stay visible everywhere is the central 60% of the upload: a **1458×486 pixel** box centered on the 2430×810 banner. Keep faces, characters, logos, and any other critical content inside this safe area.

Treat the outer edges as overflow. Extend background art (sky, ground, particles, color wash) all the way to the edges so the cropped surfaces still look complete. In mobile landscape, only a centered window of the banner is shown sharp and the rest of the image is stretched behind it as a progressive blur, so make sure the edges of your artwork still look good when blurred.
:::tip Do
- Center your key subject (face, character, wordmark) inside the central 60%.
- Extend background art to the full 2430×810 so the edges feed cleanly into crops and blur.
- Check that edge colors look good blurred.
:::
:::danger Don't
- Place faces or logos in the outer 20% of the width or height.
- Bake text or calls to action into the banner; edges blur and the bottom can be overlaid by interface elements.
- Put critical detail near the top or bottom edge, which crops off in landscape.
:::
## Developer details
Developer details provide contact information and legal links that are surfaced to users and parents through k-ID widgets and the Family Connect portal.
| Field | Description |
|-------|-------------|
| **Publisher Name** | Your company or studio name. |
| **Support Email** | The customer support contact email displayed to users. |
| **Privacy Policy URL** | A link to your product's privacy policy. Can be localized into multiple languages. |
| **Terms of Service URL** | A link to your terms of service. Can be localized into multiple languages. |
| **Customer Support URL** | A link to your support or help page. |
### Additional legal links
For products that operate across multiple platforms (for example, Xbox, PlayStation, PC), you can add platform-specific legal documents. Each additional legal link includes a **Variant ID** that can be used for platform targeting, allowing you to serve the correct legal documents based on which platform the user is accessing your product from.
### Contact emails
Configure email recipients for different notification types. This ensures that compliance-related communications, review updates, and alerts reach the right team members.

## Branding
Customize the appearance of k-ID widgets that are displayed to your users during age gate, consent, and verification flows.
| Setting | Description |
|---------|-------------|
| **Widget theme and colors** | Adjust the color scheme of k-ID widgets to match your product's visual identity. |
| **Dark mode** | Enable dark-themed widgets for your product. |
| **`What is k-ID?` button** | Control whether the informational `What is k-ID?` button is displayed on widgets. Disabling this hides the button from all widget surfaces. |
| **Use my policy links** | Replace the default k-ID Privacy Policy and Terms of Service links in the widget footer with the URLs you configured in [Developer details](#developer-details). When enabled, the `Powered by k-ID` branding is hidden. |

## Custom copy
Override the default text and messaging displayed in k-ID widgets with your own wording. This lets you tailor the language to better match your product's voice and tone (for example, customizing the age gate prompt, consent request text, or verification instructions).

---
// File: compliance-studio/product-notices
# Product configuration: Notices
Notices inform users and parents about your product's data practices and any additional information you want to communicate. This section is available for products with **CDK (Compliance Development Kit)** access.
## Data notices
Data notices inform parents and users about what data your product collects and how it's used. These disclosures are displayed during the consent and Verifiable Parental Consent (VPC) flow, giving parents visibility into your product's data practices before they grant permissions. See [Data Notices](../concepts/data-notices) for the full concept explanation.

### Selecting data elements
Choose from a predefined list of data categories that describe the types of data your product collects. Categories include personal information, location data, usage data, and more. Select every data element that applies to your product; this ensures parents receive an accurate and complete disclosure.
### Creating custom data elements
If the predefined list doesn't cover your specific data practices, you can create custom data elements. Type the name of your custom data element in the search bar and follow the prompts to add it with a name and description. Custom data elements are displayed alongside standard ones in the consent flow.
## Custom notices
Custom notices let you add additional information that's surfaced to users in specific markets or jurisdictions. Unlike data notices, which focus on data practices, custom notices can communicate any compliance-specific disclosures or product information you need to convey.

### Market-specific targeting
Each custom notice is targeted to one or more specific jurisdictions. This allows you to tailor disclosures to the regulatory requirements of individual markets (for example, adding a notice that's only displayed for users in a jurisdiction with specific disclosure obligations).
### Localized content
Provide a **title** and **description** for each custom notice in multiple languages to support your international user base.
### Use cases
- Compliance-specific disclosures required by particular jurisdictions
- Market-specific terms or conditions
- Additional product information that supplements your data notices
---
// File: compliance-studio/product-api-configuration
# Product configuration: API configuration
The API Configuration section controls how the k-ID Compliance Engine behaves for your product. These settings determine age requirements, feature permissions, parental preferences, compliance strategy, and jurisdiction-specific rules. This section is available for products with **CDK (Compliance Development Kit)** access.
## Product access
Configure who can access your product based on age and jurisdiction.
| Setting | Description |
|---------|-------------|
| **Default Global Minimum Age** | Set a global minimum age that applies across all jurisdictions. You can base this on civil age, digital consent age, or a custom age value. |
| **Data Lite Mode** | When enabled, minimizes data collection during the age gate flow. See [Data Lite Mode](../concepts/access-features-consent/data-lite-mode) for details. |
### Market-specific minimum age rules
Override the global minimum age for individual jurisdictions. Click **Add Market Specific Rules** to select a jurisdiction and set its minimum age requirement. This is useful when certain markets have stricter age requirements than your global default.
:::tip Product Policies
When a Product Policy is attached, the product access fields become **read-only**. The policy manages global minimums and market-specific rules.
:::

## Permissions
Permissions define what features in your product require consent from a parent or trusted adult. See [Permissions](../concepts/access-features-consent/permissions) for the concept explanation.
### Standard permissions
Enable predefined permission types from a list of common feature categories. Search or scroll through the available permissions and select the ones that apply.
### Custom permissions
Create your own permission types when the standard list doesn't cover a specific feature in your product. Each custom permission includes:
| Field | Description |
|-------|-------------|
| **Essential Feature** | Mark the permission as essential. Essential features can't be disabled by a parent or guardian. See [Essential Features](../concepts/access-features-consent/essential-features). |
| **Language / Localization** | Provide a title and description in multiple languages. The title is displayed to parents as the feature name, and the description explains what the feature does. |
Custom permissions also support age threshold configuration:
| Threshold | Behavior |
|-----------|----------|
| **Minimum Age** | The feature is always off for users below this age. Neither the user nor their parent/guardian can enable it. |
| **Threshold Age** | The feature is off by default for users below this age and can only be enabled by a parent/guardian. Users at or older than this age can enable it themselves. Age verification might be required; see [Age Assurance](../concepts/age-assurance). |
| **Default Off If Under Age** | The feature is off by default for users below this age but can be toggled by the user themselves (unless they're below the Threshold Age). |
:::tip Product Policies
When a Product Policy is attached, permissions included in the policy are managed by the policy. You can still add additional standard permissions on top of the policy's baseline.
:::

## Parental preferences
Parental preferences are questions presented to trusted adults and parents during the consent flow, allowing them to set boundaries for their child's experience. See [Trusted Adult Preferences](../concepts/access-features-consent/trusted-adult-preferences) for the concept explanation.
Two types of preference questions are available:
### Numerical preferences
A numeric value with a configurable minimum and maximum range. Use this for quantifiable limits.
**Example:** "How many hours per day can your child play?" with a range of 1–8 hours.
### Selection preferences
Multiple choice options with configurable defaults. Use this for feature toggles or categorical choices.
**Example:** "Which communication features should be enabled?" with options such as voice chat, text chat, and friend requests.

## Engine and overrides
Configure the compliance strategy that determines how the k-ID Global Compliance Engine applies rules to your product. See [Jurisdictions](../concepts/jurisdictions) for background on how jurisdiction-specific compliance works.

### Compliance strategy
Choose how compliance rules are applied across jurisdictions:
| Strategy | Behavior |
|----------|----------|
| **Global Compliance** | Apply the same set of rules everywhere, regardless of the user's jurisdiction. |
| **Use Conservative Age Globally** | Apply the most restrictive age requirement found across all jurisdictions to every user. This is the safest option when you want uniform, maximum-protection compliance. |
| **Country-Specific Compliance** | Configure rules on a per-jurisdiction basis. This gives you the most granular control over how your product behaves in different markets. |
### Global overrides
Set baseline compliance configuration that applies across all jurisdictions as a starting point. These overrides provide a floor for compliance behavior that jurisdiction-specific overrides can then build upon or tighten.
### Jurisdiction-specific overrides
Override the global or baseline settings for individual jurisdictions with market-specific rules. This allows you to fine-tune compliance behavior for markets that have unique regulatory requirements.
:::tip Product Policies
When a Product Policy is attached, the engine and override fields become **read-only**. The policy manages compliance strategy and jurisdiction rules.
:::
## Multi-product approval
Link related products that share the same user session, so parents can approve access to multiple products in a single consent flow. See [Multi-Product Approval](../concepts/multi-product-approval) for the concept explanation.
When multi-product approval is enabled, you can configure:
- **Essential products**: products the user must approve access to. These are presented as required during the consent flow.
- **Optional products**: additional products the user can opt in to. These are presented as optional choices during consent.
The configuration also shows which other products in your organization have marked this product as essential, giving you visibility into cross-product dependencies.
:::tip Product Policies
When a Product Policy is attached, multi-product approval settings become **read-only**.
:::

---
// File: compliance-studio/product-verification
# Product configuration: Verification
The Verification section controls how age verification and assurance is handled for your product. Different verification flows serve different purposes, and you can configure which verification methods are available and in what order they're presented. The available configuration depends on your organization's entitlements (CDK or AgeKit+).
## How verification configuration works
k-ID supports multiple verification flows, each serving a different compliance scenario. Within each flow, you configure a list of verification methods that determine how users prove their age.
Each method can be set to one of three states:
| State | Behavior |
|-------|----------|
| **Enabled** | Offered to users as a verification option |
| **Disabled** | Not offered to users |
| **Fallback Only** | Used only when primary methods fail or are unavailable |
Method ordering determines which methods are offered first. The priority order is:
1. Market-specific overrides (highest priority)
2. Global product-level configuration
3. k-ID defaults (lowest priority)
Use the drag handles to reorder methods within each level. The order you set determines the presentation order users see when multiple methods are available.
For deeper background on verification concepts, see [Verification Methods](../concepts/verification-methods) and [Age Assurance](../concepts/age-assurance).

## Market-specific overrides
Each verification flow supports market-specific overrides that let you customize verification methods for individual jurisdictions. This is useful when regulatory requirements differ across markets, for example requiring ID scanning in one country but allowing facial age estimation in another.
To add a market-specific override:
1. Open the verification flow you want to customize (Age Assurance, Age Appeal, Trusted Adult, or Parental Consent)
2. Click **Add Market Specific Rules** at the bottom of the flow configuration
3. Select a market from the jurisdiction dropdown
4. Configure the verification methods for that market using the same enable/disable/fallback-only controls and drag-to-reorder interface
Market-specific overrides take priority over the global configuration for that flow. Users in the selected jurisdiction see the override configuration instead of the global defaults. You can add overrides for as many markets as needed, and each override is displayed as a collapsible accordion that can be expanded to edit or deleted when no longer needed.

## Age assurance
:::info
Age assurance configuration requires an **AgeKit+** or **CDK** entitlement.
:::
The age assurance flow verifies a user's age with high confidence. This is the primary verification flow used when your product needs to confirm that a user meets an age threshold (for example, verifying that a user is 18 or older to access age-restricted content).
Available methods include facial age estimation, ID scanning, AgeKey, ConnectID, and others depending on your organization's configuration. For each method, choose whether it's Enabled, Disabled, or Fallback Only, and drag to reorder methods to set presentation priority.

### GCE-driven age assurance
When enabled, the Global Compliance Engine automatically determines which verification methods are required based on the user's jurisdiction and applicable regulations. Rather than relying solely on your manual configuration, the engine evaluates regulatory requirements in real time and adjusts the available methods accordingly.
This is recommended for products operating across multiple jurisdictions where verification requirements vary by region.
:::note
When a Product Policy is attached to your product, age assurance settings become read-only. To change them, update the policy or detach it from the product.
:::
## Age appeal
:::info
Age appeal configuration requires an **AgeKit+** or **CDK** entitlement.
:::
Users who believe their age was incorrectly assessed can appeal the determination. The age appeal flow lets you configure which verification methods are available when a user challenges their initial age result.
Configure the available methods by using the same enable/disable/fallback-only controls and drag-to-reorder interface as age assurance. You might want to offer different methods for appeals, for example, allowing ID document scanning as a fallback for users whose facial age estimation produced an inaccurate result.

## Trusted adult
:::info
Trusted adult verification requires an **AgeKit+** or **CDK** entitlement.
:::
When a parent or guardian needs to verify their identity as a trusted adult (for example, to grant parental consent or manage a child's permissions), the trusted adult flow determines which verification methods are available.
Configure the available methods and their ordering by using the same controls as other flows. The methods you enable here apply specifically to the adult verification step, not to the child's age determination.

## Parental consent
:::info
Parental consent verification requires a **CDK** entitlement.
:::
When parental consent is required for a minor, the parent must verify their identity before consent can be granted. The parental consent flow lets you configure which verification methods parents can use during this process.
This flow uses the same configuration pattern as other verification flows. Enable, disable, or set methods to fallback only, and drag to reorder. The methods available here are specifically for the parent's identity verification as part of the consent workflow.

---
// File: compliance-studio/product-policies
# Product policies
Product Policies let you define a shared compliance configuration once and apply it across multiple products. This is especially useful for organizations with many products that share similar compliance requirements, ensuring consistency and reducing the effort of maintaining individual product configurations.
:::info Controlled feature
Product Policies is a controlled feature, gated by an organization-level setting that only k-ID can grant. If it isn't enabled for your organization, contact your k-ID account representative or reach out to [k-ID](https://k-id.com) to request access.
:::
## What are product policies
A policy is a reusable set of compliance settings that covers product access, permissions, engine overrides, verification methods, and parental preferences. Rather than configuring each product individually, you define these settings once in a policy and attach it to as many products as you need.
Key characteristics:
- One policy can be attached to many products
- Each product can only have one policy at a time
- When a policy is attached, the covered settings become read-only on the product (with partial editability for permissions)
- Policies help ensure consistency across your product portfolio and reduce configuration drift
## Creating a policy
1. In the left sidebar, click **Policies** to navigate to the policies section.

2. Click **Create Policy** in the upper-right corner.

3. Enter a name and description for the policy. Choose a name that reflects the shared compliance profile, for example, "US COPPA Standard" or "EU Kids Games Policy."
4. Configure the policy settings across the available tabs:
- **Product Access**: define age-based access rules
- **Permissions**: configure feature permissions
- **Parental Preferences**: set trusted adult and parental consent preferences
- **Engine & Overrides**: configure Global Compliance Engine behavior and jurisdiction overrides
- **Verification**: set up Age Assurance, Age Appeal, Trusted Adult, and Parental Consent verification methods
5. Optionally attach the policy to one or more products during creation.

## Attaching a policy to a product
From the product edit page, use the **Policy Selector** to choose a policy.

When you select a policy, a diff modal is displayed showing what changes when the policy is applied. Review the differences carefully before confirming.

Once a policy is attached:
| Configuration area | Editability |
|---|---|
| **Product Access** | Read-only (managed by policy) |
| **Engine & Overrides** | Read-only (managed by policy) |
| **Verification** | Read-only (managed by policy) |
| **Permissions** | Partially editable; policy-defined permissions are managed by the policy, but you can add product-specific permissions |
You can switch to a different policy at any time by selecting a new one from the Policy Selector.
## How policy application works
When a policy is applied to a product, the policy's configuration overwrites the product's compliance settings for most areas. The key exception is permissions, which use a merge strategy.
### Application rule
For Product Access, Engine & Overrides, and Verification, the policy fully overwrites the product's configuration. The product's previous values for these areas are replaced.
### Permissions merge strategy
Permissions use a merge approach: permissions defined in the policy are managed by the policy and can't be edited at the product level. However, you can still add product-specific permissions that exist alongside the policy-managed ones.
### When changes take effect
Policy changes don't take effect immediately for end users. Changes are applied when you **push to test** or **push to live**, following the same staging workflow as direct product configuration changes.
## Policy drift
Drift occurs when a policy is updated but one or more attached products haven't yet received the latest configuration. This can happen when someone edits the policy but hasn't pushed the changes to the attached products.

Compliance Studio detects drift and shows indicators on affected products. To resolve drift:
1. Navigate to the affected product
2. Review the pending policy changes
3. Accept the update to align the product with the current policy
## Detaching a policy
When you detach a policy from a product:
- The current configuration is **copied** to the product as editable values
- All fields become directly editable again
- The product retains the configuration it had while the policy was attached, so nothing is lost
You can re-attach the same policy or a different one at any time.
## Policy engine updates
When the Global Compliance Engine is updated with new regulatory requirements, both policies and directly configured products might need to accept the updates.
For products with an attached policy, the engine update might need to be accepted at the **policy level first**. Once the policy accepts the engine update, the changes flow down to all attached products. This ensures that the policy remains the single source of truth and that all attached products stay in sync.
---
// File: compliance-studio/testing-and-publishing
# Testing and publishing
Compliance Studio provides a structured workflow for testing and publishing your product configuration. Changes are staged through test and live environments, with a review process to ensure compliance before going live. For background on the testing model, see [Testing](../concepts/testing).
## Test and live modes
Every product has two environments, **Test** and **Live**.
| Environment | Purpose | API keys |
|---|---|---|
| **Test** | Verify your configuration and integration during development | Test API keys |
| **Live** | Serve compliance rules to real users in production | Live API keys |
Changes made in the product editor are **local** until explicitly pushed to an environment. This means you can freely edit your configuration without affecting either environment until you're ready.
## Pushing to test
After configuring your product, push your changes to the test environment to make them available via test API keys.
1. In the product editor, click **Push to Test**.
2. Confirm the push in the dialog that's displayed.
Your test configuration is now active. Use your test API keys to verify that your integration works correctly with the new settings. You can push to test as many times as needed; each push overwrites the previous test configuration.
:::tip
Make it a habit to push to test and validate your integration after every significant configuration change. This catches issues early, before the review process.
:::
## Comparing test to live
Before publishing, review exactly what changes by using the comparison view. This shows the differences between your test and live configurations, organized into tabs:
- **Information**: product details and metadata changes
- **Configuration**: product access, permissions, and engine override changes
- **Notices**: data notice and custom notice changes
- **Verification**: verification method and flow changes
Change impact indicators highlight which areas have been modified, making it easy to focus your review on what's different.

## Submitting for review
Before your product can go live for the first time, or after significant configuration changes, you need to submit it for k-ID review.
1. From the product page, click **Submit for Review**.
2. The product state changes to **In Review** and a banner confirms the submission.
The k-ID team evaluates your configuration to ensure it meets compliance requirements for the jurisdictions and age groups your product targets. While in review, you can continue editing your local configuration, but you can't push new changes to test or live until the review is complete.
## Review outcomes
### Approved
Your product is cleared to publish. An approval banner is displayed on the product page with the option to push to live.
### Rejected
Feedback is provided explaining what needs to change. Review the feedback, make the necessary adjustments to your configuration, and resubmit for review.
## Publishing to live
Once your product is approved, push your configuration to the live environment to make it active for real users.
1. Click **Push to Live** from the approval banner or the product actions menu.
2. Confirm the publish in the dialog that's displayed.
Your compliance settings are now active and being served via live API keys. The product state updates to **Published**.
:::caution
Publishing to live affects real users immediately. Make sure you've thoroughly tested your configuration in the test environment and reviewed the comparison view before publishing.
:::
---
// File: compliance-studio/developer-settings
# Developer settings
The Developer Settings tab on your product detail page contains the technical configuration needed to integrate k-ID into your product. This includes API keys, webhook endpoints, target origins, and API URLs.
## API keys
Each product has separate API keys for test and live environments, keeping your development and production traffic isolated.
| Key type | Used for | Configuration source |
|---|---|---|
| **Test API keys** | Development and testing | Test environment configuration |
| **Live API keys** | Production | Published live configuration |
### Creating a key
1. Navigate to the **Developer Settings** tab on your product.
2. In the API Keys section, select the environment (Test or Live).
3. Click the create button and give the key a name, for example, "Production Server" or "Staging Environment."
4. The key is generated and displayed. Copy it immediately, as the full key isn't shown again.

You can create multiple keys per environment. This is useful when different services or deployment stages need their own keys, for example, separate keys for your game server and your analytics pipeline.
### Key security
API keys should be kept server-side and never exposed in client-side code. Treat them as you would passwords:
- Store keys in environment variables or a secrets manager
- Never commit keys to source control
- Never include keys in client-side JavaScript, mobile app bundles, or any code that ships to end users
If a key is compromised, revoke it immediately from the Developer Settings page and create a new one.
## Webhooks
Webhooks let k-ID notify your server when important events occur, such as a challenge state change, a verification result, or a session permission update.
### Configuring webhooks
Configure separate webhook endpoints for test and live environments:
1. In the Webhooks section, select the environment.
2. Enter your webhook URL. This is the endpoint on your server that receives POST requests from k-ID.
3. Save the configuration.
### Available event types
Webhook events cover the key moments in the compliance lifecycle:
- Challenge state changes
- Verification results
- Verification revocations
- Parental consent grants
- Session permission changes
- Session deletions
- Test events (for validating your webhook setup)

Each webhook delivery includes a signature header that you should verify to confirm the request originated from k-ID. For the full webhook event reference, including payload structures and signature verification, see [Webhooks](../webhooks).
## Target origins
Configure the allowed origins (domains) for client-side integrations. When using k-ID widgets or client-side APIs, requests are validated against these origins to prevent unauthorized usage.
Add your development, staging, and production domains. Each origin should be a full origin URL, for example:
- `https://yourgame.com`
- `https://staging.yourgame.com`
- `http://localhost:3000` (for local development)
:::tip
Remember to add all environments where your integration runs, including local development URLs. Requests from origins not in this list are rejected.
:::
## API URLs
Your product detail page displays the API base URLs for both environments. These are the URLs your server uses when making requests to the k-ID API.
| Environment | Usage |
|---|---|
| **Test API URL** | Used with test API keys during development |
| **Live API URL** | Used with live API keys in production |
Use the URLs shown on your product's Developer Settings page; don't construct them manually.
---
// File: compliance-studio/compliance-engine-updates
# Compliance Engine updates
The Global Compliance Engine (GCE) is the rules engine that powers k-ID's jurisdiction-specific compliance behavior. As regulations evolve worldwide, k-ID periodically updates the engine to reflect new legal requirements, updated guidance, and best practices. When an update is available for your product, you're notified and guided through the review and acceptance process.
## What are Compliance Engine updates
The GCE contains the rules that determine how your product behaves in each supported [jurisdiction](../concepts/jurisdictions) -- covering age requirements, consent rules, and verification requirements. These rules are what make your product automatically compliant across markets without you needing to track every regulatory change yourself.
Updates to the engine happen when:
- New laws or regulations take effect in a jurisdiction
- Existing requirements are amended or clarified
- Enforcement guidance is updated by regulatory authorities
- Best practices evolve based on industry developments
An engine update can affect your product's access settings, permission configurations, and verification requirements. Each update carries a version number and includes release notes describing exactly what changed, so you always know what's being updated and why.
## Notification and review
When a Compliance Engine update affects your product, a banner is displayed on the product detail page indicating that a new GCE version is available and your product needs updating.
If your product has a [Product Policy](product-policies) attached, the update might need to be accepted at the policy level first before it flows through to individual products.
Updates are never applied automatically. You always review and explicitly accept them, giving you full control over when compliance rule changes take effect.
## Reviewing an update
Click through the update banner to reach the engine update review page. The review page provides a detailed breakdown of what changed in this version:
- A **changelog** describing the update at a high level
- **Jurisdiction-level changes** showing how rules change for each affected market
- **Configuration changes** covering adjustments to age requirements and compliance rules
- **Permission-related changes** if the update affects how permissions are handled in certain jurisdictions
Take time to understand how the update affects your product before accepting. If you operate in multiple markets, pay particular attention to the jurisdictions where your product has the most users.
## Accepting an update
After reviewing the changes, accept the update to apply the new engine rules to your product configuration. Once accepted:
- The updated rules become part of your product configuration immediately in the editor
- You still need to **push to test** or **push to live** (or both) to deploy the changes to your environments
- The acceptance is tracked in your product's activity log for audit purposes
Accepting an update doesn't change your live product until you explicitly publish. This gives you the opportunity to test the new rules in your test environment before rolling them out to production.
---
// File: compliance-studio/data-requests
# Data requests
Data Requests in Compliance Studio help you manage data subject access requests (DSARs) and support requests from parents and families. Privacy regulations such as GDPR and COPPA give parents and guardians the right to request access to, deletion of, or corrections to their child's data. This section provides tools to view, manage, and resolve these requests.
:::note Role requirement
This page is accessible to **Owner** and **Admin** roles only. It's hidden from Member, Knowledge, Customer Support, and Developer roles.
:::
## What are data requests
Parents and guardians can submit requests through the Family Portal (Family Connect) when they need to exercise their data rights on behalf of their children. There are two types of requests:
- **Data Subject Access Requests (DSARs)** -- Formal requests to exercise specific data rights such as access, deletion, correction, portability, or objection to processing
- **Support requests** -- General inquiries from parents or guardians about their child's data or account
Requests are scoped to your organization's products. You are responsible for fulfilling these requests in accordance with the applicable regulations for each jurisdiction.
## Viewing requests
Navigate to **Requests** in the sidebar to access the requests dashboard. The requests table displays all incoming requests with columns for type, status, date submitted, and details.

### Filtering and sorting
Use the available filters to narrow down the requests list:
- **Search** -- Filter requests by keyword to find specific entries
- **Filter by type** -- Show only DSARs, only support requests, or all request types
- **Filter by resolution status** -- Show resolved, unresolved, or all requests
- **Sort by date** -- Order requests by newest first or oldest first
## Request details
Click any request to view its full details. For DSARs, the detail view shows which specific data right is being exercised:
| Right | Description |
|-------|-------------|
| **Deletion** | Right to erasure -- the requester wants their child's data deleted |
| **Access** | Right to access -- the requester wants a copy of their child's data |
| **Correction** | Right to rectification -- the requester wants inaccurate data corrected |
| **Portability** | Right to data portability -- the requester wants data in a portable format |
| **Objection** | Right to object to processing -- the requester objects to specific data processing |
The detail view also includes requester information, the date the request was submitted, and any additional context provided by the requester.
## Resolving requests
After you've fulfilled a request, mark it as **resolved** to track completion. You can also:
- **Unresolve** a request if further action is needed after initial resolution
- **Delete** requests that are invalid or duplicates
- **Copy a shareable link** to a specific request for team collaboration
## Exporting requests
Export your requests as JSON for record-keeping and audit purposes. The export includes all request data, status, and resolution history.
Use exports to support compliance audits, generate reports for your legal team, or maintain an independent record of how data rights requests were handled.
---
// File: concepts/age-signals
# Age signals
An age signal is information about a player's age that k-ID uses to determine what permissions and features they should have access to. Developers provide age signals from whatever sources they have, and k-ID processes them to ensure compliance.
## Types of age signals
| Type | Description |
| --- | --- |
| **Date of birth** | Provided directly by the player. Accepted formats: `YYYY`, `YYYY-MM`, or `YYYY-MM-DD` |
| **Self-attested age** | An approximate age entered by the player through an age gate |
| **Age estimation** | Estimated age from facial age estimation technology |
| **Platform age signal** | Age data from a game platform (Apple iOS, Google Play, Xbox, Meta Horizon, or k-ID). See [Platform age signals](/cdk/age-signals/overview) |
| **Verified age** | Age confirmed through ID document verification or a prior k-ID verification record |
## How k-ID handles age signals
- **Developers provide age signals**: If you've collected age information from any source (platform, existing account, prior session), pass it to k-ID. k-ID merges all available signals and uses the most conservative result.
- **CDK provides collection interfaces**: If you use the CDK, k-ID provides the age gate UI and collection flows.
- **VPC and Age Verification process signals**: These interfaces verify and act on the signals you provide to determine what the player can access.
## Platform age signals
Game platforms can send age data when a game launches. Passing this signal to k-ID can:
- **Skip the age gate** for players whose platform age signal is considered verified
- **Satisfy age verification thresholds** on high-risk permissions (for example, loot boxes in Brazil) without a separate step
- **Detect age conflicts** when a platform age contradicts a self-reported age
For integration details, supported platforms, verified declaration types, and the `PlatformAgeSignal` request shape, see [Platform age signals](/cdk/age-signals/overview).
## Age conflicts between primary age and platform age
When both a primary age (date of birth, self-attested age, or `kuid`) and a platform age signal are sent to `POST /age-gate/check`, k-ID can compare them:
- **Platform says the player is younger than their self-reported age category**: returns `400 AGE_CONFLICT` so your game can handle the discrepancy
- **Platform says the player is older than their self-reported age**: no conflict; k-ID uses the more conservative (lower) age
- **Same age category**: no conflict; the request proceeds normally
:::warning Age conflict detection is opt-in
This feature isn't enabled by default. Contact k-ID to enable age conflict detection for your product.
:::
Even without conflict detection enabled, k-ID always uses the lower of the two ages for permission resolution: an unverified platform signal can still restrict access, but it can't grant it.
## Constraining age gates with platform signals
To prevent age conflicts before they happen, use the platform signal to limit the ages a player can enter at the age gate. The `/age-gate/get-requirements` response includes two jurisdiction-specific boundary values:
- `digitalConsentAge`: The age of digital consent in the player's jurisdiction
- `civilAge`: The civil majority age
Use these to constrain your age gate UI:
| Platform category | Restrict input to |
| --- | --- |
| Child (`ageLow` < `digitalConsentAge`) | Ages lower than `digitalConsentAge` only |
| Teen (`ageLow` between `digitalConsentAge` and `civilAge`) | Ages between `digitalConsentAge` and `civilAge` |
| Adult (`ageLow` ≥ `civilAge`) | Ages at or greater than `civilAge` |
This ensures players can't claim an age that directly contradicts what the platform has already reported.
## Previously verified age
If you already know a player's age (for example, they're signed in to an account whose date of birth your platform holds), you don't need to show the age gate. Call `POST /age-gate/check` directly with the previously verified `dateOfBirth`. k-ID creates the session from that information without prompting the player for input.
## Age signal collection methods
The methods allowed for collecting age signals vary by jurisdiction. The `GET /age-gate/get-requirements` response includes an `approvedAgeCollectionMethods` array:
| Method | Description |
| --- | --- |
| `date-of-birth` | Full date of birth (`YYYY-MM-DD`) |
| `age-slider` | Age range or approximate age selection |
| `platform-account` | Age verification from an existing platform account |
When showing an age gate, use a neutral gate (no age pre-selected) so the player must actively set an age. If using a slider, the ESRB recommends capping the maximum display age at 35.
## Using age status
While permissions control individual features, some games need a coarser player experience tied to overall age. The `Session` object includes an `ageStatus` field:
| Value | Meaning |
| --- | --- |
| `DIGITAL_MINOR` | Below the age of digital consent |
| `DIGITAL_YOUTH` | At or greater than digital consent age, but below civil majority |
| `LEGAL_ADULT` | At or greater than the civil majority age |
For example, a game might suppress all in-game advertising for any player who isn't a `LEGAL_ADULT`, independent of any specific permission setting.
---
// File: concepts/overview
# Core concepts
Understanding k-ID's core concepts is essential for building compliant applications that properly handle age verification, parental consent, and user permissions. These concepts bridge the gap between legal requirements and technical implementation, helping you understand both *why* certain compliance measures are needed and *how* to implement them correctly.
## Why these concepts matter
Compliance with regulations such as COPPA, GDPR-K, and various regional laws requires understanding both the legal framework and the technical mechanisms that support compliance. k-ID's concepts map legal requirements to technical implementations, ensuring that your application:
- **Meets regulatory requirements** across 200+ markets
- **Protects users appropriately** based on their age and jurisdiction
- **Handles consent correctly** when parental approval is required
- **Manages permissions effectively** for different user types
## Core concepts
### Age and jurisdiction fundamentals
- **[Age signals](/concepts/age-signals)**: Understanding what age signals are and how to provide them to k-ID
- **[Jurisdictions](/concepts/jurisdictions)**: How geographic regions affect compliance requirements and age thresholds
- **[Age assurance](/concepts/age-assurance)**: The process of verifying or estimating a user's age to ensure compliance
### Access, features, and consent
The following concepts are essential for implementing Verifiable Parental Consent (VPC) flows:
- **[Sessions](/concepts/access-features-consent/sessions)**: Long-lived objects that track player permissions and age status
- **[Permissions](/concepts/access-features-consent/permissions)**: How to control access to game features based on age and consent
- **[Challenges](/concepts/access-features-consent/challenges)**: How consent challenges work when parental approval is required
- **[Verifiable Parental Consent (VPC)](/concepts/access-features-consent/vpc)**: The regulatory requirement and process for obtaining parental consent
- **[Age gate](/concepts/access-features-consent/age-gate)**: The mechanism for collecting and verifying user age
- **[Permissions upgrade](/concepts/access-features-consent/permissions#requesting-additional-permissions)**: Requesting additional permissions after initial consent
- **[Trusted adult preferences](/concepts/access-features-consent/trusted-adult-preferences)**: How parents configure consent and permission preferences
- **[Data-lite mode](/concepts/access-features-consent/data-lite-mode)**: Providing limited access while waiting for parental consent
- **[Essential features](/concepts/access-features-consent/essential-features)**: Features required for a product to function, even if they require consent
### Data and privacy
- **[Data notices](/concepts/data-notices)**: Disclosures about data collection, use, and sharing, and what data k-ID stores
### Advanced scenarios
- **[Multi-product approval](/concepts/multi-product-approval)**: Allowing parents to approve multiple products in a single consent flow
## Getting started
If you're new to k-ID, start with these foundational concepts:
1. **[Age signals](/concepts/age-signals)** - Understand what age information k-ID needs
2. **[Jurisdictions](/concepts/jurisdictions)** - Learn how location affects compliance
3. **[Sessions](/concepts/access-features-consent/sessions)** - Understand how permissions are tracked
4. **[VPC](/concepts/access-features-consent/vpc)** - Learn how parental consent works
For more implementation guidance, see the [Get started](/get-started/overview) section or choose your integration approach in [Choose integration](/get-started/choose-integration).
---
// File: concepts/jurisdictions
# Jurisdictions
Jurisdictions are geographic regions that have their own specific regulations regarding age verification, digital consent, and data protection. k-ID tracks regulations across 200+ markets and uses jurisdiction information to determine what compliance requirements apply to each user.
## What's a jurisdiction?
A jurisdiction is typically a country or a subdivision within a country (such as a state or province). Jurisdictions are identified using ISO codes:
- **ISO 3166-1 alpha-2 country codes**: Two-letter country codes such as `US`, `GB`, `DE`, `AU`
- **ISO 3166-2 subdivision codes**: Subdivision codes such as `US-CA` (California, United States), `GB-ENG` (England, United Kingdom), `DE-BY` (Bavaria, Germany), `AU-NSW` (New South Wales, Australia)
## Detecting location
While location can be self-declared by the player, the preferred way of getting location information is to call a service from the game that responds with information about the player's IP address. The location string can be either an ISO 3166-1 alpha-2 country code (for example, "US") or an ISO 3166-2 subdivision code (for example, "US-CA").
:::tip Best Practice
Always provide the subdivision code whenever possible. Even if a region currently doesn't have localized regulations, it might have them in the future. By providing the subdivision code, it's ensured that the player's permissions can be adjusted as regulations change.
:::
:::warning Security Considerations
- Some games have their own protections in place to detect and prevent location spoofing (for example, through players using VPNs). Make sure that the location that's sent to the k-ID API has been validated by these tools as applicable before it's sent to avoid an incorrect configuration.
- As a best practice, a player's jurisdiction is considered static when the player first launches the game, and can't be changed except if the customer files a support request - this is to discourage players from "forum shopping" to enable features that should otherwise be prohibited in their jurisdiction.
:::
One example to get the location for the player's IP address is to call a service such as [https://ipapi.co/json](https://ipapi.co/json), which produces results similar to the JSON shown below.
```json
{
"ip": "76.22.71.171",
"network": "76.22.68.0/22",
"version": "IPv4",
"city": "Issaquah",
"region": "Washington",
"region_code": "WA",
"country": "US",
"country_name": "United States",
"country_code": "US",
"country_code_iso3": "USA",
"country_capital": "Washington",
"country_tld": ".us",
"continent_code": "NA",
"in_eu": false,
"postal": "98027",
"latitude": 47.4998,
"longitude": -122.0086,
"timezone": "America/Los_Angeles",
"utc_offset": "-0800",
"country_calling_code": "+1",
"currency": "USD",
"currency_name": "Dollar",
"languages": "en-US,es-US,haw,fr",
"country_area": 9629091.0,
"country_population": 327167434,
"asn": "AS7922",
"org": "COMCAST-7922"
}
```
## Supported jurisdictions
k-ID tracks regulations across 200+ markets. The following jurisdictions are tracked in the k-ID Regulatory Hub and are accepted as valid ISO Country or Country and Region codes. Using a valid Region code not mentioned below along with a country on this list is still valid as a parameter to APIs that require a jurisdiction (for example, "US-MA"), but the behavior of the API won't differ from the country.
For a complete list of supported jurisdictions, see [Supported Jurisdictions](/concepts/supported-jurisdictions).
## Jurisdiction-specific requirements
Different jurisdictions have different requirements for:
- **Digital consent age**: The minimum age at which a player can provide digital consent for data processing
- **Civil age**: The civil/contract age at which a player is considered a legal adult in the jurisdiction
- **Minimum age**: The minimum age required to access the platform/game (typically 0 unless restricted)
- **Age collection methods**: Which methods are approved for collecting age signals (date of birth, age slider, platform account)
- **Age assurance requirements**: Whether age verification is required for players in this jurisdiction
These requirements are automatically determined by k-ID based on the jurisdiction you provide. You don't need to implement jurisdiction-specific logic yourself - k-ID handles this automatically.
---
// File: concepts/age-assurance
# Age assurance
Age assurance is the overall process that proves the age of a user for various purposes, including access to a game, app, or content within an app. Depending on the jurisdiction and how risky the feature is that you want to verify age for, different methods can apply.
## What's age assurance?
Age assurance is a legal and regulatory concept that refers to the process of verifying or estimating a user's age to ensure compliance with age-related regulations. Different jurisdictions and regulations require different levels of age assurance depending on:
- The type of content or service being accessed
- The age thresholds being enforced
- The risk level of the activity
## Age verification compared to age estimation
Age assurance can be achieved through different methods:
- **Age verification**: Verifying age through documentation or trusted methods (ID documents, credit cards)
- **Age estimation**: Estimating age through technology (facial age estimation)
The appropriate method depends on the jurisdiction and the level of assurance required. Some jurisdictions require strict verification, while others allow estimation.
## When's age assurance required?
Age assurance is required when:
- A player enters an age that would be considered an adult or teen in jurisdictions that require age assurance
- Accessing age-restricted content or features
- Meeting regulatory requirements for parental consent
- Complying with platform policies
The `ageAssuranceRequired` field returned from the `/age-gate/get-requirements` API indicates whether age assurance is required for players in the current jurisdiction.
## Age assurance methods
k-ID provides multiple methods for age assurance:
- **Facial Age Estimation**: Privacy-preserving age estimation using a device's camera
- **ID Document Verification**: Government-issued ID verification
- **AgeKey**: A reusable and anonymous age-proof generated after an initial verification process
- **Credit Card Verification**: Age verification through credit card validation (for trusted adult verification)
- **Social Security Number Verification**: Age verification using SSN (United States only)
- **ConnectID**: Australia's digital identity exchange
The specific methods available depend on the jurisdiction and your product configuration in the [Compliance Studio](/compliance-studio/creating-product). For more information, see [Verification Methods](/concepts/verification-methods).
## Age assurance workflow
The age assurance workflow typically involves:
1. **Age collection**: Collecting an age signal from the user (date of birth, age estimation)
2. **Requirement check**: Determining if age assurance is required based on jurisdiction and age
3. **Verification**: If required, presenting verification methods to the user
4. **Result processing**: Processing the verification result and updating permissions accordingly
For more information on implementing age assurance, see [Age Assurance](/concepts/age-assurance) in the API documentation.
---
// File: concepts/multi-product-approval
# Multi-product approval
Multi-product approval allows you to present multiple products to parents for approval in a single consent flow. This reduces friction in the trusted adult consent process, making it easier for families to access your games and services while maintaining compliance with privacy regulations.
## Benefits
- **Reduced Trusted Adult Friction**: Trusted Adults can approve multiple products at once instead of going through separate consent flows for each product
- **Improved Child Experience**: Children gain access to multiple products simultaneously, reducing wait times and abandoned consent flows
- **Flexible Configuration**: Choose which products to bundle together based on your platform architecture and user needs
- **Maintain Compliance**: All products in a bundle maintain their individual permission settings and regulatory requirements

## Use cases
Multi-product approval is ideal for:
- **Platform + Games**: You have an account system or platform that multiple games depend on
- **Game Collections**: You offer multiple related games that families typically want to access together
- **Tiered Access**: You have core services and optional add-ons that can be approved simultaneously
## Configuration options
There are three ways to implement multi-product approval:
### 1. Product bundling (static configuration)
Configure which products automatically appear together in consent flows through the [Compliance Studio](/compliance-studio/product-api-configuration#multi-product-approval).
**How to Configure**:
1. In the [Compliance Studio](/compliance-studio/creating-product), select your product
2. Go to **Configuration**, and click **Edit** within the **Multi-Product Approval** section.
3. Click the toggle next to **Enable Multi-Product Approval**
4. Select which products to bundle with the current product
5. Click **Push to Test**

**Trusted Adult Experience** - When parents receive a consent request for the primary product, they'll automatically see the bundled products included. Trusted Adults can remove bundled products if they don't want to approve them.
**Best for** products that are commonly used together but don't have hard dependencies
### 2. Essential products (required dependencies)
Mark another product as required for your product to function. This is typically used when your product depends on a platform or account system.
**How to Configure**:
1. In the [Compliance Studio](/compliance-studio/creating-product), select your product
2. Go to **Configuration**, and click **Edit** within the **Multi-Product Approval** section.
3. Click the toggle next to **Enable Multi-Product Approval**
4. Under **Essential Product**, select the product that must be approved alongside this product
5. Click **Push to Test**
**Trusted Adult Experience** - The essential product can't be removed from the consent flow. Trusted Adults must approve both products together, or reject the consent request entirely.
**Important Restrictions**:
- A product can only have one essential product
- Essential products can't themselves require another essential product (no chaining)
- Use this only for true dependencies, not just convenience
**Best for** products that can't function without access to another product (for example, a game that requires a platform account)
### 3. Dynamic bundling (API-based)
Programmatically select which products to bundle at the time of creating a consent challenge with the API.
**How to Use**:
Call the [`/challenge/create-bulk`](/api/endpoints/create-bulk-challenges) API endpoint with the specific product IDs you want to include:
```json
{
"jurisdiction": "US-CA",
"requestedProductIds": [123, 456, 789],
"kuid": "12b9fa0e-6d6d-4903-a1fc-f2233027b71d"
}
```
**Trusted Adult Experience** - Trusted Adults see the specific bundle of products you've defined for this consent request.
**Best for**:
- Contextual bundling based on user behavior
- A/B testing different product combinations
- Complex logic that determines which products to offer together
## Best practices
### Choosing the right approach
- **Use Bundling** when products are often used together but don't have technical dependencies
- **Use Essential Products** only when one product truly can't function without another
- **Use Dynamic Bundling** when you need runtime flexibility or contextual product selection
### Configuration tips
1. **Start Simple**: Begin with essential products only, then add bundling as needed
2. **Consider the Trusted Adult Journey**: Don't overwhelm parents with too many products in one bundle
3. **Test Your Flows**: Use Test Mode to verify the parent experience before going live
4. **Document Dependencies**: Keep clear records of which products depend on others for your development team
### Permission considerations
When multiple products are bundled:
- Trusted Adults see the **union** of all permissions from all products
- Each product maintains its own permission configuration
- Trusted Adults can allow or disallow permissions for each product individually
- All data disclosures from bundled products are presented together
## Common patterns
### Pattern 1: Account and multiple games
**Configuration**:
- Account System Product (no essential product)
- Game A: Account System as essential product
- Game B: Account System as essential product
- Game C: Account System as essential product
**Result** - Any game approval automatically requires account system approval, but parents can approve multiple games at once.
**Account System Integration** - When integrating with an account system that spans multiple games, it's typical to create one k-ID Product for each game, and then a separate k-ID Product for the Account System itself. This is useful to represent any permissions and disclosures that are common or global for all games, or the account itself.
:::tip Account System Product
If your central account or platform product is enabled as an [Account System Product](/concepts/account-system-product), it can call k-ID APIs (such as `/age-gate/check`) **on behalf of** each game by using a single API key and the `Kid-Target-Product-Id` header. Parents see only the target game's configuration, and you can still set the Account System Product as an essential product so both are approved in one flow. See the [Account System Product](/concepts/account-system-product) guide for setup and supported endpoints.
:::
When a Product is mapped to the account system, this means that for each game, there are two `Session` objects retrieved: one for the k-ID Product mapped to the game, and one for the k-ID Product mapped to the account system. The k-ID API is scoped to a Product by the API key. The correct API key must be used depending on whether you are trying to access the k-ID Product mapped to the account system or the game.
**Product context for VPC** - If the games all require an account to play, then VPC can be triggered from the account creation process. However, the request for consent must be specific to a game so that a parent knows what they're consenting to. The k-ID API determines what Product should be presented to the parent during the consent process based on what API key is used when triggering VPC.
In practice, the integration of k-ID into an account system must be able to determine at the time of account setup what game triggered the account creation process, and map that to a k-ID API key.
### Pattern 2: Game suite with optional add-ons
**Configuration**:
- Main Game: No essential product
- Expansion Pack A: Main Game as essential product, bundled with Main Game
- Expansion Pack B: Main Game as essential product, bundled with Main Game
**Result** - Trusted Adults can approve the main game with optional expansions in one flow.
### Pattern 3: Platform approach
**Configuration**:
- Platform Product: No essential product
- Social Features: Platform as essential, bundled with Platform
- Premium Content: Platform as essential, bundled with Platform
**Result** - Trusted Adults approve platform access, with easy add-ons for additional features.
## Technical details
### Webhook events
For detailed information about the webhook event structure, see [`Challenge.StateChange`](/events/webhooks/event-types/challenge-statechange).
When parents approve a multi-product bundle, you'll receive separate [`Challenge.StateChange`](/events/webhooks/event-types/challenge-statechange) webhook events for each approved product. All sessions share the same `kuid` (k-ID user ID), allowing you to associate them with the same child.
### Session management
Each approved product in a bundle creates its own session with its own `sessionId`. You should:
- Cache all sessions associated with a player's `kuid`
- Use the appropriate session when checking permissions for each product
- Query sessions using [`/session/get`](/api/endpoints/get-session) with the relevant `sessionId` or `kuid` and product API key
#### Using `kuid` for cross-product session access
k-ID exposes a global identifier called `kuid` (k-ID User ID) for all players who have had trusted adult consent. The `kuid` is returned as a property of the `Session` object. When integrating with an account system across multiple games, the `kuid` should be associated with the player's identity in the account system when present in a `Session`. This allows retrieval of a k-ID Session, if it exists, for any k-ID Product by calling [`/session/get`](/api/endpoints/get-session) providing only the `kuid` as a parameter and using the appropriate API key for the correct Product.
When a trusted adult gives consent for a child to play one or more Products, the registered webhook endpoint for each approved product is invoked with `eventType` set to [`Challenge.StateChange`](/events/webhooks/event-types/challenge-statechange). Each of the `Session` objects have the same `kuid` for the player. For more information about webhooks, see [Webhooks](/webhooks).
#### Caching sessions for multiple products
When integrating with an account system, sessions from multiple Products are cached. `Session` objects can all be cached as a map in the account system by using the k-ID Product ID as a key. When a player attempts to play a new game, the map containing `Session` objects can be queried by Product ID, and if a `Session` is already present, the player can then be allowed to continue without further trusted adult consent.
### API endpoints
- **Static Configuration**: [Compliance Studio](/compliance-studio/product-api-configuration#multi-product-approval) UI
- **Dynamic Bundling**: [`/challenge/create-bulk`](/api/endpoints/create-bulk-challenges)
- **Session Retrieval**: [`/session/get?kuid={kuid}`](/api/endpoints/get-session) (returns session for the product associated with your API key)
## Edge cases and considerations
### Age minimum requirements
:::warning Important
**The Rule** - A child must meet the age requirements for **all** products in a bundle.
If an essential product has a higher age minimum than the product that depends on it, children below that age are denied access to both products.
:::
**Example**:
- Game A: Minimum age 10, requires Account System as essential product
- Account System: Minimum age 13
**Result** - Children must be at least 13 to access Game A, even though Game A's configured minimum is 10.
**Best Practice** - Set the age minimum of dependent products to be equal to or higher than their essential product's age minimum.
### Permission handling
:::warning Important
**The Rule** - Trusted Adults must approve the **most restrictive** permission requirements across all bundled products.
:::
When products are bundled together, k-ID evaluates each permission across all products and applies the most restrictive setting:
- If a permission is **required** in any product, it becomes required for the entire bundle
- If a permission is **optional** in one product but required in another, it becomes required
- Trusted Adults must grant all required permissions to complete the consent flow
**Example**:
- Game A: "Voice Chat" is optional (parent can choose)
- Account System (essential to Game A): "Voice Chat" is required (parent must approve)
**Result** - Trusted Adults must approve "Voice Chat" to grant consent, even though Game A alone would have made it optional.
**Best Practice**:
- Align permission requirements between dependent products when possible
- Review the combined permission set that parents see
- Consider whether certain permissions should be required at the platform level or game level
---
// File: concepts/verification-methods
# Verification methods
Age verification is a critical component of digital compliance, ensuring users meet minimum age requirements for accessing certain content, features, or services. These verification methods are used throughout all the supported flows of the CDK (Compliance Development Kit). k-ID's age verification methods serve two primary purposes: verifying a user's age to determine access to age-appropriate features or content, and verifying that a user is a trusted adult when a digital minor is seeking trusted adult consent.
k-ID provides a comprehensive suite of age verification methods that balance security, user experience, and regulatory compliance. By leveraging multiple verification providers and technologies, k-ID automatically selects the most appropriate verification methods based on jurisdiction requirements and user circumstances.
## How k-ID verification works
k-ID's age verification system is flexible and intelligent, incorporating various verification techniques automatically selected based on jurisdiction requirements, user demographics, and assurance levels needed. You can configure which verification methods are enabled through the [Compliance Studio](/compliance-studio/creating-product).
:::info Fallback only
In **Compliance Studio**, under the **Assurance and Verification** tab, you can set any **verification method** to **Fallback only**. The same enabled, disabled, and **Fallback only** options apply to every verification method shown under that tab. In the **Fallback only** case, the method is offered only after the user has tried another method and that attempt **didn't yield an age signal** (no conclusive age determination). For UI context and related settings on this tab, see the [Adult verification](/compliance-studio/product-verification) guide.
:::
The system is privacy-focused. k-ID doesn't store private data or images from the verification process. It simply confirms whether users meet defined age thresholds (adult or digital youth) according to their jurisdiction's requirements, minimizing data collection while maintaining robust verification capabilities.
## Universal age verification methods
These verification methods are available across multiple jurisdictions and provide broad coverage for age verification requirements.
### 📄 ID scan verification
ID scan verification uses advanced document verification technology provided by Veratad to authenticate government-issued identification documents including passports, driver's licenses, and national ID cards.
Users simply photograph their ID document with their device's camera. The system analyzes the document to verify authenticity and extract age information. Enhanced security implementations might also require a face scan to confirm the user's face matches the photo in the ID document.
This method is highly effective because government-issued IDs undergo rigorous verification processes before being issued, making them reliable age sources. The technology detects security features and tampering signs, providing strong fraud protection.
### 📸 Facial age estimation scan
Facial age estimation provides a user-friendly verification method where users verify their age by scanning their face with their device's camera. This privacy-preserving technology uses AI to estimate age categories without storing biometric data.
The process is simple. Users look into their camera and the system provides an age estimate within seconds. The technology provides an estimate of a user's age, and it's recommended to use age categories (adult or digital youth) as the criteria, rather than a precise age.
### 🔑 AgeKey
AgeKey provides the lowest friction experience for repeat users. After successfully completing any other verification method, users can create an AgeKey that serves as a reusable age credential for future verifications. Users can also create an AgeKey directly at [AgeKey.org](https://agekey.org).
Once generated, this secure credential allows users to verify their age in subsequent interactions simply by sharing their AgeKey rather than repeating the full verification process. Your AgeKey is stored locally on your device and protected by your biometrics (Face ID, fingerprint, or device passcode). This significantly reduces friction while maintaining security and privacy for users who have already been verified once.
### 💳 Credit card verification
Credit card verification provides age verification through Stripe's secure payment processing, particularly in jurisdictions where card ownership is restricted to adults. Users provide credit card information, and a small charge might be made to validate their card is real. If charged, this amount is refunded within 14 business days.
The system uses Stripe's established payment infrastructure, ensuring financial data is handled with the highest security standards. This method only allows verification by using a credit card, as debit cards and pre-paid gift cards are available to younger users.
#### Testing credit card verification integration
While your product is in test mode, use the provided [Stripe testing card numbers](https://docs.stripe.com/testing#cards) to cover all testing scenarios.
### 📧 Email address age estimation
Email address age estimation uses only an email address to estimate a user's age. Users provide their email and complete a one-time password verification to confirm ownership. The system analyzes the digital footprint associated with the email address to estimate age categories.
This method works by examining the digital history linked to an email address, which can indicate age ranges. The process typically completes within seconds and includes fraud prevention measures. It requires no ID scans, selfies, or credit card information, making it a simple alternative for age verification.
## Region specific verification methods
Some age verification methods are tailored to specific jurisdictions, taking advantage of unique digital identity infrastructure and regulatory frameworks in those regions.
### 🇦🇺 ConnectID (Australia)
ConnectID is Australia's digital identity exchange operated by Australian Payments Plus. The service enables age verification by connecting users with identity providers they already have relationships with, such as their banks.
Users select their preferred identity provider (typically their bank) and authenticate using their existing banking credentials. They then consent to share specific information, such as confirmation that they're over 18, without revealing additional personal details.
ConnectID is accredited by the Australian Government as an identity exchange and operates within Australia's Trusted Digital Identity Framework. The service is supported by major Australian banks and functions as a data bridge without storing personal information.
#### Testing ConnectID Integration
When your k-ID product is in test mode, ConnectID presents fictional banks for selection during the verification process. To complete mock verifications, you can use any of these test account credentials:
| Bank | Username | Password |
|----------|----------|----------|
| Capital | `dev+user1@connectid.com.au` | `T3stUs3rs!` |
| ABank | `dev+user2@connectid.com.au` | `T3stUs3rs!` |
| National | `dev+user3@connectid.com.au` | `T3stUs3rs!` |
| WBank | `dev+user4@connectid.com.au` | `T3stUs3rs!` |
These test accounts allow you to simulate the complete ConnectID verification flow and validate your integration before moving to live mode.
For mobile app integrations, you can test the full redirect flow by using the ConnectID Mock Bank C test apps. These apps simulate the third-party app redirect behavior described in the [mobile app guide](/get-started/quickstart-guides/mobile-apps#third-party-app-verification-flows):
- **iOS**: [ConnectID Mock Bank C TestFlight invite](https://testflight.apple.com/join/1PLFTUpR)
- **Android**: [ConnectID Mock Bank C on Google Play](https://play.google.com/store/apps/details?id=au.com.connectid.idp.mock&hl=en&gl=US)
Install these test apps on your device to validate the complete redirect flow when testing ConnectID verification in mobile applications.
### 🇺🇸 Social Security number verification (United States)
For United States users, Social Security Number verification provides robust age verification by using government data systems. Users provide the last four digits of their Social Security Number along with their birth date, which is cross-referenced with authoritative government databases to confirm both identity and age.
This method is valuable for users who might not have driver's licenses or photo identification, providing an inclusive verification option that relies on widely held government-issued credentials.
#### Testing Social Security Number verification integration
When your k-ID product is in test mode, you can use the following test data to trigger a successful verification:
| First Name | Last Name | Birthday | SSN (Last 4) |
|------------|-----------|------------|--------------|
| Barbara | Miller | 08-21-1974 | 5698 |
This test data allows you to simulate the complete SSN verification flow and validate your integration before moving to live mode.
### 🇸🇬 Singpass (Singapore)
Singpass is Singapore's national digital identity system that enables users to authenticate and consent to share verified attributes with relying parties. For age verification, users authenticate with Singpass and consent to share their date of birth (DOB). k-ID uses the DOB to determine whether the user meets the configured age threshold, avoiding the need to store unnecessary personal data. This leverages the trusted government-backed ecosystem and strong security controls of Singpass.
### 🇰🇷 KISA identity verification (South Korea)
South Korea supports real-name and age verification through methods designated by the Korea Internet & Security Agency (KISA). Platforms can request user verification via one of several options: mobile phone (carrier) verification, credit card, or public certificate. For age checks, the provider returns the user's date of birth (DOB), which k-ID uses to determine whether the user meets the required age threshold. These methods are standardized under South Korea's real-name verification regime and are widely used across online services.
### 🇧🇷 CPF verification (Brazil)
CPF (Cadastro de Pessoas Físicas) is Brazil's national taxpayer registry, issued by the Federal Revenue Service (Receita Federal). Every Brazilian citizen and resident has a unique CPF number tied to their identity. For age verification, users provide their CPF number, which is validated against the government registry to confirm the individual's identity and age. The verified date of birth is then used to determine whether the user meets the configured age threshold. This leverages Brazil's widely adopted national identification system, providing a reliable and familiar verification method for Brazilian users.
#### Testing CPF verification integration
While using a test mode, you can use the following test data to trigger a successful or failed verification
- Enter the credentials and submit.
- CPF number: (see credential options below)
- At the end, you're presented with test data for the verification session.
| CPF Number | CPF Status | Verification Result |
|---|---|---|
| 40442820135 | Regular | ✅ Success |
| 07691852312 | Pending Regularization | ✅ Success |
| 12345678909 | Regular (Age 12) | ⚠️ Inconclusive (under-18 criteria) / ❌ Failure (18+ criteria) |
| 40532176871 | Suspended | ❌ Failure |
| 01648527949 | Canceled | ❌ Failure |
| 98302514705 | Nullified | ❌ Failure |
| 05137518743 | Deceased | ❌ Failure |
## Testing and development considerations
k-ID provides two modes for building and operating your integration:
- **Test mode**: Use provider test credentials (for example, test cards and test IDs) to simulate outcomes such as PASS, FAIL, and specific failure reasons. Because inputs and provider responses are simulated, test mode can't yield real, verified results for real users and must not be used with production traffic.
- **Live mode**: Verify real users against real providers and authoritative data sources. Users must supply real credentials, and results reflect the user's actual date of birth or age outcome from the provider.
Test mode mirrors the live user experience and response shapes (including webhooks and client events), so you can safely build, exercise error paths, and validate end-to-end handling. When you switch to live mode, simulation features are turned off and only genuine verification methods are available to end users. For broader testing strategy and how to exercise the API, see the [Testing guide](/concepts/testing).
### Mock and real providers
When your product is configured with a **Test Mode** API key, the Family Connect verification flow displays a **TEST MODE** toolbar at the top of the widget. The toolbar is never shown in Live Mode and isn't visible to real end users.
The toolbar includes a switch between two paths:
- **Mock Providers** (the default): the widget replaces each provider UI with a built-in simulator, letting you drive verification outcomes without contacting the real provider.
- **Real Providers**: each verification method opens its real sandbox integration (for example, Stripe for credit card, ConnectID Mock Bank, Veratad test records). Use this path to validate the end-to-end integration against a live provider sandbox.
The switch is locked once a verification method is in progress, so you can't swap mid-flow.
#### Using mock providers
Selecting a method while **Mock Providers** is active opens a simulator screen with three groups of controls.
**Context card.** Shows the relevant age thresholds from your product's jurisdiction build so you can see what a submission needs to clear:
- The target verification age.
- Jurisdiction (for example, `US-CA`).
- Civil age and digital consent age.
- For age estimation methods, the FAE pass/fail thresholds.
- A method-specific note (for example, "Credit card verification always proves adult status.").
**Simulate Attempt presets.** One-click personas that submit a ready-made result derived from the product's real jurisdiction thresholds:
- `Adult`, `Teen`, `Child` for methods that return an exact or estimated age.
- `Passes Age Check` or `Fails Age Check` for threshold-only methods such as ConnectID or AgeKey.
**Advanced panel.** An expandable section for custom input:
- **Custom Age Range** (estimation methods only): two numeric inputs (Low and High, 0–120). When you change Low and haven't manually edited High, High autofills to `Low + 4` to match the typical FAE confidence band.
- **Custom Date of Birth** (exact and threshold methods): an HTML date input. The pass/fail status is computed from the date at submission time using the same logic real providers use.
**Simulate Failed Attempt presets.** Explicit non-age failures:
- `Inconclusive` records an incomplete attempt with no age signal. It counts against the method's max attempts but leaves the method available.
- `Fraudulent` records an incomplete attempt and disables the method for this verification, forcing the user to choose another option.
After any submission, a result pill is displayed under the matching group (for example, `Teen → PASS`) so you can confirm what was recorded.
#### Methods that support mocking
Each active verification method exposes the controls that match its shape:
| Method | Age entry | Presets |
| --- | --- | --- |
| Age Estimation Scan | Custom age range | Adult, Teen, Child, Inconclusive, Fraudulent |
| ID Document | Exact age and date of birth | Adult, Teen, Child, Inconclusive, Fraudulent |
| Credit Card | Fixed (18+) | Adult, Inconclusive |
| Social Security Number | Exact age | Adult (18+), Inconclusive |
| Singpass | Exact age | Adult, Teen, Inconclusive |
| Korean Real Name | Exact age and date of birth | Adult, Teen, Inconclusive |
| ConnectID | Threshold and date of birth | Passes Age Check, Fails Age Check, Inconclusive |
| Email Estimation | Fixed (18+) | Adult, Inconclusive |
| Brazil CPF | Exact age and date of birth | Adult, Teen, Inconclusive |
| AgeKey | Threshold | Passes Age Check, Inconclusive |
| Age Attestation | Exact age and date of birth | Adult, Teen, Child, Inconclusive |
#### Using real providers
Switch the toolbar to **Real Providers** when you need to exercise the actual provider integration. Each method opens its vendor sandbox; see the per-provider test credentials earlier on this page, such as:
- [Stripe test card numbers](#testing-credit-card-verification-integration) for Credit Card.
- [ConnectID Mock Bank credentials and mobile test apps](#testing-connectid-integration) for ConnectID.
- Veratad test records for Social Security Number and ID Document.
#### How mock results are processed
A Mock Providers submission isn't a separate code path: the result is routed through the same service that records a real provider's result, so webhooks (for example, [`Verification.Result`](/events/webhooks/event-types/verification-result)), DOM events, and session updates fire identically to a production verification. Only the provider UI is swapped out.
For server-to-server test scenarios without the widget, the [Set age verification status](/api/endpoints/set-age-verification-status) endpoint offers a programmatic equivalent that's also available only in Test Mode.
## Choosing the right verification methods
k-ID's intelligent system automatically selects appropriate verification methods based on several factors including jurisdiction requirements, user demographics, and the level of assurance required. However, understanding the strengths of each method can help you optimize your age verification strategy.
For maximum coverage and user convenience, implementing multiple verification methods provides the best user experience. Users can choose the method that works best for their circumstances, whether they prefer the convenience of facial scanning, the security of document verification, or the simplicity of leveraging existing digital identity relationships.
The system's automatic selection ensures that users in different jurisdictions see methods that are both legally compliant and technically supported in their regions. For example, users in Australia might be offered ConnectID, and users in other regions might see universal methods such as ID scanning or facial age estimation.
## Privacy and security
Throughout all verification methods, k-ID maintains a strong commitment to user privacy and data security. The system is designed to collect only the minimum information necessary to meet age verification requirements. Personal data and images aren't stored, and verification results focus on age category determination rather than collecting detailed personal information.
This privacy-preserving approach aligns with global data protection regulations while providing businesses with the assurance they need to meet age verification requirements. By leveraging trusted third-party verification providers and established digital identity infrastructure, k-ID can provide robust verification capabilities without compromising user privacy.
The verification system also includes strong fraud prevention measures, using advanced technologies to detect manipulation attempts and ensure the integrity of the verification process. These security measures help protect both businesses and users from fraudulent activities while maintaining a smooth user experience.
## Integration and Implementation
k-ID's age verification methods integrate seamlessly into existing applications and workflows through standardized APIs. The system handles the complexity of managing multiple verification providers and methods, presenting a consistent interface to developers while leveraging the most appropriate verification techniques behind the scenes.
The verification process is designed to be embedded naturally into user journeys, whether as part of account creation, feature activation, or content access. Results are delivered through both webhooks and JavaScript events, allowing applications to respond appropriately to verification outcomes and provide immediate feedback to users.
By understanding these various age verification methods and their appropriate use cases, developers can create more inclusive, secure, and compliant applications that meet the diverse needs of users across different jurisdictions and circumstances.
---
// File: concepts/account-system-product
# Account System Product
This guide explains how to implement the **Account System Product** (ASP) capability in k-ID. It allows your organization's central account or platform product to create authentication challenges and sessions on behalf of other products in your organization, using your Account System Product's API key and an optional header, without managing separate API keys per product.
## Features
- **Single API key**: Your Account System Product uses its own API key for all cross-product calls; no per-product key management.
- **Product-specific consent**: When you create a challenge for another product, parents see only that product's data notices, permissions, and branding.
- **Organization scoping**: You can only act on behalf of products in the same organization; cross-org use is rejected.
- **Full visibility**: Both your Account System Product and the target product receive webhooks, with fields indicating who initiated the flow and on whose behalf.
### Flow overview
```mermaid
sequenceDiagram
participant Account as Your Account System
participant Product as Your Product
participant API as k-ID Game-API
participant Parent as Family Portal
Account->>API: Request with Kid-Target-Product-Id: Product ID
API->>API: Validate same organization & access ✓
API->>Parent: Display product's configuration
Parent->>API: Parent completes flow
par Notifications
API->>Account: Webhook (with target info)
and
API->>Product: Webhook (with initiator info)
end
```
Your Account System Product sends a request to one of the supported endpoints ([`/age-gate/check`](/api/endpoints/check-age-gate), [`/challenge/send-email`](/api/endpoints/send-email), or [`/challenge/generate-otp`](/api/endpoints/generate-otp)) with the `Kid-Target-Product-Id` header set to the **target (non–Account System Product) product's** ID. The API validates that both products are in the same organization and that the caller is an Account System Product. The parent sees only the target product's configuration (notices, permissions, branding), not the Account System Product's. For each webhook event (for example, challenge state change), both the Account System Product and the target product receive the event: the Account System Product's payload includes `onBehalfOfProductId`, the target's includes `initiatedByProductId`.
## Prerequisites
1. **Account System Product flag**: Your authentication/account product must have the Account System Product toggle **enabled** in [Compliance Studio](/compliance-studio/creating-product) (Product Details → Information). Contact k-ID if the option isn't available for your organization.
2. **Target products**: The products you act on behalf of must be in the same organization and must have the Account System Product toggle **disabled**. You can only use the header for non–Account System Products.
3. **API key**: Use the API key of the **Account System Product** (the product with the toggle turned on). Don't use the target product's API key.
## Step 1: Enable Account System Product in Compliance Studio
Enable the Account System Product setting for your designated product (for example, your central account or auth product) in [Compliance Studio](/compliance-studio/creating-product). Target products you act on behalf of keep their existing configuration; you don't need to create new products or API keys for them.
### How to turn on Account System Product
1. In [Compliance Studio](/compliance-studio/creating-product), open **Products** in the left sidebar and select the product that acts as your Account System Product (the one that creates challenges and sessions on behalf of others).
2. Open the **Information** tab for that product.
3. In the **Product Details** section you'll see **Account System Product** (initially disabled). Click **Edit** (top right of the page).
4. Scroll to the **Account System Product** section at the bottom of the Product Details form. The description reads: *"Enable this if this product can create challenges and sessions on behalf of other products."*
5. Turn the **Account System Product** toggle **on**.
6. Click **Push to Test** to save and apply the configuration.
After this, that product's API key can be used with the `Kid-Target-Product-Id` header to perform actions on behalf of other products in the same organization.
## Step 2: Call the API with the target product header
When your Account System Product needs to perform an action (such as an age gate check or creating a challenge) **on behalf of** another product, send the same request you would for your own product, and add the header:
| Header | Value | Required |
| --- | --- | --- |
| `Kid-Target-Product-Id` | The **non–Account System Product** ID (target product's k-ID product ID, positive integer) | Yes, when acting on behalf of another product |
Use the **Account System Product** API key in the `Authorization` header. The request runs in the context of the target product, but authentication is with the Account System Product's key.
### Example: Check age gate on behalf of a game
Your Account System Product checks whether the user can access a specific game (product ID `12345`) without managing that game's API key:
```http
POST /api/v1/age-gate/check
Content-Type: application/json
Authorization: Bearer ACCOUNT_SYSTEM_PRODUCT_API_KEY
Kid-Target-Product-Id: 12345
{
"jurisdiction": "US-CA",
"dateOfBirth": "2015-03-20"
}
```
As in the standard flow, the API returns a response that can include a URL when a challenge is required. That URL initiates the challenge for the **target** product (here `12345`), not for the Account System Product. When the parent opens the URL, they see the target product's configuration (notices, permissions, branding), not the Account System Product's.
### Example: Send challenge email on behalf of a product
Send a parental consent email for a challenge belonging to another product by including the header:
```http
POST /api/v1/challenge/send-email
Content-Type: application/json
Authorization: Bearer YOUR_ACCOUNT_SYSTEM_API_KEY
Kid-Target-Product-Id: 12345
{
"challengeId": "challenge-uuid",
"email": "parent@example.com"
}
```
The email is sent in the context of product `12345`; the parent sees that product's configuration when they complete the flow.
### Endpoints that support `Kid-Target-Product-Id`
Only these endpoints accept the optional `Kid-Target-Product-Id` header when the caller is an Account System Product:
- [`/age-gate/check`](/api/endpoints/check-age-gate)
- [`/challenge/send-email`](/api/endpoints/send-email)
- [`/challenge/generate-otp`](/api/endpoints/generate-otp)
If the header is omitted, the request applies to your own product (the Account System Product), as with a normal integration.
### Multi-product approval
Account System Product works with [multi-product approval](/concepts/multi-product-approval). A common setup is to mark the Account System Product as an **essential product** of the target product (for example, a game). Then when you call `/age-gate/check` with your Account System Product API key and `Kid-Target-Product-Id` set to that game, the consent flow the parent sees can include both the game and the Account System Product in a single approval, so the parent approves the game and the platform together. Configuration is done in Compliance Studio on the target product: set the Account System Product as that product's essential product in the multi-product approval settings. For full options (essential products, bundling, dynamic bundling), see the [Multi-product approval](/concepts/multi-product-approval) guide.
## Step 3: Handle webhooks for cross-product flows
When your Account System Product initiates a flow on behalf of another product, **each** webhook event is sent to **both** endpoints in a pair: one to the Account System Product's webhook URL and one to the target (non–Account System Product) product's webhook URL. This happens for every event throughout the challenge lifecycle (for example, state changes), not only when the flow completes.
- **Account System Product's webhook**: includes `onBehalfOfProductId` (the target product ID you acted on behalf of).
- **Target product's webhook**: includes `initiatedByProductId` (the Account System Product ID that initiated the flow).
Event payloads use these fields so you can tell cross-product flows apart:
| Field | Present on | Description |
| --- | --- | --- |
| `onBehalfOfProductId` | Account System Product's webhook | The product ID you acted on behalf of (the target product). |
| `initiatedByProductId` | Target product's webhook | The product ID that initiated the flow (your Account System Product). |
### Example: `Challenge.StateChange` (account system side)
When the Account System Product's webhook receives a challenge state change for a flow it initiated on behalf of product `12345`:
```json
{
"eventType": "Challenge.StateChange",
"data": {
"id": "9d6b056e-7d62-4a9e-907a-3d0f6f1d1b9a",
"productId": 12345,
"status": "PASS",
"onBehalfOfProductId": 12345,
"sessionId": "b6d1a7c2-8f34-4c83-bf0b-3a6d4a2f9d31",
"approverEmail": "parent@example.com",
"kuid": "7a1f2c3d-4e5f-6789-abcd-ef0123456789"
}
}
```
Here `productId` and `onBehalfOfProductId` both refer to the target product. Your handler can use `onBehalfOfProductId` to know which product this consent was for.
### Example: `Challenge.StateChange` (target product side)
The target product's webhook receives the same event with `initiatedByProductId` set to your Account System Product's product ID:
```json
{
"eventType": "Challenge.StateChange",
"data": {
"id": "9d6b056e-7d62-4a9e-907a-3d0f6f1d1b9a",
"productId": 12345,
"status": "PASS",
"initiatedByProductId": 99999,
"sessionId": "b6d1a7c2-8f34-4c83-bf0b-3a6d4a2f9d31",
"approverEmail": "parent@example.com",
"kuid": "7a1f2c3d-4e5f-6789-abcd-ef0123456789"
}
}
```
`99999` is the Account System Product's product ID. In the target product's payload, `productId` is the target product's own ID; `initiatedByProductId` identifies which Account System Product started the flow. The same dual delivery and field pattern applies to other webhook event types for that challenge (for example, session-related events). See [Webhooks](/webhooks) and [`Challenge.StateChange`](/events/webhooks/event-types/challenge-statechange) for full payload details.
## Security and validation
- **Organization check**: The target product must belong to the same organization as your Account System Product. Requests for products in other organizations are rejected with `400 Bad Request`.
- **No ASP-to-ASP**: The target product can't be an Account System Product. Such requests are rejected.
- **API key**: Only the Account System Product's API key can be used with `Kid-Target-Product-Id`. Using another product's key with this header returns `403 Forbidden`.
- **Header format**: `Kid-Target-Product-Id` must be a positive integer. Invalid values return `400 Bad Request`.
## Typical flows
1. **Unified account, multiple games**
Your platform is the Account System Product. When a user launches a game, your server calls the k-ID API with `Kid-Target-Product-Id` set to that game's product ID. Parents see only that game's consent; your Account System Product and the game both get webhooks.
2. **Central auth service**
Your auth service is the Account System Product. It creates challenges and checks age gates for several client products (for example, games, apps) in the same organization. Each client product keeps its own Compliance Studio configuration and webhook URL.
3. **Session and challenge consistency**
You can create challenges and retrieve sessions for the same target product using the same header on the relevant endpoints, so your Account System Product remains the single integration point while each product's compliance and UX stay independent.
## Next steps
- Configure your Account System Product and target products in [Compliance Studio](/compliance-studio/creating-product).
- Use the [API reference](/api/overview) to confirm request/response shapes for the endpoints you use.
- Set up [webhooks](/webhooks) for both your Account System Product and target products, and handle `onBehalfOfProductId` / `initiatedByProductId` where needed.
- For multi-product consent flows (for example, essential products, bundling), see [Multi-product approval](/concepts/multi-product-approval).
---
// File: concepts/browser-device-compatibility
# Browser and device compatibility
k-ID's browser-based verification flow runs in the user's browser and offers multiple verification methods behind the scenes. One of those methods is [AgeKey](/agekit-plus/overview), a reusable low-friction age signal built on WebAuthn passkeys. AgeKey has a higher browser bar than the rest of the flow because it needs [Related Origin Requests for passkeys](https://developer.chrome.com/blog/passkeys-updates-chrome-129#related-origin-requests), a Chromium feature that landed in Chrome 129. The widget checks the browser at runtime and only offers AgeKey when the browser supports it; on older browsers the widget still works, but AgeKey is hidden and users complete verification through another method.
> **How to read this document:** Each table shows two minimums per row: the "widget" minimum is where the compliance flow runs at all, and the "AgeKey" minimum is where AgeKey is offered as a verification method. End-user device support is determined by whether the device runs a supported OS version and browser. No specific hardware model is required beyond what's needed to run that OS.
---
## Desktop browsers
| Browser | Widget minimum | AgeKey minimum | Notes |
| --- | --- | --- | --- |
| Chrome | 108 | 129 | AgeKey hidden below Chrome 129. |
| Edge | 108 | 129 | Chromium-based; AgeKey support matches Chrome. |
| Safari | 16 | 16 | Requires macOS 12.4 (Monterey) or later. |
| Firefox | 122 | 122 | AgeKey on macOS also requires macOS 13 (Ventura) or later. |
| Opera | 97 | 115 | Chromium-based; AgeKey hidden below Opera 115. |
---
## Mobile browsers
| Browser | Widget minimum | AgeKey minimum | Notes |
| --- | --- | --- | --- |
| Chrome (Android) | 108 | 129 | Android 9+ required. AgeKey hidden below Chrome 129. |
| Edge (Android) | 108 | 129 | Android 9+ required. AgeKey hidden below Edge 129. |
| Firefox (Android) | 128 | 128 | Android 9+ required. AgeKey via Android Credential Manager. |
| Opera Mobile (Android) | 80 | 89 | Android 9+ required. Opera Mini isn't supported. |
| Safari (iOS) | 15.2 | 16 | AgeKey requires iOS 16+. |
Apple requires all iOS browsers to use WebKit, so **Chrome, Firefox, Edge, and other browsers on iOS follow the Safari (iOS) row** rather than their Android or desktop version requirements.
---
## Mobile embed
If your integration opens the verification flow inside an embedded browser (rather than launching the device's default browser), the following minimums apply. Both iOS [ASWebAuthenticationSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) and [SFSafariViewController](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller) support AgeKey; on Android, [Android Custom Tabs](https://developer.chrome.com/docs/android/custom-tabs) is the supported surface.
| Surface | Widget minimum | AgeKey minimum | Notes |
| --- | --- | --- | --- |
| iOS [ASWebAuthenticationSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) | iOS 15.2 | iOS 16+ | Recommended. OS-managed browser view; carries passkey entitlements. |
| iOS [SFSafariViewController](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller) | iOS 15.2 | iOS 16+ | Also supported. Same passkey entitlements as ASWebAuthenticationSession. |
| iOS [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview) (native app embed) | iOS 15.2 | ❌ (widget hides AgeKey) | Generic WKWebView instances lack passkey entitlements. The widget detects `window.webkit.messageHandlers` and hides AgeKey; users complete verification through another method. WKWebView-based browsers with entitlements (Chrome for iOS, Firefox for iOS, Edge for iOS) are allowlisted. |
| [Android Custom Tabs](https://developer.chrome.com/docs/android/custom-tabs) | Chrome 108 / Android 9+ | Chrome 129 / Android 9+ | Recommended. AgeKey via Custom Tabs + Credential Manager from Chrome 129. |
| Android [WebView](https://developer.android.com/reference/android/webkit/WebView) | Android 9+ | ❌ (widget hides AgeKey) | Android WebView doesn't support WebAuthn. The widget checks the `publickey-credentials-create` feature policy and hides AgeKey; users complete verification through another method. |
| Android [Trusted Web Activity](https://developer.android.com/develop/ui/views/layout/webapps/trusted-web-activities) | ❌ | ❌ | Requires digital-asset-link setup on the k-ID domain, which isn't available. TWA falls back to Custom Tabs; use Custom Tabs directly. |
---
## Desktop embed and game engines
Desktop apps, launchers, and game engines often host web content through an embedded [Chromium Embedded Framework (CEF)](https://bitbucket.org/chromiumembedded/cef/src/master/) surface, such as [CefSharp](https://cefsharp.github.io/) or the CEF-based browser bundled with a game engine.
The AgeKey requirement is the same as for desktop Chrome: the underlying Chromium branch must include [Related Origin Requests](https://developer.chrome.com/blog/passkeys-updates-chrome-129#related-origin-requests), which landed in Chromium 129. Below CEF 129, use ceremonies silently fail, so the widget hides AgeKey and offers other verification methods instead.
| CEF branch | AgeKey status |
| --- | --- |
| CEF below 129 | Not usable. Widget hides AgeKey and offers other verification methods. |
| CEF 129+ | Fully supported. Both AgeKey creation and re-use work through the OS platform authenticator (Windows Hello, Touch ID). |
For questions about a specific device or browser configuration, reach out to your k-ID account representative or [k-ID](https://k-id.com).
---
// File: concepts/data-notices
# Data notices
Data notices are disclosures that inform users about how their data is collected, used, and shared. In many jurisdictions, data notices are required by law, especially for children and teens.
## What are data notices?
Data notices typically include information about:
- What data is collected
- How data is used
- Who data is shared with
- User rights regarding their data
- How to contact the company about data practices
## When are data notices required?
Data notices are required when:
- Collecting personal information from users
- Processing data for children or teens
- Required by regulations in the user's jurisdiction (such as GDPR, COPPA)
## Data notices in k-ID
k-ID handles data notices as part of the compliance flow. When using the CDK widgets (End-to-End widget), data notices are automatically displayed and consent is collected as part of the flow.
When building custom flows with the API, you might need to:
1. Display data notices to users
2. Collect consent for data processing
3. Store consent records for compliance
## Configuring data notices
Data notices are configured in the [Compliance Studio](/compliance-studio/product-notices) for your product. You can:
- Create custom data notices
- Configure which notices are shown based on jurisdiction
- Set up consent requirements for different data processing activities
For more information on configuring data notices, see [Data notices configuration](/compliance-studio/product-notices).
## Data notices and sessions
When a session is created, data notice consents are included in the session. The session tracks which data notices have been accepted and when they were accepted.
For information about what data is stored for kids and trusted adults, see [Access, features, and consent](/concepts/access-features-consent/overview#what-data-is-stored) in the Core concepts section.
## Privacy and compliance
Data notices are a critical component of privacy compliance. Ensure that:
- Data notices are clear and understandable
- Users can easily access data notices
- Consent is properly recorded and stored
- Data notices are updated when data practices change
---
// File: concepts/testing
# Testing
Testing your k-ID integration is essential to ensure it works correctly before going live. This guide covers testing strategies and tools available in the k-ID platform.
## Test mode and live mode
k-ID provides two environments for testing and production:
### Test mode
Test mode  allows developers to test integrations without affecting real data or production systems. When using a **Test Mode** API key, requests are sent to the test environment at [https://game-api.test.k-id.com](https://game-api.test.k-id.com). This environment is designed to simulate the live system but operates with mock data.
Test-mode **challenge URLs** (such as the URLs returned by [`/age-gate/check`](/api/endpoints/check-age-gate), [`/session/upgrade`](/api/endpoints/upgrade-session), and [`/challenge/send-email`](/api/endpoints/send-email)) intentionally expire after **7 minutes** instead of the live-mode **2 weeks**. The short lifespan lets you exercise expired-URL handling without waiting. For details, see [Challenge URL and email link expiration](/concepts/access-features-consent/challenges#challenge-url-and-email-link-expiration). AgeKit+ age verification URLs are always valid for **2 weeks** regardless of mode (see [Verification URL validity](/agekit-plus/waterfall-flow#verification-url-validity)).
### Live mode
Live mode  is used for production. When using a **Live Mode** API key, requests are sent to the production environment at [https://game-api.k-id.com](https://game-api.k-id.com). This environment processes real data and interacts with live systems.
> **Note**: Always verify that you are using the correct API key and endpoint for the intended mode to avoid unintended impacts on production data.
## Mock and real providers
When a product is running in Test Mode, the Family Connect widget shows a **TEST MODE** toolbar with a switch between a built-in simulator (**Mock Providers**) and each method's real sandbox (**Real Providers**). For the full behavior, including toolbar controls, simulator screens, supported presets per method, and how mock submissions are processed, see [Verification methods: mock and real providers](/concepts/verification-methods#mock-and-real-providers).
## Testing with the interactive API reference
Before writing code to call the k-ID API, you can test endpoints directly using the [interactive API reference](/api/interactive-reference) built into the documentation. Get your test API key from the [Compliance Studio](/compliance-studio/creating-product) by going to the **Developer Settings** page of your product.
### Set up API testing
1. Navigate to any endpoint page in the [API Reference](/api/overview), for example the [`/age-gate/check`](/api/endpoints/check-age-gate) endpoint.
2. **Select the Base URL**: Hover over the Base URL field in the request interface to reveal an Edit button. Click the Edit button and select the **Test** environment (`https://game-api.test.k-id.com/api/v1/`).
3. **Authenticate your request**: Paste your test API key into the "Bearer Token" field.
4. **Configure the request body**: Modify the request body values as needed. For testing, use:
```json
{
"jurisdiction": "US-CA",
"dateOfBirth": "2005-04-15"
}
```
5. **Send the request**: Click "Send API Request" to complete the API call.
6. **View the response**: The response is displayed below the request interface, showing the API's response to your request.
You've now made a successful call to k-ID! For more detailed information about using the interactive reference, see the [Interactive reference](/api/interactive-reference) guide.
## Testing trusted adult consent
To see the parent experience in Family Connect, you need to create a Consent Challenge. To do that, make another call to [`/age-gate/check`](/api/endpoints/check-age-gate) using the interactive reference, this time with the age of a child below the age of digital consent in the US jurisdiction.
Use:
```json
{
"jurisdiction": "US-CA",
"dateOfBirth": "2015-04-15"
}
```
This results in a response that looks like this:
```json
{
"challenge": {
"challengeId": "",
"childLiteAccessEnabled": false,
"oneTimePassword": "",
"type": "CHALLENGE_PARENTAL_CONSENT",
"url": "https://family.k-id.com/authorize?otp="
},
"status": "CHALLENGE"
}
```
You can then navigate to the URL in the `url` field in a browser. You are asked for an email address, and then receive an email with a link to Family Connect where you can grant consent to play the game. Once you finish the consent flow in the browser, you can call the `challenge/get-status` API with the `challengeId` field previously provided. The response is:
```json
{
"approverEmail": "email@example.com",
"sessionId": "",
"status": "PASS"
}
```
You have now successfully created a k-ID `Session`! You can see it by supplying the `sessionId` field as a parameter to [`/session/get`](/api/endpoints/get-session).
```json
{
"ageStatus": "DIGITAL_MINOR",
"dateOfBirth": "2015-04-15",
"etag": "",
"jurisdiction": "US",
"permissions": [],
"sessionId": "",
"kuid": "",
"status": "ACTIVE"
}
```
---
// File: concepts/access-features-consent/overview
# Access, features, and consent
The concepts in this section are essential for implementing **Verifiable Parental Consent (VPC) flows**. These concepts help you understand how to collect age information, obtain parental consent, manage permissions, and track player access to features in your game or application.

:::important
These concepts apply specifically to **VPC flows** (consent flows), not to **age verification flows**. Age verification flows are simpler and don't involve sessions, permissions, consent challenges, or family configuration.
:::
## Why these concepts matter
VPC flows involve multiple interconnected concepts that work together to ensure compliance with parental consent regulations:
- **Age collection**: Determining a player's age and whether parental consent is required
- **Consent management**: Obtaining and tracking parental approval
- **Permission control**: Managing which features players can access based on their age and consent status
- **Session management**: Tracking player permissions and consent state over time
Understanding these concepts helps you build compliant applications that properly handle parental consent while providing appropriate user experiences for players of different ages.
## Core concepts
### Age collection and consent
- **[Age gate](/concepts/access-features-consent/age-gate)**: The mechanism for collecting and verifying user age before allowing access
- **[Verifiable Parental Consent (VPC)](/concepts/access-features-consent/vpc)**: The regulatory requirement and process for obtaining parental consent for minors
- **[Challenges](/concepts/access-features-consent/challenges)**: How consent challenges work when parental approval is required
### Session and permission management
- **[Sessions](/concepts/access-features-consent/sessions)**: Long-lived objects that track player permissions and age status
- **[Permissions](/concepts/access-features-consent/permissions)**: How to control access to game features based on age and consent
- **[Permissions upgrade](/concepts/access-features-consent/permissions#requesting-additional-permissions)**: Requesting additional permissions after initial consent
### Parent preferences and features
- **[Trusted adult preferences](/concepts/access-features-consent/trusted-adult-preferences)**: How parents configure consent and permission preferences
- **[Essential features](/concepts/access-features-consent/essential-features)**: Features required for a product to function, even if they require consent
- **[Data-lite mode](/concepts/access-features-consent/data-lite-mode)**: Providing limited access while waiting for parental consent
## Getting started with VPC flows
If you're new to VPC flows, start with these foundational concepts:
1. **[VPC](/concepts/access-features-consent/vpc)** - Understand the regulatory requirement and basic flow
2. **[Age gate](/concepts/access-features-consent/age-gate)** - Learn how to collect age information
3. **[Sessions](/concepts/access-features-consent/sessions)** - Understand how permissions are tracked
4. **[Challenges](/concepts/access-features-consent/challenges)** - Learn how consent challenges work
For implementation guidance, see the [Get started](/get-started/overview) section or the [VPC quick start guide](/get-started/quickstart-guides/vpc).
## What data is stored
The diagram below is a high-level view of what data is stored in k-ID for kids, trusted adults, and sessions.

Verified parent data and child data is maintained independently across all Products. Once a parent is verified, verification doesn't need to be done again for new games.
### Confirmed date of birth
If consent is required, a trusted adult is asked to confirm the player's date of birth which results in a change from what the player entered in the age gate or what was acquired from a Platform or existing account. The confirmed date of birth for the player is returned in the `dateOfBirth` field from the `/age-gate/check` and `/session/get` API.
### Email of the trusted adult
The email of the approving trusted adult for the current player is returned in the `approverEmail` field of the response from `/challenge/get-status`. This email address can be associated with the player account in the game for use in customer support cases.
---
// File: concepts/access-features-consent/sessions
# Sessions
The k-ID `Session` contains the collection of permissions and age status for the current player and location. Every player requires an active `Session`. The game should consult the active `Session` to determine whether features are allowed or disallowed in the game.
## What's a session?
A k-ID `Session` is a long-lived object that describes a player's permissions in a game for a given jurisdiction. Every player regardless of age gets a long-lived k-ID `Session` which is required to access the game. The session contains the permissions for the player in the jurisdiction in which they're playing the game.
For a kid or teen, the session can be modified by a trusted adult adding or removing permissions, or a change in age because of a birthday. Sessions are generally cached in local or cloud storage by the game. Sessions can also be associated with the identity of the player in the game if the game implements player accounts.
## Session lifecycle
A k-ID `Session` doesn't automatically expire. It's designed to be cached in local or cloud storage associated with the player. For kids and teens, the Session can change either if a parent adds or removes permissions by using Family Connect, or the player has a birthday and "ages up" to a new age category in the jurisdiction where the session was established.
Once a player has been granted access by their trusted adult, they have exactly one session per product. When permissions change (whether through parent modifications, age-up events, or permission upgrades), the same session ID is updated with the new permissions. A new session isn't created; the existing session reflects the current state of the player's access. However, if a session is revoked and the trusted adult goes through the consent flow again, a new session with a new session ID is created.
Since the `Session` is designed to be cached, it should be refreshed by using the [`/session/get`](/api/endpoints/get-session) API every time the game starts to get any changes that have been made.
## Session structure
A session contains:
- **`sessionId`**: A unique identifier for the session
- **`jurisdiction`**: The jurisdiction where the session was created
- **`dateOfBirth`**: The player's date of birth (if collected)
- **`ageStatus`**: The player's age status (`DIGITAL_MINOR`, `DIGITAL_YOUTH`, or `LEGAL_ADULT`)
- **`permissions`**: An array of permissions with their enabled/off status
- **`kuid`**: The k-ID user ID (if the player has been through VPC)
- **`status`**: The session status (always `ACTIVE`)
- **`etag`**: An entity tag for cache validation
Example session:
```json
{
"session": {
"ageStatus": "LEGAL_ADULT",
"dateOfBirth": "2005-04-15",
"etag": "6d9d24fccd428f845b355122799948dd0a52fc5d",
"jurisdiction": "US-CA",
"kuid": "123456",
"permissions": [
{
"enabled": true,
"managedBy": "PLAYER",
"name": "ai-generated-avatars"
},
{
"enabled": true,
"managedBy": "PLAYER",
"name": "text-chat-private"
}
],
"sessionId": "608616da-4fd2-4742-82bf-ec1d4ffd8187",
"status": "ACTIVE"
},
"status": "PASS"
}
```
## Getting a session
You can retrieve a session two ways:
- **[`/session/get`](/api/endpoints/get-session)**: Get a session by `sessionId` or `kuid`
- **[`/age-gate/check`](/api/endpoints/check-age-gate)**: Creates or updates a session as part of the age gate flow
The [`/session/get`](/api/endpoints/get-session) API supports conditional requests by using the `etag` parameter. If the session hasn't changed since the last request, the API returns HTTP 304 (Not Modified), allowing you to avoid unnecessary data transfer.
## Caching sessions
The `Session` should be cached in local or cloud storage, and can be associated with a player's account. k-ID Sessions only change when a parent updates a permission, or a kid or teen "ages up" to the next age category, or it's deleted by the parent or player.
While it's recommended that the game refresh the Session from the [`/session/get`](/api/endpoints/get-session) API every time the game restarts, this isn't explicitly required. Additionally, k-ID Webhooks can be used to receive `Session` updates instead of calling the [`/session/get`](/api/endpoints/get-session) API. If there's a problem with the k-ID APIs, the refresh can also be deferred until later. A cached Session can be used to manage permissions without connecting to the k-ID API while any problem is resolved.
## Sessions and player identity
The k-ID `Session` can be thought of as being strongly associated with a Player's ID. If the game uses an ID system, the `Session` itself can be fully stored as a JSON document with storage associated with the Player's account.
## Sessions across devices
When caching `Session` objects in local storage, different Sessions can exist in the local storage of multiple devices for the same player. If the player plays the game on a new device, the age gate is displayed to the player, and they must seek consent again if it was required before.
To avoid having to request consent more than once, the k-ID session can be stored in cloud storage associated with the player account and retrieved whenever the player logs in on any device. In this case, there is only one `Session` for the player even across devices.
## Session webhooks
In the [Compliance Studio](/compliance-studio/creating-product), you can register a Webhook to receive events from k-ID. This avoids the need to call `/session/get` to retrieve infrequent changes to k-ID Sessions except for when players _age up_.
The following webhook events are related to sessions:
- **[`Session.ChangePermissions`](/events/webhooks/event-types/session-changepermissions)**: Sent when permissions are modified by a parent
- **[`Session.Delete`](/events/webhooks/event-types/session-delete)**: Sent when a session is deleted
:::important Age-up changes don't trigger webhooks
When a player ages up and permissions change (for example, permissions becoming player-managed), k-ID doesn't send a webhook notification. You must use session comparison to detect these changes. See [Managing sessions and permissions](/get-started/quickstart-guides/managing-sessions-permissions) for implementation guidance.
:::
## Session deletion
When a parent revokes access to your product through Family Connect, the session is deleted. When querying a deleted session by using [`/session/get`](/api/endpoints/get-session), the API returns HTTP 400 with an error code of `NOT_FOUND`. Deleted sessions aren't returned by the API (they appear as if they never existed). This is intentional: once a session is deleted, it should be treated as if it no longer exists.
For more information, see [Webhooks](/webhooks).
---
// File: concepts/access-features-consent/permissions
# Permissions
Permissions in the k-ID Regulatory Hub represent classifications of game features that are addressed in regulations in one or more jurisdictions worldwide. Permissions are configured in the [Compliance Studio](/compliance-studio/product-api-configuration#permissions) for the game. Each k-ID Permission that matches a game feature should be enabled in the Compliance Studio.
:::tip Managing permissions across multiple products
If you manage multiple products with similar permission configurations, [Product Policies](/compliance-studio/product-policies) let you define shared compliance settings once and apply them across products. Each product inherits the policy's permissions as a baseline, and individual products can add additional standard permissions on top.
:::
## What are permissions?
Permissions represent features or capabilities in your game that might require parental consent or have age restrictions. Each permission can be enabled or disabled for a player based on:
- Their age and jurisdiction
- Parental consent (if required)
- The permission's configuration in Compliance Studio
## Permission structure
Each permission in a session has the following structure:
```json
{
"enabled": true,
"managedBy": "PLAYER",
"name": "text-chat-private"
}
```
Some permissions also carry a `verifiedAgeThreshold` field when the jurisdiction requires a verified age before the feature can be unlocked:
```json
{
"enabled": false,
"managedBy": "PLAYER",
"name": "loot-boxes-paid-gameplay-impacting",
"verifiedAgeThreshold": 18
}
```
### Permission fields {#permission-fields}
| Field | Type | Description |
| --- | --- | --- |
| `name` | string | The identifier of the permission (for example, `text-chat-private`, `loot-boxes-paid-gameplay-impacting`) |
| `enabled` | boolean | Whether the permission is currently enabled for the player |
| `managedBy` | string | Who controls this permission. One of `PLAYER`, `GUARDIAN`, or `PROHIBITED` (see below) |
| `verifiedAgeThreshold` | integer | Present only on high-risk permissions. The minimum verified age required to enable this permission. `enabled` stays `false` until the player's verified age meets or exceeds this threshold, regardless of consent. See [Age assurance for high-risk features](/cdk/age-assurance) |
**`managedBy` values:**
- **`PLAYER`**: The player can enable or disable this permission without parental consent. If `verifiedAgeThreshold` is present, the player must also pass age assurance before `enabled` becomes `true`.
- **`GUARDIAN`**: Only a trusted adult can enable or disable this permission.
- **`PROHIBITED`**: This permission isn't allowed for the current player, either because it's unavailable in their jurisdiction or because their age is below the `verifiedAgeThreshold` and can't be unlocked through any means at their current age.
:::info `managedBy` can change over time
The `managedBy` field isn't static. When a player ages up and no longer requires parental consent, permissions that were previously `managedBy: "GUARDIAN"` might change to `managedBy: "PLAYER"`. When a player requests to enable a `PLAYER`-managed permission via the [`/session/upgrade`](/api/endpoints/upgrade-session) API, it's automatically enabled without creating a challenge. Your application should handle these changes by comparing sessions over time.
:::
## Who can enable a permission?
The game code should use each k-ID Permission to control access to the corresponding features in the game. If the `enabled` field is `true` for a permission, this means that the feature can be enabled for the player in the game. If the `enabled` field is `false`, the feature must be turned off.
Some jurisdictions require that games turn off certain features by default if the player is a certain age even if it's acceptable for the player to access the feature (this is sometimes referred to as a "privacy by default" requirement). In this case `enabled` is `false` and the `managedBy` field contains `PLAYER`.
If a feature can only be turned on or off by a trusted adult, then the value of the `managedBy` field is `GUARDIAN`. If a feature isn't available for the current player, the `managedBy` field is `PROHIBITED`. This happens either because the feature is banned in the player's jurisdiction or because the player's age is below the `verifiedAgeThreshold` and can't be satisfied at their current age. When a permission is `PROHIBITED`, remove the feature entirely from the user experience rather than show it as disabled.
When a player ages up and no longer requires parental consent, permissions that were previously `managedBy: "GUARDIAN"` might change to `managedBy: "PLAYER"`, allowing the player to control them directly. When a player requests to enable a `PLAYER`-managed permission via the [`/session/upgrade`](/api/endpoints/upgrade-session) API, it's automatically enabled without creating a challenge. Your game should provide UI controls that allow players to manage these permissions themselves.
## High-risk permissions and age assurance
Some permissions require more than consent: they require a **verified age**. A verified age is one confirmed through a strong verification path (such as a government-ID-checked platform signal or an AgeKit+ challenge), not simply a self-reported date of birth.
These permissions carry a `verifiedAgeThreshold` integer field. Currently this applies in Brazil (`BR`):
| Permission | `verifiedAgeThreshold` |
| --- | --- |
| `profiling` | `18` |
| `targeted-ads` | `18` |
| `loot-boxes-paid-cosmetic-only` | `18` |
| `loot-boxes-paid-gameplay-impacting` | `18` |
| `direct-marketing` | `12` |
### How they differ from standard permissions
| Behaviour | Standard (`GUARDIAN`-managed) | High-risk (`verifiedAgeThreshold`) |
| --- | --- | --- |
| Guardian consent alone unlocks it | Yes | No |
| Verified platform signal at age gate can satisfy it | No | Yes |
| Requires age assurance challenge if not already satisfied | No | Yes |
| Shows as `PROHIBITED` when player is too young | Yes (jurisdiction ban) | Yes (age below threshold) |
### Satisfying a `verifiedAgeThreshold`
There are two ways for a permission's threshold to be satisfied:
1. **Verified platform signal at the age gate.** If a platform age signal is passed to `POST /age-gate/check` and the signal is considered verified (for example, Apple iOS `governmentIDChecked` or Google Play `VERIFIED`) with `ageLow >= verifiedAgeThreshold`, k-ID records an `ageVerification` on the session and the permission becomes `enabled: true` immediately.
2. **Age assurance via `/session/upgrade`.** If the threshold isn't already satisfied, calling `POST /session/upgrade` with the permission name triggers a `CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE` challenge rather than a standard parental-consent challenge. The player completes age verification through AgeKit+, and the permission is unlocked on success.
:::info Recovery flow
If a player initially provided an age below the threshold and later wants to appeal, see [Age assurance for high-risk features](/cdk/age-assurance) for the full recovery flow, including how to use a k-ID platform signal from a successful appeal to restart the age gate.
:::
## Matching permissions to game features
Permissions in the k-ID Regulatory Hub represent classifications of game features that are addressed in regulations in one or more jurisdictions worldwide. Permissions are configured in the [Compliance Studio](/compliance-studio/product-api-configuration#permissions) for the game. Each k-ID Permission that matches a game feature should be enabled in the Compliance Studio. The k-ID Permissions chosen are presented to parents when they give consent for a child to play a game.
:::info
When displaying features in the game that are mapped to k-ID Permissions, the `Session` should be checked to see whether the feature is enabled, and whether the player is allowed to turn it on.
:::
## Available permissions
The following permissions are available in the [Compliance Studio](https://portal.k-id.com):
### Social permissions
- **Online Multiplayer** (`multiplayer`)
- **Leaderboard and Rankings** (`leaderboard-and-rankings`)
- **Join Groups** (`join-groups`)
- **Public Profile** (`public-profile`)
- **Custom Avatar** (`custom-avatar`)
- **Custom Username** (`custom-username`)
- **Text Chat (Private)** (`text-chat-private`)
- **Text Chat (Public)** (`text-chat-public`)
- **Voice Chat** (`voice-chat`)
- **Video Chat** (`video-chat`)
- **Online Status** (`online-status`)
- **Public Friend List** (`public-friend-list`)
- **Send Accept Friend Requests** (`send-accept-friend-requests`)
- **Link to Third Party Chat** (`link-to-third-party-chat`)
- **Virtual Events** (`virtual-events`)
- **Share to Social Media** (`share-to-social-media`)
### Marketing permissions
- **Personalized Recommendations** (`personalized-recommendations`)
- **Targeted Ads** (`targeted-ads`): requires `verifiedAgeThreshold: 18` in Brazil
- **Profiling** (`profiling`): requires `verifiedAgeThreshold: 18` in Brazil
- **Push Notifications** (`push-notifications`)
- **Direct Marketing** (`direct-marketing`): requires `verifiedAgeThreshold: 12` in Brazil
- **Forums** (`forums`)
### Commerce permissions
- **In-Game Purchases** (`in-game-purchases`)
- **Loot Boxes Paid Cosmetic Only** (`loot-boxes-paid-cosmetic-only`): requires `verifiedAgeThreshold: 18` in Brazil
- **Loot Boxes Paid Gameplay Impacting** (`loot-boxes-paid-gameplay-impacting`): requires `verifiedAgeThreshold: 18` in Brazil
- **Loot Boxes Kompu Gacha** (`loot-boxes-kompu-gacha`)
- **Send Gifts** (`send-gifts`)
- **Simulated Gambling** (`simulated-gambling`)
- **Virtual Property Ownership** (`virtual-property-ownership`)
### Create content or share data permissions
- **Camera Access** (`camera-access`)
- **Share Game Clips Screenshots** (`share-game-clips-screenshots`)
- **Photo Video Sharing** (`photo-video-sharing`)
- **Precise Location Sharing** (`real-time-location-sharing`)
- **User-generated content** (`mods`)
- **Gameplay streaming** (`gameplay-streaming`)
- **Gameplay recording** (`gameplay-recording`)
- **Link to Third-Party Streaming App** (`link-to-third-party-streaming-app`)
### AI permissions
- **AI Chat** (`ai-chat`)
- **AI Media Generation** (`ai-media-generation`)
- **AI Voice Mode** (`ai-voice-mode`)
- **AI Memory** (`ai-memory`)
- **Companion Chatbots** (`ai-companion-chatbot`)
- **AI Media Upload** (`ai-media-upload`)
- **AI Model Training** (`ai-model-training`)
If you're building an AI-specific product, see the [AI products quick start](/get-started/quickstart-guides/ai-products) for end-to-end integration guidance.
### Advanced permissions
- **Augmented Reality** (`augmented-reality`)
- **Mature Language** (`mature-language`)
- **Motion Data** (`motion-data`)
## Requesting additional permissions
After a player has received a session with permissions, they might want to allow additional permissions. Permissions can be disallowed for reasons such as:
1. They're below the age threshold for the permission to be enabled by default.
2. They're below the absolute minimum required age for the permission.
3. The permission can only be enabled with a parent/guardian's consent, and it wasn't enabled during the consent process.
4. The permission has a `verifiedAgeThreshold` that hasn't been satisfied yet (no verified platform age signal on record for the session).
### Using the session upgrade API
The [`/session/upgrade`](/api/endpoints/upgrade-session) API can be used to enable additional permissions. The type of challenge created depends on the permission:
- **`PLAYER`-managed permissions** are enabled immediately with no challenge.
- **`GUARDIAN`-managed permissions** create a `CHALLENGE_PARENTAL_CONSENT` challenge for the trusted adult to complete.
- **Permissions with `verifiedAgeThreshold`** create a `CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE` challenge. The player completes age verification through AgeKit+ rather than parental consent. See [Age assurance for high-risk features](/cdk/age-assurance) for the full flow.
### Example request
```json
POST /api/v1/session/upgrade
Content-Type: application/json
Authorization: Bearer your-api-key
{
"sessionId": "608616da-4fd2-4742-82bf-ec1d4ffd8187",
"requestedPermissions": [
{
"name": "voice-chat"
}
]
}
```
### Example response with challenge
If a permission requires guardian consent, the response includes a challenge:
```json
{
"status": "CHALLENGE",
"challenge": {
"challengeId": "683409f1-2930-4132-89ad-827462eed9af",
"oneTimePassword": "ABC123",
"type": "CHALLENGE_PARENTAL_CONSENT",
"url": "https://family.k-id.com/authorize?otp=ABC123"
}
}
```
### Example response without challenge
If all requested permissions can be enabled by the player, the response includes the updated session:
```json
{
"status": "PASS",
"session": {
"sessionId": "608616da-4fd2-4742-82bf-ec1d4ffd8187",
"permissions": [
{
"enabled": true,
"managedBy": "PLAYER",
"name": "voice-chat"
}
]
}
}
```
### Asking for parental consent
When trusted adult consent is needed to enable a permission, a `challenge` is included in the response. This challenge can be shared similarly to the initial age gate process, by using a QR code, OTP, or email. For more information, see [Challenges](/concepts/access-features-consent/challenges).
:::tip Email notification for permission upgrades
One quality-of-life difference is that it's possible to use the [`/challenge/send-email`](/api/endpoints/send-email) API without specifying an email address, and the API sends an email to the trusted adult who most recently approved a permission for the player. This enables players to request permission changes without specifying an email address themselves. You can check the `hasApproverEmail` flag in the Session to determine if the session already has an associated email address before calling the API. If no associated email address is found, the [`/challenge/send-email`](/api/endpoints/send-email) API responds with an `INVALID_EMAIL` error code, and you must fall back to another method, such as providing a QR code, OTP, or asking the player to enter their trusted adult's email address.
:::
### Handling the upgrade flow
1. **Check permissions**: Before requesting an upgrade, check the current session to see which permissions are available and which require guardian consent
2. **Request upgrade**: Call [`/session/upgrade`](/api/endpoints/upgrade-session) with the requested permissions
3. **Handle challenge**: If a challenge is returned, follow the same workflow as the initial VPC flow
4. **Update session**: Once consent is granted, retrieve the updated session using [`/session/get`](/api/endpoints/get-session)
---
// File: concepts/access-features-consent/challenges
# Challenges
A `Consent Challenge` is created when a child seeks consent for access to a game or game feature. The challenge is displayed as a Request for Access to the trusted adult in k-ID Family Connect once a verified trusted adult is associated with the Consent Challenge.
## What's a challenge?
A consent challenge represents a player's request for parental consent. When a player's age requires Verifiable Parental Consent (VPC) in the current jurisdiction, k-ID creates a challenge that must be approved by a trusted adult before the player can access the game or feature.
## Challenge creation
Challenges are created automatically when:
- A player's age requires parental consent (determined by the [`/age-gate/check`](/api/endpoints/check-age-gate) API)
- A player requests access to a permission that requires guardian consent (via the [`/session/upgrade`](/api/endpoints/upgrade-session) API)
The challenge ID is returned in the API response and should be stored by the game in local storage or cloud storage associated with the player.
## Challenge information
When a challenge is created, the API returns:
- **`challengeId`**: A unique identifier for the challenge
- **`oneTimePassword`**: A password that can be entered by a parent to access the consent portal
- **`url`**: A URL that can be rendered as a QR code for easy mobile access
The consent challenge window should show the QR code, one time password, and an input field to allow the player to enter the email address of a parent or guardian who can grant consent.

## Challenge status
After showing the consent challenge window, the game should wait until the challenge has been completed successfully. It's strongly recommended to use Webhooks, listening for the [`Challenge.StateChange`](/events/webhooks/event-types/challenge-statechange) event. As a fallback option, polling is available by calling the [`/challenge/get-status`](/api/endpoints/get-challenge-status) API periodically with the challenge ID.
The challenge status can be:
- **`PASS`**: Consent has been granted by a trusted adult
- **`FAIL`**: Consent was denied by a trusted adult
- **`PENDING`**: The challenge is still waiting for a response
- **`POLL_TIMEOUT`**: The polling timeout has been reached (when using polling with timeout)
:::info Challenges don't expire
A challenge remains in `PENDING` status indefinitely until a trusted adult either approves or denies it. Only the access methods (OTP, QR code, email link) expire, not the challenge itself. For details on expiration times and how to refresh access methods, see [Challenge expiration and time-based authentication](#challenge-expiration-and-time-based-authentication).
:::
## Notifying trusted adults
There are several ways to notify trusted adults about a consent challenge:
### QR code and one-time password
The challenge response includes a URL that can be rendered as a QR code and a one-time password. Parents can use either method to access the consent portal directly.
### Email notification
If an email address for a parent or guardian is provided by the player, the game should pass this email address as a parameter to the [`/challenge/send-email`](/api/endpoints/send-email) API, which sends a pre-configured email to the address with a link that leads a parent to Family Connect where they can approve. Configuration options for this email are provided in the [Compliance Studio](/compliance-studio/creating-product).
## Waiting for consent
The game must determine how long to wait for consent to be granted and what to do if that time expires. The choice of duration could be short (10 minutes) or longer (24 hours or more).
There might be many calls made to the [`/challenge/get-status`](/api/endpoints/get-challenge-status) API during this time. Between calls to [`/challenge/get-status`](/api/endpoints/get-challenge-status), there should be a minimum of 5 seconds delay. Also, [`/challenge/get-status`](/api/endpoints/get-challenge-status) can return HTTP code 429. The game should implement appropriate retry logic when handling 429 responses.
## What can players do while waiting for consent?
For some games, it's appropriate to stop the player from continuing at all in the game until a parent grants consent, and the window should be modal. In other games, it might be reasonable that an underage player is allowed to continue for a certain amount of time before being denied access while trusted adult consent is requested. In this case, while waiting for trusted adult consent, the game is responsible to avoid collecting data about the player, or giving access to other functions that might require consent.
## Pending consent challenges
When a status of `CHALLENGE` is returned from [`/age-gate/check`](/api/endpoints/check-age-gate), the returned challenge ID should be stored in local storage while the challenge is active. The presence of an active challenge when the game starts directs the game to show the same consent challenge window from before. After retrieving the challenge ID from local storage, the [`/challenge/get`](/api/endpoints/get-challenge) API should be invoked to retrieve information about the current challenge, including the one time password and QR code URL, and the challenge window should again be displayed to the user.
## Challenge expiration and time-based authentication
Although the consent challenge itself doesn't expire, the generated time-based authentication methods do (for example, OTP, email link). k-ID implements time-based authentication mechanisms to ensure security while maintaining usability.
### One-time passwords (OTPs)
Each challenge creates a unique OTP that parents can use to access the consent portal. These OTPs are:
- Generated automatically when a challenge is created
- Valid for 1 hour
- Refreshed through the [`/challenge/generate-otp`](/api/endpoints/generate-otp) API
### QR code URLs
The challenge response includes a URL that can be rendered as a QR code for easy mobile access. This URL:
- Contains embedded OTP
- Expires alongside the OTP (1 hour)
- Provides a seamless mobile experience for parents
### Challenge URL and email link expiration
The challenge URL returned from challenge-creating endpoints (such as `/age-gate/check` and `/session/upgrade`) and the link sent by [`/challenge/send-email`](/api/endpoints/send-email) carry the same cryptographically signed challenge JWT. The JWT's `exp` claim governs how long the link is usable:
- **Live mode:** challenge URLs expire **2 weeks** after creation.
- **Test mode:** challenge URLs expire **7 minutes** after creation. The short lifespan is intentional so you can exercise expired-URL handling without waiting two weeks. See [Testing](/concepts/testing) for details on test mode.
To check whether a previously generated URL is still usable without opening it, decode the JWT and inspect the standard `exp` claim. Because the challenge itself doesn't expire (see [Challenges don't expire](#challenge-status)), you don't need to create a new challenge when a link expires. Refresh the access method instead:
- Call [`/challenge/get`](/api/endpoints/get-challenge) with the same `challengeId` to retrieve a fresh URL and OTP for an active challenge.
- Call [`/challenge/send-email`](/api/endpoints/send-email) again to resend the email link.
### Best practices
When implementing k-ID's time-based authentication:
1. **Monitor expiration proactively**, rather than reactively
2. **Implement graceful degradation** when credentials expire
3. **Provide clear user feedback** about timeout status
4. **Test timeout scenarios** thoroughly in test environments
5. **Plan for edge cases** such as network interruptions during refresh operations
## Getting the trusted adult email address
If consent is granted, the email address of the parent is returned in the `approverEmail` field in the response from [`/challenge/get-status`](/api/endpoints/get-challenge-status). This can be stored by the game for use in future customer service cases.
## Testing challenges
It's possible to test trusted adult consent in the API without going through the trusted adult consent flow in Family Connect. To do this, you can call [`/test/set-challenge-status`](/api/endpoints/set-challenge-status) to set the status of the consent challenge. You can assign the status to `PASS` or `FAIL`. You must also pass the `age` and `jurisdiction` as part of the body. Optionally you can pass an email to return in the `approverEmail` field. The next call to [`/challenge/get-status`](/api/endpoints/get-challenge-status) using the same `challengeId` returns the information you assigned. This allows you to rapidly test trusted adult consent scenarios in your game.
---
// File: concepts/access-features-consent/vpc
# Verifiable Parental Consent (VPC)
Verifiable Parental Consent (VPC) is a regulatory requirement that ensures parents or trusted adults can provide informed consent for children to access digital content, services, or features. When a child attempts to access age-restricted content, the system creates a **Challenge** that requires parental approval before access can be granted.
## What's VPC?
VPC is a legal requirement in many jurisdictions (such as COPPA in the United States and GDPR-K in the European Union) that requires platforms to obtain verifiable consent from parents or guardians before collecting, using, or disclosing personal information from children under a certain age.
The VPC flow typically involves:
1. **Age Collection**: Determining the child's age through appropriate methods
2. **Challenge Creation**: Creating a consent challenge when parental approval is required
3. **Parental Notification**: Notifying parents through various channels (email, QR code)
4. **Consent Processing**: Parents review and approve/deny the request
5. **Session Management**: Creating or updating the child's permissions based on consent results
## When's VPC required?
VPC is required when:
- A player's age is below the digital consent age in their jurisdiction
- A player attempts to access features or content that require parental consent
- Regulatory requirements mandate parental consent for data processing
The `/age-gate/check` API automatically determines whether VPC is required based on the player's age and jurisdiction. If VPC is required, the API returns a `CHALLENGE` status with challenge information.
## VPC workflow
### Step 1: Age gate check
When a player provides their age, call `/age-gate/check` with the player's date of birth and jurisdiction. If VPC is required, the API returns:
```json
{
"status": "CHALLENGE",
"challenge": {
"challengeId": "",
"oneTimePassword": "",
"type": "CHALLENGE_PARENTAL_CONSENT",
"url": "https://family.k-id.com/authorize?otp="
}
}
```
### Step 2: Display challenge
Show the consent challenge to the player, including:
- QR code (rendered from the `url` field)
- One-time password
- Input field for parent/guardian email address
### Step 3: Notify trusted adult
If the player provides an email address, call `/challenge/send-email` to send a notification email to the trusted adult. The email contains a link to Family Connect where they can review and approve the request.
### Step 4: Wait for consent
Wait for the trusted adult to complete the consent process. You can:
- Use Webhooks to listen for `Challenge.StateChange` events (recommended)
- Poll the `/challenge/get-status` API periodically
### Step 5: Process result
Once consent is granted or denied:
- **PASS**: Retrieve the session ID and grant access to the player
- **FAIL**: Restrict access and inform the player that consent was denied
## Family Connect
Family Connect is k-ID's parent portal where trusted adults can:
- Review consent requests
- Grant or deny consent
- Manage permissions for their children
- View and update consent settings across multiple games
Once a trusted adult is verified, they can manage consent for multiple children across many games in Family Connect without needing to verify again.
## Trusted adult verification
Before a trusted adult can grant consent, they must verify their identity. k-ID provides multiple methods for trusted adult verification:
- **Credit Card Verification**: Verify using a valid credit card
- **ID Document Verification**: Verify using government-issued ID
- **Social Security Number Verification**: Verify using SSN (United States only)
These methods ensure that only legitimate adults can grant consent for children.
For information about what data is stored for kids and trusted adults, see [Access, features, and consent](/concepts/access-features-consent/overview#what-data-is-stored).
---
// File: concepts/access-features-consent/age-gate
# Age gate
An **Age Gate** is a mechanism used to collect and verify a user's age before allowing access to age-restricted content, features, or services. Age gates are required by regulations in many jurisdictions to ensure compliance with laws governing digital content access for minors.
## What's an age gate?
Age gates serve several important purposes:
- **Regulatory Compliance**: Meet legal requirements for verification in different jurisdictions
- **Content Protection**: Prevent minors from accessing inappropriate content
- **Data Privacy**: Ensure proper handling of children's data according to regulations such as COPPA, GDPR-K, and others
- **Parental Control**: Enable parents to make informed decisions about their children's digital access

## When's an age gate required?
To determine whether an age gate is required, call the [`/age-gate/get-requirements`](/api/endpoints/get-age-gate-requirements) API with the player's jurisdiction. The API returns:
- **`shouldDisplay`**: Whether an age gate should be displayed based on local regulations
- **`approvedAgeCollectionMethods`**: Which methods are allowed for collecting age in this jurisdiction
- **`digitalConsentAge`**: The minimum age at which a player can provide digital consent
- **`civilAge`**: The civil/contract age at which a player is considered a legal adult
- **`minimumAge`**: The minimum age required to access the platform/game
- **`ageAssuranceRequired`**: Whether age verification is required for players in this jurisdiction
:::info
If [`/age-gate/get-requirements`](/api/endpoints/get-age-gate-requirements) responds with `shouldDisplay` = `false`, then no age gate should be shown and the player's date of birth isn't defined. In this case, the game still creates a `Session` by retrieving default permissions for the jurisdiction by calling [`/age-gate/get-default-permissions`](/api/endpoints/get-default-permissions), which means that permissions don't vary based on age in this jurisdiction. Some features in a game might be prohibited for all age audiences based on jurisdiction, so the game should still consult the `Session` permissions to check whether a feature can be enabled.
:::
## Showing an age gate
If an age gate is required (`shouldDisplay` = `true`), the age gate UI should be shown, and the user must enter a date of birth to continue.
Certain jurisdictions are specific on whether an age gate can contain a slider, or must request an explicit date of birth. The allowed methods for collecting age are specified in the `approvedAgeCollectionMethods` field:
- **`date-of-birth`**: Full date of birth (YYYY-MM-DD)
- **`age-slider`**: Age range or approximate age selection
- **`platform-account`**: Using existing platform account age verification
:::tip Best Practice
When showing an age gate, a best practice is to show a "neutral age gate," one which doesn't have an age already set so the user has to take action to set an age. Additionally, if the age gate uses a slider for the age value, it's recommended by the ESRB that the maximum age in a slider age gate should be 35.
:::
## Date of birth format
The date of birth can be provided in any of the following formats:
- `YYYY` (year only)
- `YYYY-MM` (year and month)
- `YYYY-MM-DD` (full date of birth)
The jurisdiction determines which format is required or acceptable. Some jurisdictions require full date of birth, while others allow less precise age information.
## Checking age for access
After the player provides their age, call [`/age-gate/check`](/api/endpoints/check-age-gate) with the date of birth and jurisdiction to determine the next step in the workflow:
- **`PROHIBITED`**: The player's age is below the minimum age for the game. The player should be blocked from continuing.
- **`CHALLENGE`**: The player must complete a challenge before accessing the product. The `challenge.type` field distinguishes the sub-flow (for example `CHALLENGE_PARENTAL_CONSENT` for Verifiable Parental Consent, or `CHALLENGE_AGE_GATE_AGE_ASSURANCE` when Automatic age assurance is enabled).
- **`PASS`**: The player can continue into the game. A session is created or returned on the response.
## Handling age assurance
Some jurisdictions require age assurance in addition to the age gate itself. When the `ageAssuranceRequired` field returned from [`/age-gate/get-requirements`](/api/endpoints/get-age-gate-requirements) is `true`, the game must verify claims beyond the digital consent age by using methods such as facial age estimation or ID document verification. You can satisfy this requirement in two ways:
- **Run assurance yourself**: Treat the age gate `PASS` response as provisional and call [`/age-verification/perform-access-age-verification`](/api/endpoints/perform-access-age-verification) before granting access. You control when and how verification is presented. See the [Age verification quick start guide](/get-started/quickstart-guides/age-verification).
- **Let k-ID run assurance automatically**: Enable [Automatic age assurance](/cdk/age-gate#automatic-age-assurance) on the product in the Compliance Studio. When enabled, `/age-gate/check` returns a `CHALLENGE_AGE_GATE_AGE_ASSURANCE` challenge for players who claim an age old enough to skip parental consent, and the session is created after the player passes the embedded verification. A trustworthy [platform age signal](/cdk/age-signals/overview) can satisfy assurance without the challenge being issued. The feature is gated by an organization-level setting that only k-ID can grant.
If assurance fails or the age the player provided falls under the lowest value in the estimated age range, the player should be treated as the minimum age of that range. They either require trusted adult consent or are blocked altogether, depending on the minimum age configured for the game in the [Compliance Studio](/compliance-studio/creating-product).
---
// File: concepts/access-features-consent/trusted-adult-preferences
# Trusted adult preferences
Trusted adults can configure preferences for how they want to manage consent and permissions for their children. These preferences are configured in Family Connect and affect how consent challenges are presented and processed.
## What are trusted adult preferences?
Trusted adult preferences allow parents and guardians to:
- Set default permission preferences for their children
- Configure how they want to be notified about consent requests
- Manage consent settings across multiple games and products
- Set preferences for how permissions are managed
## Configuring preferences
Trusted adult preferences are configured in the k-ID Family Connect portal. These preferences can be set at different levels:
- **Global preferences**: Preferences that apply across all games and products
- **Product-specific preferences**: Preferences that apply to a specific game or product
- **Child-specific preferences**: Preferences that apply to a specific child
## Preference types
Trusted adults can configure preferences for:
- **Default permissions**: Which permissions should be enabled or turned off by default
- **Notification preferences**: How they want to be notified about consent requests (email, SMS)
- **Consent duration**: How long consent should remain valid
- **Permission management**: Whether they want to approve each permission individually or in groups
## Using preferences in your integration
When a trusted adult grants consent, their preferences are applied to the session. The preferences affect:
- Which permissions are enabled by default
- How future consent requests are handled
- How permission changes are managed
You don't need to implement preference logic yourself - k-ID handles this automatically based on the preferences configured in Family Connect.
## Accessing preferences
Trusted adult preferences aren't directly exposed through the API. Instead, they're applied automatically when:
- A consent challenge is created
- A session is created or updated
- Permissions are upgraded
The preferences are considered when determining which permissions are enabled and how consent challenges are presented.
---
// File: concepts/access-features-consent/data-lite-mode
# Data-lite mode
Data-lite mode is a feature that allows underage users to access a limited version of your game or application while waiting for parental consent. This provides a better user experience by allowing players to start using your product immediately, rather than blocking them completely until consent is granted.
## What's data-lite mode?
Data-lite mode is a restricted experience that:
- Allows players to access basic features of your game
- Prevents data collection that requires parental consent
- Restricts access to features that require consent
- Provides a way for players to continue using your product while waiting for parental consent
## When to use data-lite mode
Data-lite mode is appropriate when:
- A player's age requires Verifiable Parental Consent
- You want to provide a positive user experience while waiting for consent
- You can provide a meaningful experience without collecting personal data
- You can restrict access to features that require consent
## Implementing data-lite mode
When a player's age requires parental consent, you have several options:
### Option 1: Block access completely
Block the player from continuing in the game until a parent grants consent. This is the most restrictive approach and ensures no data is collected until consent is obtained.
### Option 2: Allow limited access
:::warning Important
Allow the player to continue for a certain amount of time before being denied access while trusted adult consent is requested. In this case, while waiting for trusted adult consent, the game is responsible to:
- Avoid collecting data about the player
- Restrict access to features that require consent
- Not enable permissions that require guardian approval
- Provide a clear indication that full access requires parental consent
:::
## Best practices
When implementing data-lite mode:
1. **Clear communication**: Inform players that they're in a limited mode and why
2. **Feature restrictions**: Clearly disable or hide features that require consent
3. **No data collection**: Ensure you're not collecting personal data that requires consent
4. **Consent prompts**: Make it easy for players to request consent from their parents
5. **Time limits**: Consider implementing time limits for data-lite mode to encourage consent completion
## Example implementation
When a challenge is created, you can:
1. Show the consent challenge to the player
2. Allow them to continue in a limited mode
3. Restrict access to features that require consent
4. Monitor for consent completion via webhooks
5. Grant full access once consent is obtained
---
// File: concepts/access-features-consent/essential-features
# Essential features
Essential features are features that are required for a product to function, even if they require parental consent. These features are typically core capabilities that can't be turned off without making the product unusable.
## What are essential features?
Essential features are features that:
- Are required for the product to function
- Can't be turned off without breaking core capabilities
- Might require parental consent but are necessary for the product to work
- Are typically included in the initial consent flow
## Essential features compared to optional features
- **Essential features**: Must be enabled for the product to function (for example, account creation, basic gameplay)
- **Optional features**: Can be turned off without breaking core capabilities (for example, social features, in-game purchases)
## Configuring essential features
Essential features are configured in the [Compliance Studio](/compliance-studio/product-api-configuration#permissions) for your product. When configuring permissions, you can mark certain permissions as essential, which means:
- They're included in the initial consent request
- They can't be turned off by parents (though they can still be denied consent entirely)
- They're required for the product to function
## Multi-product approval and essential features
When using multi-product approval, if one product has an essential feature that requires consent, the entire consent flow might require that feature to be approved. This is because essential features are necessary for the product to function.
For more information on multi-product approval, see [Multi-product approval](/concepts/multi-product-approval).
---
// File: events/dom-events/event-structures/verification-error
# `Verification.Error`
Emitted when a verification operation encounters an error.
## Fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `eventType` | string | yes | Always `"Verification.Error"` |
| `method` | string | yes | Verification method used (for example, `"age-key"`) |
| `status` | string | yes | Always `"ERROR"` |
## Example
```json
{
"eventType": "Verification.Error",
"method": "age-key",
"status": "ERROR"
}
```
---
// File: events/dom-events/event-structures/verification-result
# `Verification.Result`
Emitted with the result of a verification attempt.
## Fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `eventType` | string | yes | Always `"Verification.Result"` |
| `data` | object | yes | Verification result data |
| `data.verificationId` | string (UUID) | yes | Unique verification ID |
| `data.status` | string | yes | `"PASS"` or `"FAIL"` |
| `data.method` | string | yes | Verification method used (for example, `"age-estimation-scan"`) |
| `data.provider` | string | yes | Verification provider (for example, `"privately"`) |
| `data.age` | object | no | Age range details |
| `data.age.low` | number | no | Lower bound of estimated age |
| `data.age.high` | number | no | Upper bound of estimated age |
| `data.ageCategory` | string | no | One of `"adult"`, `"digital-youth"`, `"digital-minor"` |
| `data.dob` | string | no | The date of birth in ISO 8601 format (YYYY-MM-DD). This is only set if the verification method provides it (for example, ID document scan) |
## Example
```json
{
"eventType": "Verification.Result",
"data": {
"verificationId": "4e57301e-a4d1-498f-ac3f-f3d4de19abf6",
"status": "PASS",
"method": "id-document",
"provider": "veratad",
"age": {
"low": 43,
"high": 43
},
"ageCategory": "adult",
"dob": "1981-06-20"
}
}
```
---
// File: events/dom-events/event-structures/widget-agegate-challenge
# `Widget.AgeGate.Challenge`
Emitted when the Age Gate flow triggers a challenge.
## Fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `eventType` | string | yes | Always `"Widget.AgeGate.Challenge"` |
| `data` | object | yes | Challenge details |
| `data.status` | string | yes | Challenge status (for example, `"PENDING"`) |
| `data.challengeId` | string (UUID) | yes | Challenge ID |
## Example
```json
{
"eventType": "Widget.AgeGate.Challenge",
"data": {
"status": "PENDING",
"challengeId": "a04467a0-83de-40ca-8572-8cc761e896aa"
}
}
```
---
// File: events/dom-events/event-structures/widget-agegate-result
# `Widget.AgeGate.Result`
Emitted when the Age Gate flow completes with a result. A session is created when the flow completes successfully, and a challenge is created whenever the flow needs one, either for Verifiable Parental Consent or for [Automatic age assurance](/cdk/age-gate#automatic-age-assurance) when the player claims an age old enough to skip parental consent.
## Fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `eventType` | string | yes | Always `"Widget.AgeGate.Result"` |
| `data` | object | yes | Result details |
| `data.status` | string | yes | Result status (for example, `"PASS"`) |
| `data.sessionId` | string (UUID) | no | Session ID when the flow completes successfully. Always present when `status` is `"PASS"`. |
| `data.challengeId` | string (UUID) | no | Challenge ID when a challenge was resolved during the flow (for example, parental consent or auto age-assurance). Only present when a challenge was created. |
## Examples
### When a challenge was resolved during the flow
```json
{
"eventType": "Widget.AgeGate.Result",
"data": {
"status": "PASS",
"sessionId": "f1af4704-74b4-4966-b13a-18eb16bccf9c",
"challengeId": "a04467a0-83de-40ca-8572-8cc761e896aa"
}
}
```
### When no challenge was required
```json
{
"eventType": "Widget.AgeGate.Result",
"data": {
"status": "PASS",
"sessionId": "f1af4704-74b4-4966-b13a-18eb16bccf9c"
}
}
```
---
// File: events/dom-events/event-structures/widget-datanotices-consentapproved
# `Widget.DataNotices.ConsentApproved`
Emitted when a user approves data notices consent.
## Fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `eventType` | string | yes | Always `"Widget.DataNotices.ConsentApproved"` |
| `data` | object | yes | Consent details |
| `data.jurisdiction` | string | yes | Jurisdiction code (for example, `"US"`) |
## Example
```json
{
"eventType": "Widget.DataNotices.ConsentApproved",
"data": {
"jurisdiction": "US"
}
}
```
---
// File: events/dom-events/event-structures/widget-exitreview
# `Widget.ExitReview`
Emitted when a widget or age verification exits a flow by clicking on the 'Done' button.
## Fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `eventType` | string | yes | Always `"Widget.ExitReview"` |
| `data` | object | yes | Event data |
| `data.sessionId` | string (UUID) | no | Present when a session is available |
## Example
```json
{
"eventType": "Widget.ExitReview",
"data": {
"sessionId": "68f102fa-c7bb-41b7-b859-03a57a4e0348"
}
}
```
---
// File: events/dom-events/overview
# DOM events overview
This section documents DOM events emitted by k-ID widgets and flows. Use these events to react to user actions in your application. Event payload schemas are listed under Event structures.
## Event structures
| Event Type | Description |
|------------|-------------|
| [`Verification.Error`](/events/dom-events/event-structures/verification-error) | Emitted when a verification operation encounters an error |
| [`Verification.Result`](/events/dom-events/event-structures/verification-result) | Emitted with the result of a verification attempt |
| [`Widget.AgeGate.Challenge`](/events/dom-events/event-structures/widget-agegate-challenge) | Emitted when the Age Gate flow triggers a challenge |
| [`Widget.AgeGate.Result`](/events/dom-events/event-structures/widget-agegate-result) | Emitted when the Age Gate flow completes with a result |
| [`Widget.DataNotices.ConsentApproved`](/events/dom-events/event-structures/widget-datanotices-consentapproved) | Emitted when a user approves data notices consent |
| [`Widget.ExitReview`](/events/dom-events/event-structures/widget-exitreview) | Emitted when a widget exits a review flow by clicking on the 'Done' button |
## Domain validation
When listening for DOM events, always validate the message origin to ensure events are coming from k-ID:
```javascript
window.addEventListener('message', (event) => {
// Validate origin for security
if (!event.origin.endsWith('.k-id.com')) {
return;
}
// Handle the event
console.log('Event:', event.data);
});
```
---
// File: events/overview
# Events overview
k-ID provides two types of events to help you respond to user actions and system changes in real-time - **DOM Events** and **Webhooks**. Both event types allow you to build responsive integrations that react to important changes in the k-ID system.
## What are events?
Events are notifications sent by k-ID when specific actions occur in your application. They allow you to:
- **React to user actions**: Respond immediately when users complete verification, grant consent, or interact with widgets
- **Update your application state**: Keep your application synchronized with k-ID's state changes
- **Handle asynchronous operations**: Receive notifications when long-running processes complete
- **Build real-time experiences**: Create responsive user interfaces that update automatically
## DOM events
**DOM Events** are JavaScript events emitted by k-ID widgets that run in iframes on your website. These events are sent via the browser's `postMessage` API and can be listened to using standard JavaScript event listeners.
### When to use DOM Events
- **Responsive UI updates**: To update your interface immediately when users interact with widgets
- **Client-side interactions**: When you need to react to widget interactions in the browser
- **Real-time user feedback**: To provide immediate visual feedback to users
### Key features
- Emitted directly from widgets in iframes
- Received via JavaScript `message` event listeners
- Requires origin validation for security
- Perfect for client-side JavaScript applications and responsive UI
For detailed information, see [DOM Events](/events/dom-events/overview).
## Webhooks
**Webhooks** are HTTP POST requests sent from k-ID's servers to your server endpoints. They provide server-to-server notifications about important events in the k-ID system.
### When to use webhooks
- **Data integrity**: To reliably update your database and maintain data consistency
- **Server-side processing**: When you need to handle events on your servers
- **Reliable delivery**: For critical events that must be processed even if users navigate away
- **Background processing**: To handle events asynchronously without blocking user interactions
- **State synchronization**: To keep your server state synchronized with k-ID's state
### Key features
- Sent as HTTP POST requests to your configured endpoints
- Include cryptographic signatures for security validation
- Configured per Product in Compliance Studio
- Ideal for server-side integrations, data integrity, and reliable state management
For detailed information, see [Webhooks](/events/webhooks/overview).
## Choosing between DOM events and webhooks
Both event types serve different purposes and can be used together:
| Use Case | Recommended Approach |
|----------|---------------------|
| Real-time UI updates in browser | DOM Events |
| Data integrity and state management | Webhooks or server-side API calls |
| Server-side processing and database updates | Webhooks |
| Responsive user feedback | DOM Events |
| Reliable event processing | Webhooks |
| Widget interactions | DOM Events |
:::tip Best Practice
Use DOM Events for responsive UI updates, and use webhooks or server-side API calls (such as [`/age-verification/get-status`](/api/endpoints/get-age-verification-status) or [`/challenge/get-status`](/api/endpoints/get-challenge-status)) for data integrity and reliable state management. Many applications use both, DOM Events for immediate UI feedback and webhooks for reliable server-side processing and data persistence.
:::
## Getting started
- **[DOM Events](/events/dom-events/overview)** - Learn about DOM events emitted by widgets
- **[Webhooks](/events/webhooks/overview)** - Learn about webhook configuration and handling
- **[Event structures](/events/dom-events/event-structures/verification-result)** - Explore available event types and payloads
- **[Webhook event types](/events/webhooks/event-types/challenge-statechange)** - Explore available webhook event types
---
// File: events/webhooks/event-types/account-delete
# `Account.Delete`
Emitted when an account is deleted.
## Fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `eventType` | string | yes | Always `"Account.Delete"` |
| `data` | object | yes | Deletion details |
| `data.kuid` | string (UUID) | yes | k-ID user ID |
| `data.productId` | number | yes | Product ID |
## Example
```json
{
"eventType": "Account.Delete",
"data": {
"kuid": "7a1f2c3d-4e5f-6789-abcd-ef0123456789",
"productId": 11472
}
}
```
---
// File: events/webhooks/event-types/ageassurance-result
# `AgeAssurance.Result`
Emitted with the result of an Age Assurance evaluation.
:::warning Deprecated
This event type is deprecated and has been replaced by `Verification.Result`.
:::
## Fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `eventType` | string | yes | Always `"AgeAssurance.Result"` |
| `data` | object | yes | Age assurance result data |
| `data.id` | string (UUID) | yes | Verification ID |
| `data.status` | string | yes | `PASS` or `FAIL` |
| `data.ageCategory` | string | no | `adult` | `digital-youth` | `digital-minor` |
| `data.method` | string | no | Verification method (for example, `age-estimation`) |
| `data.age` | object | no | Age details when available |
| `data.age.low` | number | no | Lower bound of estimated age |
| `data.age.high` | number | no | Upper bound of estimated age |
| `data.dob` | string (YYYY-MM-DD) | no | Date of birth when available |
## Example
```json
{
"eventType": "AgeAssurance.Result",
"data": {
"id": "2f2b1e6d-9e3d-4c96-9c0e-7b9d8a5e5f10",
"status": "PASS",
"ageCategory": "digital-youth",
"method": "age-estimation",
"age": { "low": 14, "high": 16 },
"dob": "2011-07-12"
}
}
```
---
// File: events/webhooks/event-types/challenge-statechange
# `Challenge.StateChange`
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
Emitted when a challenge changes state.
:::info Delivery and recovery
Webhook events are retried up to 2 times on failure. If your server misses an event, poll [`GET /challenge/get-status`](/api/endpoints/get-challenge-status) with the saved challenge ID. See [Delivery, retries, and recovery](/events/webhooks/overview#delivery-retries-and-recovery) for details.
:::
:::info Parental consent challenges don't expire
A parental consent challenge remains pending until a trusted adult either approves or denies it. Only the access methods (OTP, QR code, email link) expire, not the challenge itself. For details, see [Challenge expiration and time-based authentication](/concepts/access-features-consent/challenges#challenge-expiration-and-time-based-authentication).
:::
## Fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `eventType` | string | yes | Always `"Challenge.StateChange"` |
| `data` | object | yes | Challenge state change data |
| `data.id` | string (UUID) | yes | Challenge ID |
| `data.productId` | number | yes | The `productId` for the product |
| `data.status` | string | yes | One of `PASS`, `FAIL`, `IN_PROGRESS` |
| `data.type` | string | yes | The challenge type. One of: `CHALLENGE_SESSION_UPGRADE`, `CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE`, `CHALLENGE_PARENTAL_CONSENT`, `CHALLENGE_AGE_ATTESTATION`, `CHALLENGE_CUSTOM`, `CHALLENGE_BULK_APPROVAL_REQUEST`, `CHALLENGE_UPDATE_JURISDICTION`, `CHALLENGE_MATERIAL_CHANGE`, `CHALLENGE_AGE_GATE_AGE_ASSURANCE`, `CHALLENGE_PARENT_INVITE` |
| `data.dob` | string (YYYY-MM-DD) | no | Player date of birth; present when available |
| `data.sessionId` | string (UUID) | no | Present when `status` is `PASS` |
| `data.approverEmail` | string | no | Present on `PASS` when a trusted adult approved the challenge |
| `data.kuid` | string | no | The `kuid` (k-ID user ID); present when available |
## Example
```json
{
"eventType": "Challenge.StateChange",
"data": {
"id": "9d6b056e-7d62-4a9e-907a-3d0f6f1d1b9a",
"productId": 11472,
"status": "PASS",
"type": "CHALLENGE_PARENTAL_CONSENT",
"dob": "2011-07-12",
"sessionId": "b6d1a7c2-8f34-4c83-bf0b-3a6d4a2f9d31",
"approverEmail": "parent@example.com",
"kuid": "7a1f2c3d-4e5f-6789-abcd-ef0123456789"
}
}
```
```json
{
"eventType": "Challenge.StateChange",
"data": {
"id": "9d6b056e-7d62-4a9e-907a-3d0f6f1d1b9a",
"productId": 11472,
"status": "FAIL",
"type": "CHALLENGE_SESSION_UPGRADE"
}
}
```
```json
{
"eventType": "Challenge.StateChange",
"data": {
"id": "9d6b056e-7d62-4a9e-907a-3d0f6f1d1b9a",
"productId": 11472,
"status": "IN_PROGRESS",
"type": "CHALLENGE_PARENTAL_CONSENT"
}
}
```
## Challenge event contract
This section documents the complete contract for challenge state change events, including all possible fields, their presence rules, and how to interpret different statuses.
:::warning DOM messages aren't authoritative
Challenge results are also communicated to front-end applications via DOM messages for UI control purposes. However, **only webhook events and the [`GET /challenge/get-status`](/api/endpoints/get-challenge-status) endpoint responses should be considered reliable, authoritative data sources** for integration logic. DOM messages are provided solely for front-end application state management and shouldn't be used as the basis for critical business decisions or data persistence.
:::
### Data structures
#### Webhook event structure
Webhook events are wrapped in an event envelope:
```json
{
"eventType": "Challenge.StateChange",
"data": {
"id": "uuid",
"productId": number,
"status": "PASS" | "FAIL" | "IN_PROGRESS",
"type": "string",
"dob": "YYYY-MM-DD" | null,
"sessionId": "uuid" | null,
"approverEmail": "string" | null,
"kuid": "string" | null
}
}
```
#### API endpoint response structure
The [`GET /challenge/get-status`](/api/endpoints/get-challenge-status) endpoint returns a direct response:
```json
{
"id": "uuid",
"status": "PASS" | "FAIL" | "PENDING" | "IN_PROGRESS",
"dob": "YYYY-MM-DD" | null,
"sessionId": "uuid" | null,
"approverEmail": "string" | null
}
```
For complete endpoint documentation, see [`GET /challenge/get-status`](/api/endpoints/get-challenge-status).
### Key differences between webhook and API endpoint
| Aspect | Webhook Event | API Endpoint |
|--------|---------------|--------------|
| **Status values** | `PASS`, `FAIL`, `IN_PROGRESS` | `PASS`, `FAIL`, `PENDING`, `IN_PROGRESS` |
| **Structure** | Wrapped in `{ eventType, data }` | Direct response object |
| **`productId`** | Always included | Not included |
| **`type`** | Always included | Not included |
| **`kuid`** | Included when available | Not included |
| **When available** | Only when challenge state changes | Can be polled at any time |
### Status types
#### PASS
The challenge completed successfully. A session has been created or updated with the approved permissions and `sessionId` is included.
**Availability:**
- Webhook: ✅ Sent when the challenge completes successfully
- API Endpoint: ✅ Returned when the challenge has completed successfully
#### FAIL
The challenge was explicitly rejected (for example, a trusted adult denied the request, or the player didn't meet the age criteria). Parental consent challenges don't automatically fail due to timeout or expiration; a `FAIL` only occurs when someone actively rejects the challenge.
**Availability:**
- Webhook: ✅ Sent when the challenge is denied or fails
- API Endpoint: ✅ Returned when the challenge has been denied or failed
#### IN_PROGRESS
Someone has started the challenge flow but hasn't yet completed it.
**Availability:**
- Webhook: ✅ Sent when the flow begins
- API Endpoint: ✅ Returned when the flow is in progress
#### PENDING
The challenge has been created but no one has accessed it yet.
**Availability:**
- Webhook: ❌ Never sent (no state change has occurred)
- API Endpoint: ✅ Returned when challenge is awaiting action
### Field presence rules by status
#### Status: `PASS`
**Webhook events**
**Always included:**
- `id` (UUID) - Challenge ID
- `productId` (number) - Product ID
- `status` ("PASS")
- `type` (string) - The challenge type (for example, `CHALLENGE_PARENTAL_CONSENT`, `CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE`)
- `sessionId` (UUID) - The created or updated session ID
**Sometimes included:**
- `dob` (string, YYYY-MM-DD) - Player's confirmed date of birth, if the trusted adult confirmed or corrected it
- `approverEmail` (string) - Email of the trusted adult who approved
- `kuid` (string) - k-ID user ID, if the player has one
**API endpoint**
**Always included:**
- `id` (UUID) - Challenge ID
- `status` ("PASS")
- `sessionId` (UUID) - The created or updated session ID
**Sometimes included:**
- `dob` (string, YYYY-MM-DD) - Player's confirmed date of birth
- `approverEmail` (string) - Email of the trusted adult who approved
:::tip What you need to know
- Use the `sessionId` to fetch the full session with [`GET /session/get`](/api/endpoints/get-session)
- Store the `approverEmail` for customer support cases
- The `dob` might differ from what the player originally entered if the trusted adult corrected it
:::
#### Status: `FAIL`
**Webhook events**
**Always included:**
- `id` (UUID) - Challenge ID
- `productId` (number) - Product ID
- `status` ("FAIL")
- `type` (string) - The challenge type (for example, `CHALLENGE_SESSION_UPGRADE`, `CHALLENGE_PARENTAL_CONSENT`)
**Never included:**
- `sessionId` - No session is created when consent is denied
- `approverEmail` - Not provided for denied requests
**API endpoint**
**Always included:**
- `id` (UUID) - Challenge ID
- `status` ("FAIL")
**Never included:**
- `sessionId`
- `approverEmail`
:::tip What you need to know
- A `FAIL` status means the trusted adult explicitly denied the request
- Display an appropriate message to the player explaining that their parent declined the request
- The player can request consent again by creating a new challenge
:::
#### Status: `IN_PROGRESS`
**Webhook events**
**Always included:**
- `id` (UUID) - Challenge ID
- `productId` (number) - Product ID
- `status` ("IN_PROGRESS")
- `type` (string) - The challenge type (for example, `CHALLENGE_PARENTAL_CONSENT`, `CHALLENGE_SESSION_UPGRADE`)
**Never included:**
- `sessionId`
- `approverEmail`
- `dob`
**API endpoint**
**Always included:**
- `id` (UUID) - Challenge ID
- `status` ("IN_PROGRESS")
:::tip What you need to know
- This status indicates a trusted adult is actively working through the consent flow
- Consider updating your UI to show "Parent is reviewing" or similar
- The next webhook is either `PASS` or `FAIL`
:::
#### Status: `PENDING` (API only)
**API endpoint**
**Always included:**
- `id` (UUID) - Challenge ID
- `status` ("PENDING")
**Never included:**
- All other fields
:::tip What you need to know
- The challenge is waiting for a trusted adult to access it
- Continue displaying the QR code, OTP, or email option to the player
- Consider refreshing expired access methods with [`/challenge/generate-otp`](/api/endpoints/generate-otp)
:::
### Field presence summary
#### Webhook events
| Field | PASS | FAIL | IN_PROGRESS |
|-------|------|------|-------------|
| `id` | always | always | always |
| `productId` | always | always | always |
| `status` | always | always | always |
| `type` | always | always | always |
| `sessionId` | always | never | never |
| `dob` | sometimes | never | never |
| `approverEmail` | sometimes | never | never |
| `kuid` | sometimes | never | never |
#### API endpoint
| Field | PASS | FAIL | PENDING | IN_PROGRESS |
|-------|------|------|---------|-------------|
| `id` | always | always | always | always |
| `status` | always | always | always | always |
| `sessionId` | always | never | never | never |
| `dob` | sometimes | never | never | never |
| `approverEmail` | sometimes | never | never | never |
### Implementation checklist
- [ ] Handle all three webhook statuses: `PASS`, `FAIL`, `IN_PROGRESS`
- [ ] Use `type` to differentiate challenge types (for example, `CHALLENGE_PARENTAL_CONSENT` versus `CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE`) without a database lookup
- [ ] On `PASS`, extract `sessionId` and fetch the full session
- [ ] On `FAIL`, display appropriate messaging to the player
- [ ] On `IN_PROGRESS`, optionally update UI to show consent is being reviewed
- [ ] Store `approverEmail` when available for customer support
- [ ] Don't assume optional fields are present; check before accessing
- [ ] Use webhook events for real-time updates, API polling as fallback
---
// File: events/webhooks/event-types/session-changepermissions
# `Session.ChangePermissions`
For Session permission changes, events of this type are only sent to your webhook receiver when the permissions are directly modified by a parent in k-ID Family Connect. If a player has a birthday that would put them in a different age category, or greater than an age threshold for their jurisdiction which would allow them access to more features, the game must call the [/session/get](/api/endpoints/get-session) API to receive the changed permissions for the `Session`.
## Fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `eventType` | string | yes | Always `"Session.ChangePermissions"` |
| `data` | object | yes | Session change permissions data |
| `data.id` | string (UUID) | yes | Session ID |
| `data.productId` | number | yes | Product ID |
## Example
```json
{
"eventType": "Session.ChangePermissions",
"data": {
"id": "78c299b2-5c33-4bde-84fe-8fc950fc7a96",
"productId": 42
}
}
```
---
// File: events/webhooks/event-types/session-delete
# `Session.Delete`
Emitted when a session is deleted. This event fires when a parent revokes all access to the product for a player through Family Connect, effectively removing the player's session entirely.
When you receive this event, the player must complete the age gate and consent flow again to regain access to your product.
## Fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `eventType` | string | yes | Always `"Session.Delete"` |
| `data` | object | yes | Session deletion data |
| `data.id` | string (UUID) | yes | Session ID |
| `data.productId` | number | yes | Product ID |
## Example
```json
{
"eventType": "Session.Delete",
"data": {
"id": "2d064cf7-0726-4193-b19a-8bd387937e60",
"productId": 42
}
}
```
---
// File: events/webhooks/event-types/test
# `Test`
This event type is used to verify that the webhook is working correctly. It should be handled by the webhook receiver.
When a webhook secret is configured, two test events are fired: one with a valid signature and one without a valid signature. This allows you to verify that your signature validation logic correctly accepts valid requests and rejects invalid ones.
## Fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `eventType` | string | yes | Always `"Test"` |
| `data` | object | yes | Test event data |
| `data.id` | string (UUID) | yes | Unique test event identifier |
## Example
```json
{
"eventType": "Test",
"data": {
"id": "12345678-1234-1234-1234-123456789abc"
}
}
```
---
// File: events/webhooks/event-types/verification-result
# `Verification.Result`
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
Emitted with the result of a verification attempt.
:::info Delivery and recovery
Webhook events are retried up to 2 times on failure. If your server misses an event, poll [`GET /age-verification/get-status`](/api/endpoints/get-age-verification-status) with the saved verification ID. See [Delivery, retries, and recovery](/events/webhooks/overview#delivery-retries-and-recovery) for details.
:::
## Fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `eventType` | string | yes | Always `"Verification.Result"` |
| `data` | object | yes | Verification result data |
| `data.id` | string (UUID) | yes | The unique verification ID |
| `data.status` | string | yes | Can be `PASS` or `FAIL` |
| `data.ageCategory` | string | no | The estimated age category. Can be `adult`, `digital-youth`, or `digital-minor`. Always present for `PASS` status. For `FAIL` status, only present when `failureReason` is `age-criteria-not-met` and `age` is present |
| `data.method` | string | no | The verification method used. Always present for `PASS` status. For `FAIL` status, only present if `failureReason` is `age-criteria-not-met` |
| `data.failureReason` | string | no | The reason the verification failed. Always present when `status` is `FAIL`. Can be `age-criteria-not-met`, `max-attempts-exceeded`, or `fraudulent-activity-detected` |
| `data.age` | object | no | The age details. Present when both `age.low` and `age.high` are available. For `FAIL` status, only present if `failureReason` is `age-criteria-not-met` |
| `data.age.low` | number | no | The lower bound of the estimated age. In the case of a hard age verification method, such as an ID document check, this is the exact age |
| `data.age.high` | number | no | The upper bound of the estimated age. In the case of a hard age verification method, such as an ID document check, this is the exact age |
| `data.dob` | string (YYYY-MM-DD) | no | Verified date of birth. Only included when the verification method provides it. Always included if available from the method |
## Example
```json
{
"eventType": "Verification.Result",
"data": {
"id": "4e57301e-a4d1-498f-ac3f-f3d4de19abf6",
"status": "PASS",
"method": "id-document",
"age": {
"low": 43,
"high": 43
},
"dob": "1981-06-20"
}
}
```
```json
{
"eventType": "Verification.Result",
"data": {
"id": "fe10accb-b845-4fc8-ac44-6130b7e0b8bd",
"status": "FAIL",
"method": "age-estimation-scan",
"age": {
"low": 13,
"high": 17
},
"failureReason": "age-criteria-not-met"
}
}
```
```json
{
"eventType": "Verification.Result",
"data": {
"id": "123e4567-e89b-12d3-a456-426614174002",
"status": "FAIL",
"failureReason": "max-attempts-exceeded"
}
}
```
## Verification event contract {#verification-event-contract}
### Overview
This section defines the API contract for verification status delivered to you through two mechanisms:
1. **Webhook Events**: [`Verification.Result`](/events/webhooks/event-types/verification-result) webhook events sent when a verification completes
2. **API Endpoint**: [`GET /age-verification/get-status`](/api/endpoints/get-age-verification-status) endpoint for polling verification status
This contract specifies field presence rules, data types, and interpretation guidance for all possible outcome types. While both mechanisms provide similar data, there are important differences in status values, field presence, and structure that you must be aware of.
:::warning DOM Messages Aren't Authoritative
Verification results are also communicated to front-end applications via DOM messages for UI control purposes. However, **only [webhook events](/events/webhooks/overview) and the [`GET /age-verification/get-status`](/api/endpoints/get-age-verification-status) endpoint responses should be considered reliable, authoritative data sources** for integration logic. DOM messages are provided solely for front-end application state management and shouldn't be used as the basis for critical business decisions or data persistence.
:::
### Data structures
#### Webhook event structure
Webhook events are wrapped in an event envelope:
```json
{
"eventType": "Verification.Result",
"data": {
"id": "uuid",
"status": "PASS" | "FAIL",
"ageCategory": "adult" | "digital-youth" | "digital-minor",
"method": "id-document" | "credit-card" | "self-confirmation" | "social-security-number" | "email-confirmation" | "age-estimation-scan",
"age": {
"low": number,
"high": number
},
"dob": "YYYY-MM-DD",
"failureReason": string
}
}
```
#### API endpoint response structure
The [`GET /age-verification/get-status`](/api/endpoints/get-age-verification-status) endpoint returns a direct response:
```json
{
"id": "uuid",
"status": "PASS" | "FAIL" | "PENDING" | "IN_PROGRESS",
"ageCategory": "adult" | "digital-youth" | "digital-minor",
"method": "id-document" | "credit-card" | "self-confirmation" | "social-security-number" | "email-confirmation" | "age-estimation-scan",
"age": {
"low": number,
"high": number
},
"dob": "YYYY-MM-DD",
"failureReason": string
}
```
**Query Parameters:**
- `id` (required): The verification ID
- `includeDob` (optional, default: `false`): If `true`, includes the `dob` field when available
For complete endpoint documentation, see [`GET /age-verification/get-status`](/api/endpoints/get-age-verification-status).
### Key differences between webhook and API endpoint
| Aspect | Webhook Event | API Endpoint |
|--------|---------------|--------------|
| **Status Values** | Only `PASS` or `FAIL` | `PASS`, `FAIL`, `PENDING`, `IN_PROGRESS` |
| **Structure** | Wrapped in `{ eventType, data }` | Direct response object |
| **DOB Field** | Always included if available | Only included if `includeDob=true` query parameter is set |
| **AgeCategory** | Present for `PASS` status, and for `FAIL` status when `failureReason` is `age-criteria-not-met` | Present for both `PASS` and `FAIL` statuses when age data is available |
| **Method** | Only present when status is `PASS` | Present for both `PASS` and `FAIL` statuses |
| **When Available** | Only when verification completes | Can be polled at any time, including during verification |
### Status types
#### PASS
The verification successfully determined that the user meets the age criteria.
**Availability:**
- Webhook: ✅ Sent when verification completes successfully
- API Endpoint: ✅ Returned when verification is complete and successful
#### FAIL
The verification determined that the user doesn't meet the age criteria, or the verification process failed because a conclusive age determination couldn't be made, or deceptive behavior was detected. These scenarios result in a FAIL status with the appropriate `failureReason` values.
**Availability:**
- Webhook: ✅ Sent when verification completes with failure
- API Endpoint: ✅ Returned when verification is complete and failed
#### Status: PENDING
The verification request has been created but hasn't yet started processing.
**Availability:**
- Webhook: ❌ Never sent (webhooks only fire on completion)
- API Endpoint: ✅ Returned when verification is in initial state
**API Endpoint Response for PENDING:**
- `id` (UUID) - always included
- `status` ("PENDING") - always included
- All other fields are absent
#### Status: IN_PROGRESS
The verification is currently being processed.
**Availability:**
- Webhook: ❌ Never sent (webhooks only fire on completion)
- API Endpoint: ✅ Returned when verification is actively being processed
**API Endpoint Response for IN_PROGRESS:**
- `id` (UUID) - always included
- `status` ("IN_PROGRESS") - always included
- All other fields are absent
### Field presence rules by status
#### Webhook events
Webhook events are only sent when a verification completes (status is `PASS` or `FAIL`). They don't include intermediate statuses such as `PENDING` or `IN_PROGRESS`.
#### Status: `PASS`
**Webhook events**
**Always included:**
- `id` (UUID)
- `status` ("PASS")
- `method` (string) - The verification method used
- `age` (object) - Contains both `low` and `high` bounds
- `ageCategory` (string) - One of: `adult`, `digital-youth`, `digital-minor`
**Sometimes included:**
- `dob` (string, YYYY-MM-DD format) - Verified date of birth, only when the verification method provides it (see [Date of birth field](#date-of-birth-field) section for specific methods). Always included if available.
**Never included:**
- `failureReason`
**API endpoint**
**Always included:**
- `id` (UUID)
- `status` ("PASS")
- `method` (string) - The verification method used (if available)
- `age` (object) - Contains both `low` and `high` bounds
- `ageCategory` (string) - One of: `adult`, `digital-youth`, `digital-minor`
**Sometimes included:**
- `dob` (string, YYYY-MM-DD format) - Only included if `includeDob=true` query parameter is set AND the verification method provides it (see [Date of birth field](#date-of-birth-field) section for specific methods)
**Never included:**
- `failureReason`
:::tip What you need to know
- Treat PASS status as a successful verification
- `age` and `ageCategory` are always present for PASS status - all verification methods that result in PASS provide age information and age category
- Use the `ageCategory` field to determine user permissions and access levels
- Don't rely solely on `age.low` or `age.high` for access control, use `ageCategory` instead
- You can use `dob` if it's provided, but don't assume it's always available
- For the API endpoint: Set `includeDob=true` in your query parameters if you need DOB data (see [`GET /age-verification/get-status`](/api/endpoints/get-age-verification-status))
:::
#### Status: `FAIL`
**Webhook events**
**Always included:**
- `id` (UUID)
- `status` ("FAIL")
- `failureReason` (string) - Reason for the failure
**Sometimes included:**
- `method` (string) - Only present if failure isn't due to `max-attempts-exceeded` or `fraudulent-activity-detected`
- `age` (object) - Only present if `failureReason` is `age-criteria-not-met`
- `ageCategory` (string) - Only present when `failureReason` is `age-criteria-not-met` and `age` is present
- `dob` (string) - Only present when the verification method provides it (see [Date of birth field](#date-of-birth-field) section for specific methods). Always included if available.
**API endpoint**
**Always included:**
- `id` (UUID)
- `status` ("FAIL")
- `failureReason` (string) - Reason for the failure
**Sometimes included:**
- `method` (string) - Only present if failure is due to `age-criteria-not-met`
- `age` (object) - Only present if `failureReason` is `age-criteria-not-met`
- `ageCategory` (string) - Only present if `age` is present
- `dob` (string) - Only included if `includeDob=true` query parameter is set AND the verification method provides it (see [Date of birth field](#date-of-birth-field) section for specific methods)
**Never included:**
- `ageCategory` when `age` isn't present
:::tip What you need to know
- Treat `FAIL` status as an unsuccessful verification, don't grant access or permissions
- Always check `failureReason` to understand why the verification failed
- Don't assume `age` fields are present for FAIL status, they might not be available
- If `age` is present in a `FAIL` result, you can use it for logging or analytics, but don't use it for access control decisions
- Handle cases where `method` and `age` are absent, this indicates a system-level failure, not an age determination failure
- For webhook events: `ageCategory` might be present for `FAIL` status when `failureReason` is `age-criteria-not-met`, but don't use it for access control decisions
- For API endpoint: Even if `ageCategory` is present for `FAIL` status, don't use it for access control decisions
:::
**Common Failure Reasons:**
- `age-criteria-not-met` - User's verified age doesn't meet the required threshold
- `max-attempts-exceeded` - User has exhausted all available verification attempts (this is the final failure reason when users exhaust retries after failed or inconclusive age determinations)
- `fraudulent-activity-detected` - System detected suspicious or fraudulent behavior
### Age field population rules
#### When `age` object is present
The `age` object contains two fields:
- `low` (number): The lower bound of the age estimate, or the exact age for hard verification methods (for example, ID document). When an exact age isn't provided by the verification method, this is the threshold minimum age required for the verification.
- `high` (number): The upper bound of the age estimate, or the exact age for hard verification methods. When an exact age isn't provided by the verification method, this is 100.
**Conditions for `age` to be present:**
- Always present for PASS status - all verification methods that result in PASS provide age information
- For FAIL status: `failureReason` is `age-criteria-not-met`
:::tip What you need to know
- For PASS status, `age` is always present - you can safely access `age.low` and `age.high` without checking for presence
- Always check that both `age.low` and `age.high` exist before accessing the `age` object (defensive programming for FAIL status)
- Don't assume that `age.low === age.high` means you have an exact age, it might, but it depends on the verification method
- Don't use `age` fields for access control when `status` isn't PASS
- For PASS status, prefer using `ageCategory` over raw `age` values when making access decisions
:::
#### When `ageCategory` is present
The `ageCategory` field is derived from the age and jurisdiction information.
**Values:**
- `adult` - User is classified as an adult
- `digital-youth` - User is classified as a digital youth (age-restricted but not a minor)
- `digital-minor` - User is classified as a digital minor
**Conditions for `ageCategory` to be present:**
- Always present for PASS status - age category is always calculated and provided when verification passes
- For FAIL status: Present when `failureReason` is `age-criteria-not-met` and age data is available (both `age.low` and `age.high` are present)
:::tip What you need to know
- `ageCategory` is always present for PASS status - you can safely use it for access control decisions
- Use `ageCategory` for access control decisions rather than raw `age` values
- For FAIL status, don't use `ageCategory` for access control decisions, even if it's present
:::
#### When `age` and `ageCategory` are absent
**Conditions:**
- The verification failed with `failureReason` of `max-attempts-exceeded` - No age determination was made
- The verification failed with `failureReason` of `fraudulent-activity-detected` - Age data is excluded for security reasons
- The verification method didn't provide age information
:::tip What you need to know
- Don't assume age information is always available, handle missing age fields gracefully
- Don't try to infer age from other fields when `age` is absent
- For FAIL status without age fields, rely on `failureReason` to understand what went wrong
:::
### Date of birth field
**Format:** `YYYY-MM-DD` (ISO 8601 date format)
**When present:**
The `dob` field is only included when the verification method provides a verified date of birth. It's not included for methods that only provide age estimates or age ranges. Given the various combinations of verification methods and jurisdictions, you shouldn't perform validation or have expectations about when `dob` must be present. The field is provided when it's available from the verification method, but its presence can't be guaranteed across all scenarios.
**Methods that can provide `dob`:**
- `id-document` - When the ID document contains readable date of birth information
- `credit-card` - When the credit card verification provides date of birth (not all credit card verifications include DOB)
- `social-security-number` - When the SSN verification provides date of birth
- `privy` - Always provided (Indonesian identity verification)
- `korean-real-name` - Always provided (Korean real-name verification via Inicis)
- `age-attestation` - When the attestation includes birth date information
- `singpass` - Always provided (Singapore national identity verification)
**Methods that NEVER provide `dob`:**
- `age-estimation-scan` - Only provides age ranges, not verified DOB
- `email-estimation` - Only provides age estimates, not verified DOB
**Status conditions:**
- Present for PASS status when the method provides it
- Present for FAIL status when the method provided it before determining the user didn't meet age criteria
:::tip What you need to know
- Don't assume `dob` is always present, even for PASS status
- If `dob` is present, validate that it's in `YYYY-MM-DD` format
- You can use `dob` for additional validation or logging, but don't rely on it as your primary way to determine age
:::
### Method field
**When present:**
- Always present for PASS status
- Present for FAIL status when `failureReason` is `age-criteria-not-met`
**Possible values:**
- `id-document` - Government-issued ID document verification (can include DOB)
- `credit-card` - Credit card verification (can include DOB)
- `age-estimation-scan` - Facial age estimation scan (no DOB, returns age range)
- `social-security-number` - SSN-based verification (can include DOB)
- `email-estimation` - Email-based age estimation (no DOB, only age estimates)
- `privy` - Indonesian identity verification (always includes DOB)
- `korean-real-name` - Korean real-name verification via Inicis (always includes DOB)
- `age-attestation` - Age attestation via k-ID (can include DOB)
- `singpass` - Singapore national identity verification (always includes DOB)
- `connect-id` - Australian ConnectID verification
:::tip What you need to know
- You can use `method` for analytics or logging purposes
- Don't use `method` for access control decisions
:::
### Failure reason values
Common `failureReason` values:
- `age-criteria-not-met` - User's verified age doesn't meet the required threshold
- `max-attempts-exceeded` - User has exhausted all available verification attempts
- `fraudulent-activity-detected` - System detected suspicious or fraudulent behavior
:::tip What you need to know
- Handle unknown `failureReason` values gracefully, don't break if you see something unexpected
- Don't make access decisions based solely on `failureReason`, always consider the `status` field
- It's a good idea to log `failureReason` for debugging and analytics purposes
:::
### Complete field matrix
#### Webhook events
| Field | PASS | FAIL |
|-------|------|------|
| `id` | always | always |
| `status` | always | always |
| `method` | always | sometimes¹ |
| `age` | always | sometimes² |
| `ageCategory` | always | sometimes² |
| `dob` | sometimes³ | sometimes³ |
| `failureReason` | never | always |
¹ Present unless `failureReason` is `max-attempts-exceeded` or `fraudulent-activity-detected`
² Present only if both `age.low` and `age.high` are available AND `failureReason` is `age-criteria-not-met`
³ Always included if available from the verification method
#### API endpoint
| Field | PENDING | IN_PROGRESS | PASS | FAIL |
|-------|---------|-------------|------|------|
| `id` | always | always | always | always |
| `status` | always | always | always | always |
| `method` | never | never | sometimes² | sometimes¹ |
| `age` | never | never | always | sometimes³ |
| `ageCategory` | never | never | always | sometimes⁴ |
| `dob` | never | never | sometimes⁵ | sometimes⁵ |
| `failureReason` | never | never | never | always |
¹ Present unless `failureReason` is `max-attempts-exceeded` or `fraudulent-activity-detected`
² Present if available from the verification method
³ Present only if both `age.low` and `age.high` are available AND `failureReason` is `age-criteria-not-met`
⁴ Present only when age category was successfully calculated from the age and jurisdiction (for FAIL status)
⁵ Only included if `includeDob=true` query parameter is set AND available from the verification method
### Example payloads
#### Webhook event examples
**`PASS` status with age information**
```json
{
"eventType": "Verification.Result",
"data": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"status": "PASS",
"method": "id-document",
"ageCategory": "adult",
"age": {
"low": 25,
"high": 25
},
"dob": "1998-05-15"
}
}
```
**`FAIL` status with age information**
```json
{
"eventType": "Verification.Result",
"data": {
"id": "123e4567-e89b-12d3-a456-426614174001",
"status": "FAIL",
"method": "age-estimation-scan",
"failureReason": "age-criteria-not-met",
"age": {
"low": 16,
"high": 17
},
"ageCategory": "digital-minor"
}
}
```
**`FAIL` status without age information**
```json
{
"eventType": "Verification.Result",
"data": {
"id": "123e4567-e89b-12d3-a456-426614174002",
"status": "FAIL",
"failureReason": "max-attempts-exceeded"
}
}
```
#### API endpoint response examples
**`PENDING` status**
```json
{
"id": "123e4567-e89b-12d3-a456-426614174003",
"status": "PENDING"
}
```
**`IN_PROGRESS` status**
```json
{
"id": "123e4567-e89b-12d3-a456-426614174004",
"status": "IN_PROGRESS"
}
```
**`PASS` status with age information (without DOB)**
```json
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"status": "PASS",
"method": "id-document",
"ageCategory": "adult",
"age": {
"low": 25,
"high": 25
}
}
```
**`PASS` status with DOB (`includeDob` = `true`)**
```json
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"status": "PASS",
"method": "id-document",
"ageCategory": "adult",
"age": {
"low": 25,
"high": 25
},
"dob": "1998-05-15"
}
```
**`FAIL` status with age information**
```json
{
"id": "123e4567-e89b-12d3-a456-426614174001",
"status": "FAIL",
"method": "age-estimation-scan",
"failureReason": "age-criteria-not-met",
"age": {
"low": 16,
"high": 17
},
"ageCategory": "digital-minor"
}
```
**`FAIL` status without age information**
```json
{
"id": "123e4567-e89b-12d3-a456-426614174002",
"status": "FAIL",
"failureReason": "max-attempts-exceeded"
}
```
### Implementation checklist
#### Webhook events
- [ ] Handle both status types: PASS and FAIL (webhooks never send PENDING or IN_PROGRESS)
- [ ] Check for presence of `age` object before accessing `age.low` or `age.high`
- [ ] Use `ageCategory` for access control when status is PASS and `ageCategory` is present
- [ ] Don't use `ageCategory` for access control when status is FAIL, even if it's present
- [ ] Handle cases where `method` is absent (system-level failures)
- [ ] Handle cases where `age` and `ageCategory` are absent (certain failure scenarios)
- [ ] Validate `dob` format (YYYY-MM-DD) if present
- [ ] Log `failureReason` for debugging
- [ ] Don't grant access for FAIL status
- [ ] Handle unknown `failureReason` values gracefully
#### API endpoint
- [ ] Handle all status types: PENDING, IN_PROGRESS, PASS, and FAIL
- [ ] For PENDING and IN_PROGRESS statuses, only expect `id` and `status` fields
- [ ] Set `includeDob=true` query parameter if DOB data is needed (see [`GET /age-verification/get-status`](/api/endpoints/get-age-verification-status))
- [ ] Check for presence of `age` object before accessing `age.low` or `age.high`
- [ ] Use `ageCategory` for access control when status is PASS and `ageCategory` is present
- [ ] Don't use `ageCategory` for access control when status is FAIL, even if present
- [ ] Handle cases where `method` is absent (system-level failures)
- [ ] Handle cases where `age` and `ageCategory` are absent (certain failure scenarios)
- [ ] Validate `dob` format (YYYY-MM-DD) if present
- [ ] Log `failureReason` for debugging
- [ ] Don't grant access for FAIL status
- [ ] Handle unknown `failureReason` values gracefully
- [ ] Implement appropriate polling strategy for PENDING and IN_PROGRESS statuses
### Implementation notes
:::note AgeCategory behavior
The `ageCategory` field is always included for PASS status in webhook events. For FAIL status, `ageCategory` might be included when `failureReason` is `age-criteria-not-met` and age data is available. However, you should never use `ageCategory` for access control decisions when the status is FAIL, regardless of whether it's present. The API endpoint follows the same pattern - `ageCategory` can be present for FAIL status when age data is available, but shouldn't be used for access control decisions.
:::
### Version history
- **2026-01-07**: `ageCategory` field is now included in FAIL status webhook events when `failureReason` is `age-criteria-not-met` and age data is available.
- **2025-12-04**: Initial API contract document
---
// File: events/webhooks/event-types/verification-revoke
# `Verification.Revoke`
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
Emitted when one or more previously passed verifications have been revoked, for example due to fraudulent activity or a provider-reported error. The `data.verifications` array always contains at least one item and can contain multiple items when revocations are processed in bulk.
When you receive this event, you should treat the referenced verifications as no longer valid and take appropriate action (for example, re-triggering verification for affected users).
## Fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `eventType` | string | yes | Always `"Verification.Revoke"` |
| `data` | object | yes | Revocation data |
| `data.verifications` | array | yes | List of revoked verifications (minimum 1 item) |
| `data.verifications[].id` | string (UUID) | yes | The verification ID being revoked |
| `data.verifications[].reason` | string | yes | The reason for revocation: `fraudulent-activity-detected` or `provider-reported-error` |
## Example
```json
{
"eventType": "Verification.Revoke",
"data": {
"verifications": [
{
"id": "4e57301e-a4d1-498f-ac3f-f3d4de19abf6",
"reason": "fraudulent-activity-detected"
}
]
}
}
```
```json
{
"eventType": "Verification.Revoke",
"data": {
"verifications": [
{
"id": "4e57301e-a4d1-498f-ac3f-f3d4de19abf6",
"reason": "fraudulent-activity-detected"
},
{
"id": "b2c8e91f-7a23-4d5c-8f1e-2a9b3c4d5e6f",
"reason": "provider-reported-error"
}
]
}
}
```
---
// File: events/webhooks/overview
# Webhooks overview
Webhooks notify your servers about important events in k-ID, such as challenge status changes and verification results. Configure webhook URLs and secrets in Compliance Studio under Developer Settings for each Product.
Payloads include an `eventType` and a `data` object. See Event types for payload schemas.
## Event types
| Event Type | Description |
|------------|-------------|
| [`Challenge.StateChange`](/events/webhooks/event-types/challenge-statechange) | Emitted when a parental consent challenge changes state |
| [`Verification.Result`](/events/webhooks/event-types/verification-result) | Emitted with the result of a verification attempt |
| [`Verification.Revoke`](/events/webhooks/event-types/verification-revoke) | Emitted when a previously passed verification has been revoked |
| [`Account.Delete`](/events/webhooks/event-types/account-delete) | Emitted when an account is deleted |
| [`AgeAssurance.Result`](/events/webhooks/event-types/ageassurance-result) | Emitted with the result of an Age Assurance evaluation (deprecated, replaced by `Verification.Result`) |
| [`Session.ChangePermissions`](/events/webhooks/event-types/session-changepermissions) | Emitted when session permissions are modified by a parent |
| [`Session.Delete`](/events/webhooks/event-types/session-delete) | Emitted when a session is deleted |
| [`Test`](/events/webhooks/event-types/test) | Used to verify that the webhook is working correctly |
## Signature validation
Validate webhook requests with the configured webhook secret.
### Headers
Headers sent with each request:
- `X-Signature-Timestamp`: UNIX epoch seconds
- `X-Signature-Hmac-Sha256`: HMAC SHA-256 of (timestamp + raw body), using the webhook secret as key, hex-encoded (lowercase)
### Expected behavior
- Return `200` for valid signatures
- Return `401` for invalid signatures
For implementation details, see [Validating Webhook Requests](/webhooks#validating-webhook-requests).
## Delivery, retries, and recovery
### Delivery guarantees
k-ID delivers webhook events with an at-least-once guarantee. Your endpoint can receive the same event more than once, so design your handler to be idempotent. Use the `data.id` field (the verification or challenge ID) to deduplicate events you have already processed.
### Retry policy
When your endpoint returns a non-`200` status code or the request times out, k-ID retries delivery **2 times**:
| Attempt | Delay after previous attempt |
|---------|------------------------------|
| First retry | 5 seconds |
| Second retry | 10 seconds |
After the initial attempt plus two retries (three total attempts), k-ID stops trying to deliver that event.
### Handling missed webhooks
If your server was down or a webhook wasn't delivered successfully, recover by polling the relevant status endpoint from your server by using the ID you saved when you started the flow:
- **Verifications**: [`GET /age-verification/get-status`](/api/endpoints/get-age-verification-status) with the verification ID
- **Challenges**: [`GET /challenge/get-status`](/api/endpoints/get-challenge-status) with the challenge ID
:::tip Recommended pattern
Always save the verification or challenge ID that you receive when you initiate a flow. If your webhook handler doesn't receive a result within the expected time frame, call the corresponding `get-status` endpoint to retrieve the current state. This redirect-then-poll pattern ensures you never miss a result, even if all webhook deliveries fail.
:::
### Handling duplicate events
Webhook events are idempotent. The same event can be delivered more than once, for example when a network issue causes an ambiguous delivery status. Always check whether you have already processed an event before acting on it. A simple approach is to track processed event IDs (the `data.id` field) and skip duplicates.
---
// File: get-started/overview
# Welcome to the Developer Hub
:::warning Legal disclaimer
The information contained within this documentation is not intended to be a substitute for legal counsel and does not constitute legal advice. Please consult with your legal counsel for any questions regarding your compliance strategy.
:::
k-ID enables games and applications to be compliant world-wide by providing jurisdiction-specific data from a comprehensive database of regulations to drive game behavior in 200+ markets. There are five main components in k-ID:
- **KnowledgeKit** - An always-up-to-date compliance database containing privacy & safety laws across 240+ jurisdictions (195+ countries, 22,000+ sources)
- **[Compliance Development Kit (CDK)](/cdk/overview)** - Enterprise-grade compliance framework that automatically manages regulatory logic and determines feature accessibility based on age, jurisdiction, and parental consent
- **[AgeKit+](/agekit-plus/overview)** - Privacy-preserving age verification solution that allows users to prove their age without revealing personal information
- **[Family Connect](https://family.k-id.com)** - Where parents can grant consent and manage permissions for kids and teens
- **[Compliance Studio](https://portal.k-id.com)** - Where you configure and customize your product's compliance settings, permissions, and policies
:::tip Integrate with AI coding agents
k-ID publishes official [Agent Skills](https://agentskills.io/specification), small instruction packs for supported AI coding tools. Read [Integrate with AI coding agents](/get-started/agent-skills) for installation paths and verification checklists.
:::
## How the Compliance Development Kit works
The k-ID Compliance Development Kit (CDK) is an enterprise-grade compliance framework that automatically manages regulatory logic and compliance settings. It intelligently determines which features and content are accessible to players based on their age, jurisdiction, and parental consent status.
CDK provides two integration options:
**[Widgets](/cdk/embedded-flow)** - Pre-built widgets that handle the complete compliance flow in iframes, including age gate, Verifiable Parental Consent (VPC), data notices, and permission management.
**[Custom UX workflows](/cdk/custom-workflow)** - Build completely custom compliance experiences with the k-ID API while still leveraging CDK's compliance logic.
CDK maintains all compliance logic and settings to determine:
- Which features and content are allowed to players
- Which players need parental consent
- What permissions can be granted based on age, jurisdiction, and consent
- What data notices must be displayed
All compliance policies are configured through the [Compliance Studio](/compliance-studio/creating-product), where you define and manage your product's regulatory requirements.
For more information, see [CDK](/cdk/overview).
## How AgeKit+ works
AgeKit+ is k-ID's age verification solution that provides privacy-preserving age verification capabilities. It allows users to prove their age without revealing personal information by using multiple verification methods.
AgeKit+ provides two integration approaches:
**[Waterfall flow](/agekit-plus/waterfall-flow)** - AgeKit+ acts as a single-point orchestrator for age checks, automatically cascading through a waterfall of verification providers to confirm a user's age. One API call attempts configured methods in sequence until the user's age is verified or all options are exhausted, maximizing the chances of successful verification.
**[Single method flow](/agekit-plus/single-method-flow)** - Use method-specific endpoints to choose verification methods dynamically through API calls, giving you full control over which methods are presented and how users select them.
AgeKit+ supports multiple verification methods including facial age estimation, ID document verification, AgeKey, credit card verification, email age estimation, and regional methods such as Singpass (Singapore) and ConnectID (Australia).
For more information, see [AgeKit+](/agekit-plus/overview).
## Configuration and customization
k-ID is highly customizable for the needs of the game in the markets it serves. Customization of policies, permissions, trusted adult disclosures, and white-labeled parent experiences is done in the [k-ID Compliance Studio](/compliance-studio/creating-product).
:::tip
Before you begin, make sure you have created a product in the [Compliance Studio](/compliance-studio/creating-product) and generated your API key. This is required for all k-ID integrations.
:::
---
// File: get-started/choose-integration
# Choose integration
k-ID provides two main approaches for integrating compliance into your game or application:
1. **Compliance Development Kit (CDK)** - Enterprise-grade compliance framework that automatically manages regulatory logic and determines feature accessibility based on age, jurisdiction, and parental consent
2. **AgeKit+** - Privacy-preserving age verification solution for verifying user age without revealing personal information
## Compliance Development Kit (CDK)
The CDK is an enterprise-grade compliance framework that automatically manages regulatory logic and compliance settings. It intelligently determines which features and content are accessible to players based on their age, jurisdiction, and parental consent status. All compliance policies are configured through the Compliance Studio, enabling you to define and manage your product's regulatory requirements.
The CDK uses global compliance information from k-ID's Regulatory Hub to automatically determine:
- Which features and content can be offered to different age groups
- What compliance requirements apply in different jurisdictions
- When parental consent is required
- What permissions can be granted or restricted
- What data notices must be displayed
### When to use CDK
- **Age gate and VPC**: You need to implement age gates and Verifiable Parental Consent (VPC) flows
- **Permission management**: You need to manage which features players can access based on age and parental consent
- **Data notices**: You need to display jurisdiction-appropriate data notices and collect consent
- **Session management**: You need to manage player sessions with permissions and age status
- **Compliance logic**: You want k-ID to maintain all compliance logic and settings automatically
### Integration options
CDK provides two ways to integrate:
- **Widgets**: Pre-built widgets that handle the complete compliance flow. The widget is a hosted URL you render in your app: web iframe / pop-up / redirect, mobile embedded browser, in-game browser surface, or QR-to-mobile handoff on consoles. See [Embedded flow](/cdk/embedded-flow) and the [Mobile apps quick start](/get-started/quickstart-guides/mobile-apps) for the available surfaces.
- **Custom UX workflows**: Build custom workflows using the k-ID API while leveraging CDK's compliance logic
### Key features
- Maintains all compliance logic and settings automatically
- Determines which features/content are allowed to players
- Identifies which players need parental consent
- Manages permissions based on age, jurisdiction, and consent
- Handles age gates, VPC, sessions, permissions, and data notices
- Uses global compliance data from k-ID's Regulatory Hub
- All configuration in Compliance Studio
For more information, see [Compliance Development Kit (CDK)](/cdk/overview).
## AgeKit+
AgeKit+ is k-ID's age verification solution that provides privacy-preserving age verification capabilities. It allows users to prove their age without revealing personal information with multiple verification methods.
### When to use AgeKit+
- **Age verification only**: You need to verify user age but don't need full VPC or permission management
- **Standalone verification**: You want to verify age independently of other compliance flows
- **Privacy-focused**: You need privacy-preserving age verification without collecting personal data
- **Multiple verification methods**: You want to offer users multiple ways to verify their age
### Integration options
AgeKit+ provides two ways to integrate:
- **Waterfall flow**: Standard approach where verification methods are determined by your product's configuration in Compliance Studio
- **Single method flow**: Use method-specific endpoints to choose verification methods dynamically through API calls
### Key features
- Privacy-preserving age verification
- Multiple verification methods (facial scanning, ID verification, AgeKey)
- Jurisdiction-aware compliance
- Flexible integration (waterfall flow or single method flow)
- No personal information required
For more information, see [AgeKit+](/agekit-plus/overview).
## Choosing the right approach
Both CDK and AgeKit+ use the same underlying k-ID infrastructure and global compliance data. The choice depends on your specific needs:
| Factor | CDK | AgeKit+ |
|--------|-----|---------|
| **Use case** | Age gate, VPC, permissions, sessions | Age verification only |
| **Compliance logic** | Maintains all logic automatically | Focused on age verification |
| **Parental consent** | Full VPC support | Not included |
| **Permission management** | Complete session and permission management | Not included |
| **Integration complexity** | More comprehensive | Simpler, focused |
| **Configuration** | Compliance Studio | Compliance Studio |
Many developers use both CDK and AgeKit+ in the same application - for example, using AgeKit+ for initial age verification and CDK for ongoing compliance management including VPC, permissions, and sessions.
---
// File: get-started/agent-skills
# Integrate with AI coding agents
k-ID publishes an official bundle of [Agent Skills](https://agentskills.io/specification): small, composable instruction packs that teach an AI coding agent how to integrate k-ID correctly across every jurisdictional regime k-ID supports. They cover COPPA (US), GDPR-Kids (EU), UK AADC, the UK Online Safety Act, Brazil ECA Digital, Australia Online Safety / social media minimum age, and other regional requirements. They work with 35+ AI tools, including Claude Code, Cursor, OpenAI Codex, GitHub Copilot, Gemini CLI, and any other agent that follows the open Agent Skills specification.
The skills live at **[`github.com/kidentify/skills`](https://github.com/kidentify/skills)**.
## Two integration shapes
The skills support both k-ID integration shapes. The router detects
which shape applies from the user's request and loads only the
skills that shape needs.
- **Shape A: full CDK integration.** Age gate, then session, then
consent or verification or threshold, then permissions. For games,
social platforms, and multi-feature apps with persistent per-user
state and multiple gated features. Uses 5–7 of the skills.
- **Shape B: standalone AgeKit+.** A single call to
[`/age-verification/perform-access-age-verification`](/api/endpoints/perform-access-age-verification),
an iframe for the user to complete verification, and a webhook or
polled result. No age gate, no session, no permissions. For
18+ sites (UK OSA), age-restricted downloads, Australia
social-media minimum-age checks, or any single age-proof decision.
Uses 2–3 of the skills.
## Two UI approaches in custom integrations
Within Shape A, k-ID supports two ways of rendering the age gate and
parental-consent flow, and the skills cover both. See
[Choose integration](/get-started/choose-integration) for the
canonical comparison. The short version:
- **Custom UX workflows** (the default). Build your own age-gate and
consent UI and call [`/age-gate/check`](/api/endpoints/check-age-gate),
[`/challenge/send-email`](/api/endpoints/send-email), and the rest
directly. Produces the best-looking, most brand-integrated
experience and works on every platform (web, Unity WebGL, consoles,
native desktop). Recommended for production integrations.
- **Widgets** (fast-path fallback). Pre-built k-ID iframes for the
age gate, the end-to-end flow (age gate + parental consent + data
notices + permissions + parental preferences), manage-permissions,
and data notices. They handle jurisdiction-appropriate age
collection and initiate parental-consent challenges automatically.
Use when the integration must be small, simple, and fast to ship.
The `k-id-age-gate` and `k-id-consent-and-challenges` skills both
document the custom path (Pattern A) and the widget path (Pattern B).
Just tell the agent what you want, and it picks the right one.
## What the skills give you
Eight focused skills that compose:
| Skill | What it teaches the agent | Shape |
|---|---|---|
| `k-id-integration` | Router that detects shape and picks the right sibling skills. | Both |
| `k-id-age-gate` | Builds the age gate with either a fully custom slider calling `/age-gate/check` (Pattern A: the default, best-looking and most brand-integrated) or the k-ID age-gate widget (Pattern B: fast-path fallback for small, simple integrations). Entry point to every Shape A flow. | A |
| `k-id-consent-and-challenges` | GUARDIAN-managed parental consent for minors. Use a custom consent screen with QR + OTP + email + direct link and top-level polling outside the modal (Pattern A: the default) or the end-to-end / manage-permissions widget (Pattern B: fast-path fallback). | A |
| `k-id-age-verification` | Age verification and assurance: standalone AgeKit+ (Pattern 1), unverified-adult in a session (Pattern 2), and per-permission `verifiedAgeThreshold` flows (Pattern 3, UK OSA 18+, Brazil ECA Digital, Australia social media minimum age). | Both |
| `k-id-sessions-and-permissions` | Session handling, permission-gated UI controls, `/session/upgrade` shape, and `verifiedAgeThreshold` handling. | A |
| `k-id-webhooks` | HMAC-SHA256 signature verify against the raw body, idempotency, event handlers. | Both |
| `k-id-server-trust-boundary` | API key placement, server proxy, pre-flight checks. | Both |
| `k-id-mobile-native` | iOS / Android / Unity platform age signals and in-app browsing. | A (usually) |
When the agent activates `k-id-integration`, it reads the user's task and pulls in the right combination of feature and cross-cutting skills, so you don't install them individually.
## Before you install
- You need a k-ID product in [Compliance Studio](https://portal.k-id.com) with an API key and webhook secret. See the [API authentication guide](/api/authentication).
- The skills reference API shapes from this documentation site. If you've blocked outbound network access for your AI agent, allow `docs.k-id.com`.
## Install by tool
All installation paths below get the same eight skills into your agent's context. Pick the row that matches your tool.
### Claude Code (fastest)
Inside Claude Code, run:
```text
/plugin marketplace add github.com/kidentify/skills
/plugin install k-id-skills@kidentify
```
The plugin registers all eight skills at once. Confirm with `/plugin list`. The marketplace URL must be added as a **git-style** reference (`github.com/...` or `https://github.com/...`); if you see a schema error on `plugins[].source`, update the skills repository `.claude-plugin/marketplace.json` so each plugin `source` is a relative path starting with `./` (see [Claude Code plugin marketplaces](https://code.claude.com/docs/en/plugin-marketplaces#plugin-sources)).
### Cursor
Clone the `skills/` directory from the repository into your project's `.agents/skills/` directory (or use `.cursor/skills/`). Cursor discovers project skills from those paths automatically; for user-wide skills, use `~/.agents/skills/` or `~/.cursor/skills/`. See [Cursor Agent Skills](https://cursor.com/docs/context/skills).
```bash
mkdir -p .agents/skills
git clone --depth 1 https://github.com/kidentify/skills.git /tmp/k-id-skills
cp -R /tmp/k-id-skills/skills/* .agents/skills/
rm -rf /tmp/k-id-skills
```
Reload Cursor and open a chat to confirm the skills appear in the agent's available tools. Cursor picks up new skills without a restart in most cases.
### OpenAI Codex / Codex CLI
Codex discovers skills under `.agents/skills/` from your working directory up to the repository root, and user-wide skills under `~/.agents/skills/`. Those are the paths OpenAI documents for the Codex CLI and IDE extension; `~/.codex/skills/` can still be read for backward compatibility but isn't the primary location. For per-project setup, use the same `git clone` + `cp` recipe as Cursor. For global:
```bash
mkdir -p ~/.agents/skills
git clone --depth 1 https://github.com/kidentify/skills.git /tmp/k-id-skills
cp -R /tmp/k-id-skills/skills/* ~/.agents/skills/
rm -rf /tmp/k-id-skills
```
See [Codex Agent Skills](https://developers.openai.com/codex/skills/).
### GitHub Copilot / Visual Studio Code
GitHub Copilot in Visual Studio Code loads **project** skills from `.github/skills/`, `.claude/skills/`, or `.agents/skills/` (see [Use Agent Skills in Visual Studio Code](https://code.visualstudio.com/docs/copilot/customization/agent-skills)). **Personal** skills can live under `~/.copilot/skills/`, `~/.claude/skills/`, or `~/.agents/skills/`. Copy the repository `skills/` tree into the project path you prefer; `.github/skills/` matches the GitHub-documented layout for repo-scoped skills:
```bash
mkdir -p .github/skills
git clone --depth 1 https://github.com/kidentify/skills.git /tmp/k-id-skills
cp -R /tmp/k-id-skills/skills/* .github/skills/
rm -rf /tmp/k-id-skills
```
Use `/skills` in Chat to confirm they appear. Optional `chat.agentSkillsLocations` can add more roots if your team uses a non-default folder.
### Gemini CLI
Gemini CLI discovers **user** skills in `~/.gemini/skills/` or `~/.agents/skills/` (the latter takes precedence if both exist at the same tier), and **workspace** skills in `.gemini/skills/` or `.agents/skills/`. See [Gemini CLI Agent Skills](https://geminicli.com/docs/cli/skills/). Global install:
```bash
mkdir -p ~/.gemini/skills
git clone --depth 1 https://github.com/kidentify/skills.git /tmp/k-id-skills
cp -R /tmp/k-id-skills/skills/* ~/.gemini/skills/
rm -rf /tmp/k-id-skills
```
For a repo-local install shared with the team, use `.agents/skills/` or `.gemini/skills/` the same way as the Cursor recipe. You can also run `gemini skills install https://github.com/kidentify/skills.git` (see Gemini CLI skills management).
### Other tools and air-gapped environments
Any AI agent that follows the [Agent Skills specification](https://agentskills.io/specification) can consume these skills. The [Agent Skills client showcase](https://agentskills.io/clients) lists 35+ compatible tools. Each tool documents where it reads skills from. Once you know the directory, the copy recipe is the same.
If outbound internet is blocked, download the ZIP via the "Code" button on GitHub, transfer it to your environment, and extract the `skills/` directory into the tool skills path for your agent.
:::info Last verified
These install paths were checked against Cursor, OpenAI Codex, Visual Studio Code with GitHub Copilot, and Gemini CLI documentation on 2026-05-15. If a tool changes its skills directory, follow that product documentation and the [Agent Skills client showcase](https://agentskills.io/clients).
:::
## How to use the skills
Once installed, just describe your task to the agent in natural language:
- "Integrate k-ID into a Next.js app."
- "Add parental consent with QR, OTP, and email."
- "Verify the webhook handler against the Compliance Studio Test event."
- "On iOS, read the declared age range before the age gate."
The router skill (`k-id-integration`) activates first, reads the request, and hands off to the right specialized skill. You don't need to remember skill names.
For deeper questions about the patterns the skills encode, read the relevant concept page:
- [Sessions](/concepts/access-features-consent/sessions)
- [Permissions](/concepts/access-features-consent/permissions)
- [Challenges](/concepts/access-features-consent/challenges)
- [Verifiable Parental Consent](/concepts/access-features-consent/vpc)
- [Age gate](/concepts/access-features-consent/age-gate)
- [Age assurance](/concepts/age-assurance)
- [Age signals](/concepts/age-signals)
## Design principles behind these skills
- **Doc-first.** API request and response shapes come from this documentation site. Skills encode integration patterns and known pitfalls (not endpoint reference data), so a shape change on the k-ID side never leaves a skill out of sync.
- **Invariants inline.** Each `SKILL.md` includes a "Gotchas" section with the rules that prevent real, shipped bugs, each paired with a one-line "why."
- **Calibrated prescriptiveness.** Fragile operations (API body shapes, signature verification) are specified exactly. UI adaptation and framework choice are left to you.
- **Validate at the end.** Every feature skill ends with a verification checklist so the agent confirms the integration is actually working before declaring done.
## Related
- [Agent Skills specification](https://agentskills.io/specification)
- [Agent Skills client showcase](https://agentskills.io/clients): 35+ compatible AI tools
- [`github.com/kidentify/skills`](https://github.com/kidentify/skills): the repository
- [API authentication](/api/authentication)
- [Testing guide](/concepts/testing)
---
// File: get-started/quickstart-guides/mobile-apps
# Mobile apps
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
This guide covers best practices for integrating k-ID **age verification** into mobile applications. On the web, k-ID interfaces are commonly embedded in iframes, but mobile apps need different approaches to display verification URLs effectively.
## Overview
The display methods in this guide apply to any hosted k-ID URL, including the [widget URLs](/cdk/embedded-flow). The examples focus on the hosted URLs that present a jurisdiction-aware verification interface (AgeKeys, facial age estimation, ID verification, and other methods). Two kinds are covered:
- **Age verification URL**: the `url` returned by the [`/age-verification/perform-access-age-verification`](/api/endpoints/perform-access-age-verification) endpoint (AgeKit+ standalone verification)
- **Age assurance challenge URL**: the `challenge.url` of a `CHALLENGE_AGE_GATE_AGE_ASSURANCE` challenge returned by [`/age-gate/check`](/api/endpoints/check-age-gate) (when automatic age assurance is enabled) or a `CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE` challenge returned by [`/session/upgrade`](/api/endpoints/upgrade-session)
The examples in this guide use the [`/age-verification/perform-access-age-verification`](/api/endpoints/perform-access-age-verification) endpoint, but the same display methods and result handling apply to any of these URLs.
When embedding one of these URLs in a mobile application, you have several options for displaying it, each with different capabilities and trade-offs. The key considerations are:
- **AgeKeys support**: Whether users can create and use AgeKeys (FIDO-based passkeys) for future verifications
- **Result communication**: How verification results are delivered back to your app
- **User experience**: The level of integration and native feel
- **Device orientation**: Verification works best in portrait. In-app browser surfaces inherit your app's orientation, so if your app is locked to landscape, use the default external browser instead to let users rotate to portrait. See [Device orientation](#device-orientation)
:::tip Age gate and end-to-end widgets on mobile
The **age gate** and **end-to-end** widgets are fully supported on mobile and are displayed with the same methods described in this guide. For the age gate and consent steps, though, building the UX natively with the [custom workflow](/cdk/custom-workflow) and the [CDK UX guidelines](/cdk/ux-guidelines) typically delivers the most seamless, brand-integrated player experience, and the widget's mobile UX is being continuously optimized.
:::
## Mobile implementation methods
### Android options
Android provides three primary methods for displaying verification URLs:
- **[Custom Tabs](https://developer.chrome.com/docs/android/custom-tabs/overview/)** ⭐ **Recommended** - Opens the verification URL in a customized Chrome browser tab that maintains your app's branding. This provides full browser capabilities while keeping users within your app's context. Custom Tabs share cookies and authentication state with Chrome, enabling seamless experiences.
- **[WebView](https://developer.android.com/reference/android/webkit/WebView)** - Embeds web content directly within your app using Android's native WebView component. While simple to implement, WebView has limited support for modern web standards and can't access certain browser features.
:::warning Not recommended
WebView doesn't support WebAuthn, which is required for AgeKeys. Users won't be able to create or use AgeKeys when a verification URL is embedded using WebView. For the best user experience with full AgeKeys support, use Custom Tabs instead.
:::
- **[Trusted Web Activity (TWA)](https://developer.android.com/develop/ui/views/layout/webapps/trusted-web-activities)** - Displays web content in full-screen mode, primarily designed for Progressive Web Apps. TWAs require establishing a digital asset link between your app and the k-ID domain. When digital asset links aren't detected, TWA automatically falls back to Custom Tabs.
:::warning Not recommended
Trusted Web Activities aren't supported for embedding verification URLs. Digital asset links would need to be configured on k-ID's domains, which isn't available. Since TWA falls back to Custom Tabs in this case, use Custom Tabs directly for a simpler implementation.
:::
### iOS options
iOS provides three primary methods for displaying verification URLs:
- **[ASWebAuthenticationSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession)** ⭐ **Recommended** - Designed specifically for secure authentication flows, this method presents web content in a system-managed browser view. It shares cookies with Safari and provides access to modern web features such as WebAuthn, making it ideal for verification flows.
- **[SFSafariViewController](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller)** - Presents web content in a Safari-like interface that shares cookies and authentication state with Safari. This provides a familiar browsing experience while maintaining app context.
- **[WKWebView](https://developer.apple.com/documentation/webkit/wkwebview)** - Apple's modern web view component that embeds web content within your app. Similar to Android's WebView, WKWebView has limitations with certain web standards and can't access all browser features.
:::warning Not recommended
WKWebView doesn't support WebAuthn, which is required for AgeKeys. Users won't be able to create or use AgeKeys when a verification URL is embedded using WKWebView. For the best user experience with full AgeKeys support, use ASWebAuthenticationSession instead.
:::
### Default browser (Android and iOS)
The **default external browser** works on both Android and iOS. Instead of presenting the verification URL in an in-app browser surface, you launch the user's default browser as a separate app ([`Intent.ACTION_VIEW`](https://developer.android.com/reference/android/content/Intent#ACTION_VIEW) on Android, [`UIApplication.open`](https://developer.apple.com/documentation/uikit/uiapplication/1648685-open) on iOS).
- **Full AgeKeys support** - The browser provides WebAuthn, so users can create and use AgeKeys.
- **Independent orientation** - Because the browser is a separate app, it manages its own orientation. This makes it the recommended method for apps with a **locked landscape orientation**, since the user can rotate to portrait for the best verification experience. See [Device orientation](#device-orientation).
- **Callback URL required** - The user leaves your app, so results are delivered through the `redirectUrl` callback, which also returns focus to your app when the flow completes. DOM messages aren't available.
## AgeKeys support limitations
AgeKeys are reusable, anonymous age-proof credentials based on FIDO and WebAuthn standards. They allow users to verify their age once and reuse that verification across different services without revealing personal information.
:::warning AgeKeys limitation
AgeKeys require WebAuthn support, which isn't available in [Android WebView](https://developer.android.com/reference/android/webkit/WebView) or [iOS WKWebView](https://developer.apple.com/documentation/webkit/wkwebview). If you embed a verification URL by using these components, users won't see AgeKeys as an option during verification, and they can't create AgeKeys after successful verification.
:::
To enable AgeKeys for your users, you must use one of these methods:
- **Android**: [Custom Tabs](https://developer.chrome.com/docs/android/custom-tabs/overview/)
- **iOS**: [ASWebAuthenticationSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) or [SFSafariViewController](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller)
## Device orientation
Age verification works best in **portrait** orientation. Methods such as facial age estimation and ID document capture are easier to complete when the device is upright, and the verification interface is laid out for portrait.
In-app browser surfaces ([Custom Tabs](https://developer.chrome.com/docs/android/custom-tabs/overview/), [ASWebAuthenticationSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession), [SFSafariViewController](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller), and WebView/WKWebView) inherit your app's orientation constraints. If your game or app locks to **landscape**, the verification interface is forced into landscape too, which degrades the experience.
:::tip Recommendation for landscape-locked apps
If your app has a locked landscape orientation, open the verification URL in the device's **default external browser** instead of an in-app browser surface. The external browser runs as a separate app and manages its own orientation, so the user can rotate their device to portrait for the best experience.
Set a `redirectUrl` callback when generating the verification URL. When the user completes the verification flow, the browser redirects to your deep link, which returns focus to your app or game.
:::
The external browser fully supports **AgeKeys** (WebAuthn is available). As with Custom Tabs and ASWebAuthenticationSession, DOM messages aren't available, so you must use a [callback URL](#callback-url-universal-method) to receive results.
Open the verification URL in the default browser as follows:
```swift
import UIKit
func displayVerificationInBrowser(verificationUrl: URL) {
// Opens the system default browser (a separate app), which manages
// its own orientation regardless of your app's locked orientation.
UIApplication.shared.open(verificationUrl)
}
```
```kotlin
import android.app.Activity
import android.content.Intent
import android.net.Uri
fun displayVerificationInBrowser(activity: Activity, verificationUrl: String?) {
if (verificationUrl == null) {
// Handle error: failed to generate verification URL
return
}
// ACTION_VIEW launches the user's default browser as a separate app,
// which manages its own orientation regardless of your app's lock.
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(verificationUrl))
activity.startActivity(intent)
}
```
Handle the returning deep link exactly as shown in [Step 4](#step-4-handle-the-callback).
## Receiving verification results
Your mobile app needs to receive results after users complete the verification flow. There are two approaches, each with different availability:
For detailed information about analyzing verification results, including field presence rules, status types, and implementation guidance, see the [Verification Event Contract](/events/webhooks/event-types/verification-result#verification-event-contract).
### Callback URL (universal method)
:::tip Recommended approach
The recommended approach for all implementation methods is to use a callback URL. When you call the API to generate a verification URL, include a `redirectUrl` parameter. After the verification flow completes, it redirects to this URL with the results included as query parameters.
:::
**Advantages of callback URLs:**
- Works with all implementation methods
- More reliable than DOM messages
- Standard deep linking pattern for mobile apps
- Results are always delivered, even if the app moves to the background
#### How callback URLs work
1. Register a deep link handler in your app (for example, `myapp://verification-complete`)
2. Include the deep link as `redirectUrl` when calling the API
3. The verification page redirects to your deep link after completion
4. Your app handles the deep link and extracts the results
:::note
Redirects only occur when the verification URL is opened directly in a browser or web view, not when embedded in an iframe.
:::
#### Callback URL parameters
When the verification page redirects to your callback URL, it includes query parameters relevant to the flow. For example:
- Age verification includes `verificationId` and `result`
- When the URL came from a session upgrade, it can also include `sessionId` and status information
Example callback URL:
```
myapp://verification-complete?verificationId=7854909b-9124-4bed-9282-24b44c4a3c97&result=PASS
```
#### Implementing callback URLs
Include `redirectUrl` in your request when calling the age verification API. The URL can be:
- An HTTPS URL: `https://example.com/verification-complete`
- A custom deep link: `myapp://verification-complete`
### DOM messages (WebView/WKWebView only) {#webview-postmessage}
When using [Android WebView](https://developer.android.com/reference/android/webkit/WebView) or [iOS WKWebView](https://developer.apple.com/documentation/webkit/wkwebview), you can listen for JavaScript messages sent from the verification page. This allows you to:
- Receive verification results in real-time
- Control when the web view closes
- Update your app's UI based on verification events
The age verification interface emits the [`Verification.Result`](/events/dom-events/event-structures/verification-result) event, which includes the `status` (such as `PASS` or `FAIL`) and, on success, the resolved `ageCategory`.
DOM messages are sent as `postMessage` events that you can intercept in your native code. For details about available events, see the [DOM events overview](/events/dom-events/overview).
:::note Limited availability
DOM messages only work with WebView and WKWebView, which don't support AgeKeys. They're not available with [Custom Tabs](https://developer.chrome.com/docs/android/custom-tabs/overview/), [Trusted Web Activity](https://developer.android.com/develop/ui/views/layout/webapps/trusted-web-activities), [ASWebAuthenticationSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession), or [SFSafariViewController](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller). Because these components can't create AgeKeys, use a callback URL with a recommended display method instead.
:::
:::tip Platform-specific implementation
For iOS WKWebView, you can receive k-ID events natively by registering a message handler named `kid`. The verification page automatically detects this handler and sends events directly to it. However, Android WebView doesn't have a native mechanism to receive `postMessage` events. You must inject JavaScript to listen for messages and forward them to your native code via a JavaScript interface.
:::
## Third-party app verification flows {#third-party-app-verification-flows}
Some verification methods, such as ConnectID, require redirecting users to a third-party mobile app as part of the verification process. For example, ConnectID opens the user's banking app to complete identity verification.
### How third-party app flows work
When using verification methods that involve third-party apps, the flow passes through multiple applications before returning to your app:
```mermaid
sequenceDiagram
participant App as 📱 Your App
participant Server as 🖥️ Your Server
participant kID as ⚡ k-ID API
participant WebView as 🌐 Web Component
participant ThirdParty as 🏦 Third Party App
participant Browser as 🌐 Native Browser
App->>Server: 1. Request verification URL
Server->>kID: 2. Call k-ID API (with redirectUrl)
kID-->>Server: 3. Return URL with token
Server-->>App: 4. Return URL to app
App->>WebView: 5. Open URL in web component
WebView->>ThirdParty: 6. Deep link to third-party app
ThirdParty->>Browser: 7. Redirect to k-ID result page
Browser->>App: 8. k-ID redirects to your redirectUrl
```
**Step-by-step breakdown:**
1. Your app requests a verification URL from your server
2. Your server calls the k-ID API with your API key, including the app's `redirectUrl`
3. k-ID returns a URL containing the verification interface to your server
4. Your server returns the URL to your app
5. Your app opens the URL in a web component (ASWebAuthenticationSession or Custom Tabs)
6. When the user selects a verification method such as ConnectID, the k-ID UI deep links to the third-party verification app (such as a banking app)
7. After verification completes, the third-party app redirects back to a k-ID result page in the device's native browser
8. k-ID retrieves your stored `redirectUrl` and redirects the user back to your app with the verification result
### Key considerations for third-party app flows
**Web component requirement** - These verification methods must be opened in a system browser context (ASWebAuthenticationSession, Custom Tabs, or SFSafariViewController) rather than an embedded WebView. The third-party app redirect flow requires the full browser context to work correctly.
**Native browser handoff** - After the third-party app completes verification, it redirects to a k-ID URL that opens in the device's native browser rather than returning directly to your original web component. This is a platform limitation with how app-to-app redirects work on mobile devices.
**Callback URL is essential** - Since the verification flow passes through multiple apps and browsers, the `redirectUrl` parameter is critical for returning users to your app after completion. Always include a `redirectUrl` when initiating verifications that might use third-party app methods.
### Testing third-party app flows
The following verification methods use third-party app redirects:
- **[ConnectID](/concepts/verification-methods#testing-connectid-integration)**: Includes test apps for validating the redirect flow in mobile applications.
## Method comparison
| Method | Platform | AgeKeys | DOM Messages | Callback URL | Best For |
|--------|----------|---------|--------------|--------------|----------|
| **WebView** | Android | ❌ | ✅ | ✅ | Not recommended (no AgeKeys support) |
| **Custom Tabs** | Android | ✅ | ❌ | ✅ | Most use cases (recommended) |
| **Trusted Web Activity** | Android | ✅ | ❌ | ✅ | Not recommended (requires digital asset links) |
| **WKWebView** | iOS | ❌ | ✅ | ✅ | Not recommended (no AgeKeys support) |
| **ASWebAuthenticationSession** | iOS | ✅ | ❌ | ✅ | Most use cases (recommended) |
| **SFSafariViewController** | iOS | ✅ | ❌ | ✅ | Safari-like experience |
| **Default external browser** | Android & iOS | ✅ | ❌ | ✅ | Landscape-locked apps (allows portrait rotation) |
## Recommended implementation
### Android: Custom Tabs
Use [Custom Tabs](https://developer.chrome.com/docs/android/custom-tabs/overview/) with callback URLs for the best balance of features and user experience.
**Why Custom Tabs:**
- Full AgeKeys support via WebAuthn
- Access to all modern web features
- Seamless user experience with app branding
- Reliable callback mechanism
- Shares authentication state with Chrome
**Implementation steps:**
1. Register a deep link handler for your callback URL
2. Include `redirectUrl` in your API request
3. Open the verification URL using Custom Tabs
4. Handle the deep link callback with verification results
### iOS: ASWebAuthenticationSession
Use [ASWebAuthenticationSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) with callback URLs for secure, native-feeling compliance flows.
**Why ASWebAuthenticationSession:**
- Full AgeKeys support via WebAuthn
- Access to all modern web features
- System-managed security UI
- Shares cookies with Safari
- Reliable callback mechanism
**Implementation steps:**
1. Register a URL scheme handler for your callback URL
2. Include `redirectUrl` in your API request
3. Present the verification URL using ASWebAuthenticationSession
4. Handle the URL scheme callback with verification results
## Complete implementation example
Here's a step-by-step example of implementing the recommended approach with complete code samples:
### Step 1: Register deep link handler
Register a URL scheme in `Info.plist`:
```xml
CFBundleURLTypesCFBundleURLSchemesmyapp
```
Add an intent filter in `AndroidManifest.xml`:
```xml
```
### Step 2: Generate the verification URL from your server
:::warning Important
The verification URL must be generated from your server, not directly from the mobile app. This protects your API key from being exposed in client-side code. Your mobile app should call your own server API, which then makes the server-to-server call to k-ID.
:::
#### Architecture overview
```mermaid
sequenceDiagram
participant App as 📱 Mobile App
participant Server as 🖥️ Your Server
participant kID as ⚡ k-ID API
App->>Server: 1. Request verification URL
Server->>kID: 2. Call k-ID API (with API key)
kID-->>Server: 3. Return verification URL
Server-->>App: 4. Return verification URL
```
#### Server implementation
Your server calls the [`/age-verification/perform-access-age-verification`](/api/endpoints/perform-access-age-verification) endpoint with your API key, including the `redirectUrl` deep link:
```json
POST https://game-api.k-id.com/api/v1/age-verification/perform-access-age-verification
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY
{
"jurisdiction": "US-CA",
"criteria": {
"ageCategory": "DIGITAL_YOUTH_OR_ADULT"
},
"options": {
"redirectUrl": "myapp://verification-complete"
}
}
```
:::note
For testing, use the test environment endpoint: `https://game-api.test.k-id.com/api/v1/age-verification/perform-access-age-verification`
:::
**Response:**
```json
{
"id": "7854909b-9124-4bed-9282-24b44c4a3c97",
"url": "https://family.k-id.com/verify?token=eyJhbGciOiJFUzM4NCIs...",
"shortUrl": "https://family.k-id.com/v/7854909b-9124-4bed-9282-24b44c4a3c97?pid=42&s=qr"
}
```
:::tip Other supported mobile URLs
The same display and result handling apply to age assurance **challenge URLs**: the `challenge.url` of a `CHALLENGE_AGE_GATE_AGE_ASSURANCE` challenge from [`/age-gate/check`](/api/endpoints/check-age-gate) (when automatic age assurance is enabled) and a `CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE` challenge from [`/session/upgrade`](/api/endpoints/upgrade-session). Only the endpoint that produces the URL differs.
:::
#### Mobile client implementation
Your mobile app calls your server to get the verification URL:
```swift
import Foundation
func fetchVerificationUrl(completion: @escaping (URL?) -> Void) {
// Call YOUR server endpoint, not k-ID directly
guard let url = URL(string: "https://your-server.com/api/generate-verification-url") else {
completion(nil)
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// Add your own authentication (session token, etc.)
request.setValue("Bearer USER_SESSION_TOKEN", forHTTPHeaderField: "Authorization")
let requestBody: [String: Any] = [
"jurisdiction": "US-CA",
"criteria": [
"ageCategory": "DIGITAL_YOUTH_OR_ADULT"
],
"options": [
"redirectUrl": "myapp://verification-complete"
]
]
guard let httpBody = try? JSONSerialization.data(withJSONObject: requestBody) else {
completion(nil)
return
}
request.httpBody = httpBody
URLSession.shared.dataTask(with: request) { data, response, error in
guard error == nil,
let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode),
let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let verificationUrlString = json["url"] as? String,
let verificationUrl = URL(string: verificationUrlString) else {
completion(nil)
return
}
completion(verificationUrl)
}.resume()
}
```
```kotlin
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.io.IOException
fun fetchVerificationUrl(callback: (String?) -> Unit) {
val client = OkHttpClient()
val mediaType = "application/json".toMediaType()
val requestBody = JSONObject().apply {
put("jurisdiction", "US-CA")
put("criteria", JSONObject().apply {
put("ageCategory", "DIGITAL_YOUTH_OR_ADULT")
})
put("options", JSONObject().apply {
put("redirectUrl", "myapp://verification-complete")
})
}.toString().toRequestBody(mediaType)
// Call YOUR server endpoint, not k-ID directly
val request = Request.Builder()
.url("https://your-server.com/api/generate-verification-url")
.post(requestBody)
// Add your own authentication (session token, etc.)
.addHeader("Authorization", "Bearer USER_SESSION_TOKEN")
.addHeader("Content-Type", "application/json")
.build()
client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) {
callback(null)
return
}
response.body?.use { body ->
try {
val jsonResponse = JSONObject(body.string())
val verificationUrl = jsonResponse.optString("url", null)
callback(verificationUrl)
} catch (e: Exception) {
callback(null)
}
} ?: callback(null)
}
override fun onFailure(call: Call, e: IOException) {
callback(null)
}
})
}
```
### Step 3: Display the verification URL
```swift
import AuthenticationServices
// Store session as a property to prevent deallocation
var authSession: ASWebAuthenticationSession?
func displayVerification(verificationUrl: URL) {
authSession = ASWebAuthenticationSession(
url: verificationUrl,
callbackURLScheme: "myapp"
) { callbackURL, error in
if let error = error {
// Handle error (user cancelled, etc.)
return
}
if let callbackURL = callbackURL {
handleVerificationCallback(callbackURL)
}
}
authSession?.presentationContextProvider = self
authSession?.start()
}
```
```kotlin
import android.app.Activity
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
fun displayVerification(activity: Activity, verificationUrl: String?) {
if (verificationUrl == null) {
// Handle error: failed to generate verification URL
return
}
val builder = CustomTabsIntent.Builder()
val customTabsIntent = builder.build()
val uri = Uri.parse(verificationUrl)
customTabsIntent.launchUrl(activity, uri)
}
```
### Step 4: Handle the callback
```swift
func handleVerificationCallback(_ callbackURL: URL) {
// Check if this is a callback URL we're expecting
guard callbackURL.scheme == "myapp",
callbackURL.host == "verification-complete",
let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems else {
return
}
let verificationId = queryItems.first(where: { $0.name == "verificationId" })?.value
let result = queryItems.first(where: { $0.name == "result" })?.value
// Present only when the verification URL came from a session upgrade
let sessionId = queryItems.first(where: { $0.name == "sessionId" })?.value
// Update UI based on the verification result
if result == "PASS" {
// Handle successful verification
} else if result == "FAIL" {
// Handle failed verification
}
// Optionally verify server-side using API endpoints
if let verificationId = verificationId {
verifyResultServerSide(verificationId: verificationId)
} else if let sessionId = sessionId {
verifyResultServerSide(sessionId: sessionId)
}
}
```
```kotlin
import android.content.Intent
import android.net.Uri
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent) // Important: ensure the new intent is used
val data: Uri? = intent.data
if (data != null && data.scheme == "myapp" && data.host == "verification-complete") {
val verificationId = data.getQueryParameter("verificationId")
val result = data.getQueryParameter("result")
// Present only when the verification URL came from a session upgrade
val sessionId = data.getQueryParameter("sessionId")
// Update UI based on the verification result
if (result == "PASS") {
// Handle successful verification
} else {
// Handle failed verification
}
// Optionally verify server-side using API endpoints
if (verificationId != null) {
verifyResultServerSide(verificationId)
} else if (sessionId != null) {
verifyResultServerSide(sessionId)
}
}
}
```
:::tip Best practice
Always verify results server-side using the appropriate API endpoint rather than relying solely on client-side data for security and data integrity. Use the [`/age-verification/get-status`](/api/endpoints/get-age-verification-status) endpoint for a verification, or [`/session/get`](/api/endpoints/get-session) when the verification URL came from a session upgrade. For detailed information about analyzing verification results, including field presence rules, status types, and implementation guidance, see the [Verification Event Contract](/events/webhooks/event-types/verification-result#verification-event-contract). If you rely on webhooks, see [Delivery, retries, and recovery](/events/webhooks/overview#delivery-retries-and-recovery) for the retry policy and how to recover from missed events.
:::
## Hosted URLs on mobile
The display methods and result handling in this guide apply to all hosted k-ID URLs, including the [age gate and end-to-end widget URLs](/cdk/embedded-flow) and these verification URLs:
- **Age verification URL** from [Access age verification](/agekit-plus/waterfall-flow) ([`/age-verification/perform-access-age-verification`](/api/endpoints/perform-access-age-verification)): Standalone age verification with no session (AgeKit+). Supports the [waterfall flow](/agekit-plus/waterfall-flow) and [single method flow](/agekit-plus/single-method-flow).
- **`CHALLENGE_AGE_GATE_AGE_ASSURANCE` challenge URL** from [`/age-gate/check`](/api/endpoints/check-age-gate): The `challenge.url` returned when automatic age assurance is enabled for the product and the player must prove a claimed age.
- **`CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE` challenge URL** from [`/session/upgrade`](/api/endpoints/upgrade-session): The `challenge.url` returned when a permission requires age assurance.
For the age gate and consent flows, we recommend building the UX natively with the [custom workflow](/cdk/custom-workflow), following the [CDK UX guidelines](/cdk/ux-guidelines), for the most seamless, brand-integrated experience, though the age gate and end-to-end widgets are fully supported on mobile as well.
---
// File: get-started/quickstart-guides/age-verification
# Age verification
Age verification is becoming increasingly important as new regulations require platforms to verify user ages before providing access to certain features or content. k-ID's Compliance Development Kit makes compliance simple by providing a comprehensive, jurisdiction-aware verification system that adapts to local legal requirements.
This guide walks you through implementing age verification in just a few steps, allowing you to quickly meet regulatory requirements while providing a smooth user experience.
:::tip Integrate with AI coding agents
Official [Agent Skills](/get-started/agent-skills) cover access age verification, verification URLs, webhooks, and server-side API patterns for AI-assisted integration.
:::
:::tip Try the k-ID Dev Explorer
Use the [k-ID Dev Explorer](https://github.com/kidentify/k-id-dev-explorer), an open source developer sandbox, to test age verification flows and view all traffic in an event log. You can also use it as a starting point for your own age verification implementation.
:::
## Prerequisites
Before you begin, you'll need:
1. **A k-ID Product**: [Create and configure your product](/compliance-studio/creating-product) in the [k-ID Compliance Studio](https://portal.k-id.com/)
2. **API Key**: Generate your API key from the Developer Settings page of your product in the [Compliance Studio](/compliance-studio/creating-product)
3. **Webhook Endpoint** (optional but recommended): Set up a secure HTTPS endpoint to receive verification results. For more detail, see [Webhooks](/webhooks).
## Step 1: Initiate age verification
Call the [`/age-verification/perform-access-age-verification`](/api/endpoints/perform-access-age-verification) API to create a verification request. This returns a unique URL where users can complete their age verification.
:::tip
Use the [API reference](/api/overview) with your API key to quickly generate your Age Verification URL.
:::
:::warning Important
For your implementation, this should be a server-to-server call to protect your API key from being exposed in client-side code.
:::
:::important Create verifications when users start the flow
Call verification creation endpoints (for example [Perform access age verification](/api/endpoints/perform-access-age-verification)) only after the user takes an action to begin verification. Don't pre-generate verifications or widget URLs for flows they might never start. See [Best practices](/agekit-plus/best-practices#when-to-create-verifications) for more detail.
:::
### Example request
```json
POST /api/v1/age-verification/perform-access-age-verification
Content-Type: application/json
Authorization: Bearer your-api-key
{
"jurisdiction": "GB",
"criteria": {
"ageCategory": "ADULT"
}
}
```
### Example response
```json
{
"id": "7854909b-9124-4bed-9282-24b44c4a3c97",
"url": "https://family.k-id.com/verify?token=eyJhbGciOiJFUzM4NCIs...",
"shortUrl": "https://family.k-id.com/v/7854909b-9124-4bed-9282-24b44c4a3c97?pid=42&s=qr"
}
```
Store the `id` (verification ID) so you can check status later without relying on the URL. The URL is valid for **2 weeks** after creation; the expiry is in the JWT `exp` claim in the `token` query parameter. If a URL has expired, call [`/age-verification/get-status`](/api/endpoints/get-age-verification-status) with the verification ID. If get-status returns 400 with error code `INVALID_INPUT`, create a new one. For full details on URL validity, expired URLs, and verification retention, see [Verification URL validity and checking verification status](/agekit-plus/waterfall-flow#verification-url-validity).
The `jurisdiction` parameter ensures compliance with local regulations (such as setting `"GB"` for UK requirements), while the `ageCategory: "ADULT"` criteria verifies users meet the age criteria requirements defined in that jurisdiction.
## Step 2: Display the verification interface
Use the returned URL to create an iframe in your website or app. Users complete their verification through this interface, with available methods automatically adapting to jurisdictional requirements.

### HTML Implementation
```html
```
The iframe presents users with multiple verification methods such as:
- **AgeKey**: A reusable and anonymous age-proof generated after an initial verification process.
- **Facial Age Estimation**: Privacy-preserving age estimation using a device's camera
- **ID Document Verification**: Government-issued ID verification
The specific methods available depend on the jurisdiction and your product configuration. For more detail, see [Verification Methods](/concepts/verification-methods)
## Step 3: Handle verification results
Once the user has successfully completed the age verification, or the user has retried the maximum number of times and hasn't succeeded, you can receive verification results. **Implementations should use a combination of client-side and server-side methods**: client-side events are best for controlling UI elements, while for data integrity, the actual results should come from either a webhook or a call to [`/age-verification/get-status`](/api/endpoints/get-age-verification-status).
For detailed information about analyzing verification results, including field presence rules, status types, and implementation guidance, see the [Verification Event Contract](/events/webhooks/event-types/verification-result#verification-event-contract).
### Client-side (DOM events)
Use DOM Events for responsive UI updates when verification completes. For detailed information about the event structure, see [`Verification.Result`](/events/dom-events/event-structures/verification-result).
:::important Closing the UI
Listen for the [`Widget.ExitReview`](/events/dom-events/event-structures/widget-exitreview) event to determine when to close the age verification UI. This event is emitted when the user clicks the 'Done' button, indicating the flow is complete and the iframe should be closed or hidden.
:::
```javascript
const handleMessage = (event) => {
const message = event.data;
if (message.eventType === "Verification.Result") {
if (message.data.status === "PASS") {
// User passed verification - update UI immediately
console.log("Age verified:", message.data.ageCategory);
updateUI();
} else if (message.data.status === "FAIL") {
// User failed verification - update UI immediately
console.log("Verification failed:", message.data.failureReason);
updateUI();
}
}
if (message.eventType === "Widget.ExitReview") {
// Close the verification UI when the user clicks 'Done'
closeVerificationUI();
}
};
window.addEventListener("message", handleMessage);
```
### Server-side (webhooks, API calls)
Use webhooks or API calls for data integrity and reliable state management. For data integrity, always verify results with events from webhooks or by calling [`/age-verification/get-status`](/api/endpoints/get-age-verification-status) rather than relying solely on DOM Events.
#### Webhooks
For detailed information about the webhook event structure, see [`Verification.Result`](/events/webhooks/event-types/verification-result).
[Configure your webhook endpoint](/webhooks) to receive [`Verification.Result`](/events/webhooks/event-types/verification-result) events:
```json
{
"eventType": "Verification.Result",
"data": {
"id": "7854909b-9124-4bed-9282-24b44c4a3c97",
"status": "PASS",
"ageCategory": "adult",
"method": "id-document",
"age": {
"low": 25,
"high": 25
}
}
}
```
#### API calls
Query the verification status with the verification ID by using [`/age-verification/get-status`](/api/endpoints/get-age-verification-status). This works independent of the verification URL, so you can check status even after the URL has expired. If get-status returns 400 with error code `INVALID_INPUT` (for example, the verification was still PENDING after 2 weeks and was removed), create a new verification. For detailed information about analyzing verification results, including field presence rules, status types, and implementation guidance, see the [Verification Event Contract](/events/webhooks/event-types/verification-result#verification-event-contract):
```json
GET /api/v1/age-verification/get-status?id=7854909b-9124-4bed-9282-24b44c4a3c97
Response:
{
"id": "7854909b-9124-4bed-9282-24b44c4a3c97",
"status": "PASS",
"ageCategory": "adult",
"method": "id-document"
}
```
## What's next?
Now that you've implemented basic age verification, explore these resources to enhance your integration:
- **[API Reference Documentation](/api/overview)**: Detailed documentation of all age verification APIs
- **[Verification Methods](/concepts/verification-methods)**: Configure specific verification methods and scenarios for your product
- **[Webhooks Setup](/webhooks)**: Implement robust webhook handling for production systems
- **[Best Practices](/agekit-plus/best-practices)**: Implement best practices to ensure security and a reliable user experience
- **[Pre-launch Checklist](/agekit-plus/prelaunch-checklist)**: Review requirements before going live (contact k-ID support for the latest checklist)
With k-ID's Age Verification API, you can quickly achieve compliance with age verification regulations while providing users with a smooth, privacy-focused verification experience.
---
// File: get-started/quickstart-guides/ai-products
# AI products
AI products that serve children and teens face evolving global regulations on content, data, and AI behavior, including COPPA in the US, GDPR-K in the EU, and the UK's AADC. These regulations determine which AI capabilities are available at what age. k-ID's CDK resolves the user's age and jurisdiction into a set of AI-specific permissions, which your product reads to enable or disable each capability per user.
:::tip Try the k-ID Dev Explorer
Use the [k-ID Dev Explorer](https://github.com/kidentify/k-id-dev-explorer), an open source developer sandbox, to test AI permission flows and view all traffic in an event log. You can also use it as a starting point for your own AI product implementation.
:::
## What's an AI product in k-ID's model?
An AI product is any application where users interact with generative AI capabilities such as text chat, voice, image or video generation, or persistent AI personas. k-ID treats each AI capability as a separate permission. Each permission can be enabled, disabled, or prohibited based on the user's age, jurisdiction, and parent settings.
The CDK flow is the same as any other k-ID product: age gate → VPC if the user is a minor → session → enforce permissions → respond to changes via webhook. The AI-specific part is the set of permissions returned in the session, listed in Step 1.
See the [VPC quick start](/get-started/quickstart-guides/vpc) and [Managing sessions and permissions quick start](/get-started/quickstart-guides/managing-sessions-permissions).
## Prerequisites
Before you begin, you'll need:
1. **A k-ID Product**: [Create and configure your product](/compliance-studio/creating-product) in the [k-ID Compliance Studio](https://portal.k-id.com/)
2. **API Key**: Generate your API key from the Developer Settings page of your product in the Compliance Studio
3. **Webhook Endpoint** (optional but recommended): Set up a secure HTTPS endpoint to receive session and permission events. For more detail, see [Webhooks](/webhooks).
4. **AI permissions enabled in Compliance Studio**: Turn on the AI permissions that match your product's capabilities (see Step 1).
## Step 1: Configure AI permissions
In the [Compliance Studio](https://portal.k-id.com/), open your product's [Configuration → Permissions](/compliance-studio/product-api-configuration#permissions) tab. The **AI** category contains seven permissions that map to the capabilities common in AI products:
| Permission | Display name | Description | Enable when… |
|---|---|---|---|
| `ai-chat` | AI Chat | Your child can freely communicate with a feature that uses generative AI, such as for interactivity or support functions. | Your product offers any freeform conversation with an AI, regardless of input modality. |
| `ai-companion-chatbot` | Companion Chatbots | Your child can interact with AI characters or companions that mimic human interaction through personalized content. | Your AI is positioned as a persistent companion or persona that maintains a relationship across sessions. |
| `ai-memory` | AI Memory | Your child's preferences and interactions with an AI are retained and used to create a profile of their experience over time. | Your AI retains user preferences or conversation history across sessions to personalize future interactions. |
| `ai-voice-mode` | AI Voice Mode | Your child can speak to an AI in real-time voice conversations. The AI processes the child's voice. | Your AI processes the user's voice (speech-to-text) or generates speech back (text-to-speech). |
| `ai-media-generation` | AI Media Generation | Your child can interact with an AI that can generate images, video, or both based on the child's prompt. | Your AI generates images, video, or audio in response to the user. |
| `ai-media-upload` | AI Media Upload | Your child can provide an image, video, or other media as part of their interactions with AI. | Users can upload images, video, or audio to be processed by the AI. |
| `ai-model-training` | AI Model Training | Your child's information and inputs are used to train an AI model. | User inputs can be used to train or fine-tune AI models. |
Enable the permissions that reflect your product's capabilities and leave the rest off. Jurisdiction-specific defaults are already set across all supported jurisdictions, including tailored rules for US (COPPA), UK (AADC), Australia (Online Safety Act), EU (GDPR Art. 22), and Brazil.
:::tip `ai-model-training` and COPPA
Under the 2026 COPPA Amendment, disclosures of a child's personal information to train or develop AI models are treated as **non-integral** and require their own parental consent. If your product uses user inputs for training, make sure `ai-model-training` is enabled and review the [COPPA 2026 Amendment guide](/compliance-guides/coppa-2026-amendment).
:::
For the full list of permissions across all categories, see [Available permissions](/concepts/access-features-consent/permissions#available-permissions).
## Step 2: Collect parental consent and create a session
AI products follow the same age gate and Verifiable Parental Consent (VPC) flow as any other k-ID product. The widgets and APIs handle the jurisdictional logic, so your product just needs to start the flow and receive the resulting session.
For the end-to-end integration, follow one of:
- **[VPC quick start](/get-started/quickstart-guides/vpc)**: The fastest path. Use the end-to-end widget to handle age collection, VPC, data notices, and permissions in one iframe.
- **[Custom age gate quick start](/get-started/quickstart-guides/custom-age-gate)**: Build your own age gate UI while k-ID handles the compliance logic.
Once the parent completes the consent flow, your product receives a `sessionId`. Fetch the session with [`/session/get`](/api/endpoints/get-session) to read the AI permissions you configured in Step 1:
```json
GET /api/v1/session/get?sessionId=0ad1641f-c154-4c2a-8bb2-74dbd0de7723
Response:
{
"session": {
"id": "0ad1641f-c154-4c2a-8bb2-74dbd0de7723",
"permissions": [
{ "name": "ai-chat", "enabled": true, "managedBy": "GUARDIAN" },
{ "name": "ai-companion-chatbot", "enabled": false, "managedBy": "GUARDIAN" },
{ "name": "ai-memory", "enabled": true, "managedBy": "GUARDIAN" },
{ "name": "ai-voice-mode", "enabled": false, "managedBy": "GUARDIAN" },
{ "name": "ai-media-generation", "enabled": false, "managedBy": "GUARDIAN" },
{ "name": "ai-media-upload", "enabled": false, "managedBy": "GUARDIAN" },
{ "name": "ai-model-training", "enabled": false, "managedBy": "PROHIBITED" }
]
}
}
```
The `managedBy` field tells you who controls each permission: `GUARDIAN`, `PLAYER`, or `PROHIBITED`. For the full semantics, see [Permissions](/concepts/access-features-consent/permissions#permission-fields).
## Step 3: Gate AI features based on the session
Before offering each AI feature, check the relevant permission on the session. The pattern is the same as any other k-ID-gated feature:
- **[Permissions](/concepts/access-features-consent/permissions)**: how `enabled` and `managedBy` work, including `GUARDIAN`, `PLAYER`, and `PROHIBITED` semantics.
- **[Managing sessions and permissions quick start](/get-started/quickstart-guides/managing-sessions-permissions)**: fetching sessions, comparing them over time, requesting upgrades, and handling disabled features in your UI.
### Build the AI request from the session
Read `permissions` from the session, gate each AI capability with a small helper, and pass the resulting config object to your AI provider:
```javascript
function allowed(session, name) {
const permission = session.permissions.find((p) => p.name === name);
return permission?.enabled === true;
}
function buildAIConfig(session) {
return {
chat: allowed(session, "ai-chat"),
companion: allowed(session, "ai-companion-chatbot"),
memory: allowed(session, "ai-memory"),
voice: allowed(session, "ai-voice-mode"),
mediaGeneration: allowed(session, "ai-media-generation"),
mediaUpload: allowed(session, "ai-media-upload"),
trainOnInputs: allowed(session, "ai-model-training"),
};
}
const aiConfig = buildAIConfig(session);
const response = await aiProvider.respond({ prompt, ...aiConfig });
```
Four rules to remember as you wire this up:
1. **Read, then enforce.** Call [`/session/get`](/api/endpoints/get-session) on session start.
2. **Fail closed.** Permission missing or session unreachable = feature off.
3. **Degrade gracefully.** Feature off ≠ dead end. Show a fallback.
4. **Default off for minors.** Parents enable.
## Step 4: Respond to permission changes
Parents can adjust AI permissions at any time through Family Connect. For example, a parent might disable `ai-companion-chatbot` after reading a notification or enable `ai-voice-mode` once the child is old enough. Your product needs to pick up those changes and reflect them to the user.
The detection and handling pattern is identical to any other permission change:
1. Subscribe to the [`Session.ChangePermissions`](/events/webhooks/event-types/session-changepermissions) webhook, or compare the cached session to the current session on each launch.
2. When a permission toggles, update your product's state immediately so the user doesn't try to use a feature that's been turned off.
3. Show a clear message to the user explaining what changed and why.
For the full implementation, including webhook handling, session comparison, handling `managedBy: PLAYER` for aged-up users, and communicating changes to the user, follow the [Managing sessions and permissions quick start](/get-started/quickstart-guides/managing-sessions-permissions).
## Webhook configuration
For AI products, ensure your webhook endpoint is configured to receive:
- **[`Session.ChangePermissions`](/events/webhooks/event-types/session-changepermissions)**: Notifies when a parent enables or disables an AI permission for the user.
This event is essential for keeping AI features in sync with parent decisions without waiting for the next session fetch.
## What's next?
Now that your AI product is wired up to k-ID, explore these resources to go deeper:
- **[Permissions](/concepts/access-features-consent/permissions)**: Full reference for permission structure, `managedBy` values, and upgrade flows
- **[Managing sessions and permissions](/get-started/quickstart-guides/managing-sessions-permissions)**: Real-time handling of parent-initiated permission changes
- **[COPPA 2026 Amendment guide](/compliance-guides/coppa-2026-amendment)**: Compliance implications for AI model training and other non-integral disclosures
- **[Webhooks](/webhooks)**: Complete guide to webhook implementation and validation
- **[CDK overview](/cdk/overview)**: Deeper dive into the Compliance Engine that powers permission decisions
- **[Pre-launch checklist](/cdk/prelaunch-checklist)**: Review requirements before going live
---
// File: get-started/quickstart-guides/custom-age-gate
# 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.
:::tip 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.
:::
:::info Automatic age assurance
If your product has [Automatic age assurance](#step-4b-handle-age-assurance-challenges) 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](#step-4b-handle-age-assurance-challenges).
:::
:::tip Integrate with AI coding agents
Official [Agent Skills](/get-started/agent-skills) cover custom age gates, [automatic age assurance](#step-4b-handle-age-assurance-challenges), parental consent, and webhooks.
:::
## 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](/compliance-studio/creating-product) in the [k-ID Compliance Studio](https://portal.k-id.com/)
2. **Essential Permissions Configuration**: Configure all permissions as "essential" in your product's [permissions settings](/compliance-studio/product-api-configuration#permissions)
3. **API Key**: Generate your API key from the Developer Settings page of your product in the [Compliance Studio](/compliance-studio/creating-product)
4. **Webhook Endpoint** (recommended): Set up a secure HTTPS endpoint to receive challenge and session events. For more detail, see [Webhooks](/webhooks).
## Step 1: Get age gate requirements
Before displaying your age gate, call [`/age-gate/get-requirements`](/api/endpoints/get-age-gate-requirements) to determine what you need to show based on the user's jurisdiction.
:::warning 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
```json
GET /api/v1/age-gate/get-requirements?jurisdiction=US-CA
Authorization: Bearer your-api-key
```
### Example response
```json
{
"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 |
:::info No Age Gate Required
If `shouldDisplay` is `false`, skip the age gate and call [`/age-gate/get-default-permissions`](/api/endpoints/get-default-permissions) to get the default session permissions for that jurisdiction.
:::
:::tip 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](/cdk/age-signals/overview).
:::
## 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
:::tip UX Guidelines
For detailed design recommendations, including age slider behavior, date picker requirements, and accessibility considerations, see the [UX guidelines](/cdk/ux-guidelines).
:::
### Example age gate UI
```html
Please enter your date of birth
```
## Step 3: Check age with the API
When the user submits their age, call [`/age-gate/check`](/api/endpoints/check-age-gate) 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:
```json
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:
```json
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](/cdk/age-signals/overview).
```json
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.
```json
{
"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.
```json
{
"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.
```json
{
"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.
```json
{
"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.
:::tip UX Guidelines
For detailed design recommendations for the trusted adult consent flow, including layout examples and implementation tips, see [Trusted adult consent](/cdk/ux-guidelines#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`](/api/endpoints/send-email) to send a consent request email.
```json
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.
```javascript
// 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](https://asktoplay.com) and enter the code.
### Example challenge screen implementation
```html
Ask a trusted adult to help you
Please ask a trusted adult to complete setup using one of the following
methods.
```
### 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`](/api/endpoints/get-challenge) to restore the challenge screen.
```javascript
// 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.
```html
```
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`](/api/endpoints/get-challenge-status).
```javascript
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`](/events/dom-events/event-structures/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"`).
```json
{
"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`](/events/webhooks/event-types/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:
1. **Get the session permissions**: Call [`/session/get`](/api/endpoints/get-session) with the `sessionId` to retrieve the full session details including permissions.
```json
GET /api/v1/session/get?id=0ad1641f-c154-4c2-8bb2-74dbd0de7723
Authorization: Bearer your-api-key
```
**Example response:**
```json
{
"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"
}
```
2. **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.
```javascript
// Clear the pending challenge and store the active session
user.challengeId = null;
user.sessionId = data.sessionId;
```
3. **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).
```json
{
"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
```javascript
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`](/api/endpoints/get-challenge-status) to check challenge status:
```json
GET /api/v1/challenge/get-status?id=683409f1-2930-4132-89ad-827462eed9af
Authorization: Bearer your-api-key
```
:::warning 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:
- **[`Challenge.StateChange`](/events/webhooks/event-types/challenge-statechange)**: Notifies when parental consent is approved or denied
- **[`Session.Delete`](/events/webhooks/event-types/session-delete)**: Notifies when a session is deleted by a parent
For information on validating webhook signatures, see [Webhooks](/webhooks#validating-webhook-requests).
## What's next?
Now that you've implemented the custom age gate, explore these resources to enhance your integration:
- **[UX Guidelines](/cdk/ux-guidelines)**: Design recommendations for age sliders, date pickers, and consent flows
- **[API Reference Documentation](/api/overview)**: Detailed documentation of all age gate and challenge APIs
- **[Webhooks Setup](/webhooks)**: Implement robust webhook handling for production systems
- **[Best Practices](/cdk/best-practices)**: Implement best practices to ensure security and a reliable user experience
- **[Testing](/concepts/testing)**: Test your integration with test mode APIs
- **[Pre-launch Checklist](/cdk/prelaunch-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.
---
// File: get-started/quickstart-guides/managing-sessions-permissions
# Managing sessions and permissions
Once a player and their parent have completed the consent flow (whether through the [VPC widget](/get-started/quickstart-guides/vpc) or a [custom age gate](/get-started/quickstart-guides/custom-age-gate)), your game receives a session containing the player's permissions. However, permissions can change over time: a parent might adjust settings through Family Connect, a player might have a birthday that moves them to a new age category, someone might request access to additional features, or a high-risk permission might need age verification before it can be unlocked.
This guide explains how to detect these changes, respond appropriately, and communicate clearly with players so they always understand their current feature access.
:::tip Why This Matters
When permissions change while your game is closed (or even while it's running), players can become confused if features suddenly appear or disappear. Without clear communication, these changes can appear to be bugs rather than intentional updates from a parent or age-related adjustments.
:::
## Prerequisites
Before you begin, ensure you have:
1. **A completed consent flow**: Players should already have active sessions from completing the [VPC quick start](/get-started/quickstart-guides/vpc) or [Custom age gate quick start](/get-started/quickstart-guides/custom-age-gate)
2. **A k-ID Product**: [Create and configure your product](/compliance-studio/creating-product) in the [k-ID Compliance Studio](https://portal.k-id.com/)
3. **API Key**: Generate your API key from the Developer Settings page of your product in the [Compliance Studio](/compliance-studio/creating-product)
4. **Webhook endpoint** (recommended): Set up a secure HTTPS endpoint to receive session events. For more detail, see [Webhooks](/webhooks).
## Understanding sessions and challenges
Once a player has been granted access by their trusted adult, they have exactly one session per product. When permissions change (whether through parent modifications, age-up events, or permission upgrades), the same session ID is updated with the new permissions. A new session isn't created; the existing session reflects the current state of the player's access. However, if a session is revoked and the trusted adult goes through the consent flow again, a new session with a new session ID is created.
A **challenge** is a consent request that requires parental approval. When a challenge completes successfully (`PASS`), a session is created or updated with the granted permissions. Conversely, if a session is deleted, all incomplete challenges for that player are automatically set to `FAIL`. For detailed information about challenges, see the [Challenges](/concepts/access-features-consent/challenges) concept guide.
## How permissions can change
Permission changes can occur for several reasons:
| Change type | Description | Detection method |
|------------|-------------|------------------|
| **Parent modifies permissions** | A parent uses Family Connect to enable or disable features | Webhook or session comparison |
| **Player ages up** | A birthday moves the player to a new age category. When a player ages up to no longer require parental consent, permissions have `managedBy` set to `PLAYER`, allowing the player to control them directly | Session comparison only |
| **High-risk permission unlocked** | A player completes age assurance for a permission with `verifiedAgeThreshold` (for example, loot boxes in Brazil). The permission moves from `enabled: false` to `enabled: true` | Webhook (`Session.ChangePermissions`) or session comparison |
| **Session deleted** | A parent revokes access through Family Connect, which results in the session being deleted (returns 400) | Webhook or session comparison (returns 400) |
Understanding these scenarios helps you implement the right detection strategy.
## Step 1: Choose your detection approach
You have two approaches for detecting permission changes. Most implementations should use both, with webhooks as the primary method and session comparison as a fallback.
### Approach A: Webhook-based detection (recommended)
**Use webhooks when you have a server that can receive HTTP callbacks.**
With this approach, k-ID notifies your server immediately when a parent changes permissions. Your server updates its own state, and the game reads the updated state on next launch or feature access.
**Advantages:**
- Real-time notifications with no delay in detecting changes
- Lower resource usage than constant polling
- Enables proactive notifications to players
**How it works:**
```mermaid
sequenceDiagram
participant Parent as Parent Portal
participant KID as k-ID Engine
participant Server as Your Server
participant Game as Game Client
Parent->>KID: Changes permission
KID->>Server: Webhook: Session.ChangePermissions
Note over Server: Updates local session state
Game->>Server: Game start or feature access
Server->>Game: Updated state + change flag
Note over Game: Shows dialog to player
```
### Approach B: Session comparison on restart
**Use session comparison when you don't have webhooks, or as a fallback alongside webhooks.**
With this approach, your game caches the last known session and compares it against the current session from k-ID on each restart (or periodically during gameplay).
**Advantages:**
- Doesn't require webhook configuration
- Catches age-up changes (which don't trigger webhooks)
- Simple to implement
**How it works:**
```mermaid
sequenceDiagram
participant Game as Game Client
participant Server as Your Server
participant KID as k-ID Engine
participant Parent as Parent Portal
Note over KID,Parent: While game is closed
Parent->>KID: Changes permissions
Note over Game: Game starts
Game->>Server: Check for session changes
Server->>KID: GET /session/get
KID->>Server: Current session with updated permissions
Note over Server: Compare with cached session, detect changes
Server->>Game: Updated session + changes
Note over Game: Show dialog to player
```
## Step 2: Implement webhook-based detection
If your game has a server, implement webhook-based detection for real-time permission updates.
:::warning Server-side API calls required
All k-ID API calls must be made from your server, not from client-side code. Your API key should never be exposed to game clients. The examples in this guide show server-side code (Node.js and Express). Your game client should communicate with your own server, which then makes calls to the k-ID API.
:::
### Configure your webhook endpoint
In the [Compliance Studio](https://portal.k-id.com/), configure your webhook URL under Developer Settings for your product. Ensure your endpoint can receive:
- **[`Session.ChangePermissions`](/events/webhooks/event-types/session-changepermissions)**: Fired when a parent changes permissions
- **[`Session.Delete`](/events/webhooks/event-types/session-delete)**: Fired when a session is deleted
### Handle the `Session.ChangePermissions` webhook
When k-ID fires this webhook, update your server state to flag that the session has changed:
```javascript
app.post("/webhook/k-id", (req, res) => {
const { eventType, data } = req.body;
switch (eventType) {
case "Session.ChangePermissions":
// Flag this session as having changed permissions
markSessionAsChanged(data.id, {
changeType: "permissions_updated",
changedAt: new Date().toISOString()
});
break;
case "Session.Delete":
// Parent revoked access - session has been deleted
// Note: All incomplete challenges for this player are automatically set to FAIL
markSessionAsDeleted(data.id);
break;
}
res.status(200).send("OK");
});
```
### Fetch and compare permissions
When your game client connects to your server, check for the change flag, fetch the updated session from k-ID, and compare it against your stored version to determine what changed:
```javascript
// Server-side endpoint that your game client calls
app.get("/api/check-permissions/:sessionId", async (req, res) => {
const { sessionId } = req.params;
const changeInfo = await getChangeFlag(sessionId);
if (!changeInfo.hasChanged) {
return res.json({ hasChanged: false });
}
const cachedSession = await getStoredSession(sessionId);
const response = await fetch(
`https://game-api.k-id.com/api/v1/session/get?sessionId=${sessionId}`,
{ headers: { "Authorization": `Bearer ${process.env.KID_API_KEY}` } }
);
const { session: newSession } = await response.json();
const changes = comparePermissions(cachedSession, newSession);
await storeSession(sessionId, newSession);
await clearChangeFlag(sessionId);
res.json({
hasChanged: true,
changes: changes,
session: newSession
});
});
```
The `comparePermissions` function (shown in Step 3) identifies exactly which permissions changed, enabling you to show specific messages to the player.
## Step 3: Implement session comparison
Implement session comparison as a fallback (or primary method if you don't have webhooks). This approach is also essential for detecting age-up changes, which don't trigger webhooks.
:::info Why comparison is needed
The `Session.ChangePermissions` webhook notifies you *that* permissions changed, but doesn't include *what* changed. To determine which specific permissions were enabled or disabled, you must fetch the updated session and compare it against your cached version. This comparison logic is required whether you use webhooks or polling.
:::
### Store the session
Store the session on your server whenever you receive it (in a database, cache, or other persistent storage associated with the player):
```javascript
// Server-side session storage
async function storeSession(sessionId, session) {
await db.sessions.upsert({
sessionId: sessionId,
session: session,
updatedAt: new Date().toISOString()
});
}
async function getStoredSession(sessionId) {
const record = await db.sessions.findOne({ sessionId });
return record?.session || null;
}
```
### Compare sessions on game start
When your game client starts, it should call your server to check for session changes. Your server fetches the current session from k-ID and compares it:
```javascript
// Server-side endpoint that your game client calls on startup
app.get("/api/session/:sessionId", async (req, res) => {
const { sessionId } = req.params;
const cachedSession = await getStoredSession(sessionId);
if (!cachedSession) {
return res.json({ needsConsent: true });
}
const response = await fetch(
`https://game-api.k-id.com/api/v1/session/get?sessionId=${sessionId}&etag=${cachedSession.etag}`,
{ headers: { "Authorization": `Bearer ${process.env.KID_API_KEY}` } }
);
if (response.status === 304) {
return res.json({ hasChanged: false, session: cachedSession });
}
const { session: currentSession } = await response.json();
const changes = comparePermissions(cachedSession, currentSession);
await storeSession(sessionId, currentSession);
res.json({
hasChanged: changes.length > 0,
changes: changes,
session: currentSession
});
});
```
### Detect permission differences
Compare the old and new sessions to identify specific changes:
```javascript
function comparePermissions(oldSession, newSession) {
const changes = [];
for (const newPerm of newSession.permissions) {
const oldPerm = oldSession.permissions.find(p => p.name === newPerm.name);
if (!oldPerm) {
changes.push({ type: "added", permission: newPerm.name, enabled: newPerm.enabled });
} else if (oldPerm.enabled !== newPerm.enabled) {
changes.push({
type: newPerm.enabled ? "enabled" : "disabled",
permission: newPerm.name,
previousState: oldPerm.enabled
});
} else if (oldPerm.managedBy !== newPerm.managedBy) {
changes.push({
type: "management_changed",
permission: newPerm.name,
previousManagedBy: oldPerm.managedBy,
newManagedBy: newPerm.managedBy
});
}
}
if (oldSession.ageStatus !== newSession.ageStatus) {
changes.push({
type: "age_status_changed",
previousStatus: oldSession.ageStatus,
newStatus: newSession.ageStatus
});
}
return changes;
}
```
:::important Age-up and player-managed permissions
When a player ages up and no longer requires parental consent, k-ID doesn't send a webhook notification. However, when you compare sessions, you'll notice that permissions that were previously `managedBy: "GUARDIAN"` might change to `managedBy: "PLAYER"`.
When permissions become player-managed, the player can control them directly without parental consent. You should update your UI to allow players to enable or disable these permissions themselves rather than requiring them to ask a parent. When a player requests to enable a `PLAYER`-managed permission via the [`/session/upgrade`](/api/endpoints/upgrade-session) API, it's automatically enabled without creating a challenge. The `updateFeatureAccess` function in Step 4 shows how to handle `PLAYER`-managed permissions in your UI.
:::
## Step 4: Communicate changes to players
When you detect changes, clearly communicate them to players so they understand why features have changed. This is crucial: players should never think something is broken.
:::tip UX Guidelines
For detailed design recommendations on communicating permission changes, displaying disabled features, and handling permission requests, see the [UX guidelines](/cdk/ux-guidelines).
:::
### Show an informative dialog
On the client side, display a dialog explaining what changed and why (using the `changes` array returned by your server):
```javascript
function showPermissionChangeDialog(changes) {
const disabledFeatures = changes
.filter(c => c.type === "disabled")
.map(c => getFeatureDisplayName(c.permission));
const enabledFeatures = changes
.filter(c => c.type === "enabled")
.map(c => getFeatureDisplayName(c.permission));
const ageChanged = changes.find(c => c.type === "age_status_changed");
let message = "";
if (ageChanged) {
// Player aged up
message = "Happy birthday! 🎉 Your permissions have been updated based on your new age.";
} else if (disabledFeatures.length > 0 && enabledFeatures.length === 0) {
// Parent restricted features
message = "Your parent has updated your permissions. " +
"The following features are no longer available:\n\n" +
disabledFeatures.map(f => `• ${f}`).join("\n");
} else if (enabledFeatures.length > 0 && disabledFeatures.length === 0) {
// Parent enabled features
message = "Great news! Your parent has enabled new features:\n\n" +
enabledFeatures.map(f => `• ${f}`).join("\n");
} else {
// Mixed changes
message = "Your permissions have been updated.";
if (enabledFeatures.length > 0) {
message += "\n\nNow available:\n" + enabledFeatures.map(f => `• ${f}`).join("\n");
}
if (disabledFeatures.length > 0) {
message += "\n\nNo longer available:\n" + disabledFeatures.map(f => `• ${f}`).join("\n");
}
}
showDialog({
title: "Permissions Updated",
message: message,
buttons: [{ text: "OK", action: "dismiss" }]
});
}
function getFeatureDisplayName(permissionName) {
const displayNames = {
"voice-chat": "Voice Chat",
"text-chat-private": "Private Messages",
"text-chat-public": "Public Chat",
"in-game-purchases": "In-Game Purchases",
"multiplayer": "Online Multiplayer",
// Add all your permissions here
};
return displayNames[permissionName] || permissionName;
}
```
### Handle disabled features gracefully
On the client side, when a feature is disabled, ensure the UI reflects this clearly (using the session data returned by your server):
```javascript
function updateFeatureAccess(session) {
for (const permission of session.permissions) {
const featureElement = document.querySelector(`[data-feature="${permission.name}"]`);
if (!featureElement) continue;
if (!permission.enabled) {
featureElement.classList.add("feature-disabled");
if (permission.managedBy === "GUARDIAN") {
// Requires parental consent to enable
featureElement.setAttribute("data-disabled-reason", "parent");
featureElement.querySelector(".disabled-message").textContent =
"Ask a parent to enable this feature";
} else if (permission.managedBy === "PROHIBITED") {
// Not available: jurisdiction ban or player age is below verifiedAgeThreshold
featureElement.setAttribute("data-disabled-reason", "prohibited");
featureElement.querySelector(".disabled-message").textContent =
"This feature is not available";
} else if (permission.managedBy === "PLAYER" && permission.verifiedAgeThreshold) {
// Player-managed but requires age verification first (for example, loot boxes in Brazil)
featureElement.setAttribute("data-disabled-reason", "age-verification");
featureElement.querySelector(".disabled-message").textContent =
"Age verification required to enable this feature";
featureElement.addEventListener("click", () => requestAgeVerification(permission.name));
} else if (permission.managedBy === "PLAYER") {
// Player can enable directly: no consent or verification needed
featureElement.setAttribute("data-disabled-reason", "player-choice");
featureElement.querySelector(".disabled-message").textContent = "Tap to enable";
featureElement.addEventListener("click", () => togglePlayerPermission(permission.name));
}
} else {
featureElement.classList.remove("feature-disabled");
if (permission.managedBy === "PLAYER") {
featureElement.setAttribute("data-managed-by", "player");
}
}
}
}
```
:::tip Handling player-managed permissions
When `managedBy` is `PLAYER`, the player can control the permission directly without parental consent. This typically occurs after a player ages up. You should provide UI controls such as toggles and buttons that allow players to enable or disable these permissions. When a player requests to enable a `PLAYER`-managed permission via the [`/session/upgrade`](/api/endpoints/upgrade-session) API, it's automatically enabled without creating a challenge (no parental consent is required).
:::
## Step 5: Handle session deletion
When a parent revokes all access to your product for a player through Family Connect, the session is deleted as the final step. The session simply disappears: queries return HTTP 400. When this happens (detected via webhook or during session comparison), the player must complete the age gate and consent flow again to regain access.
### Understanding session deletion
When a parent revokes access to your game or product:
1. **The session is deleted** - As the final step, the session is removed. Queries return HTTP 400, making it appear as if the session never existed
2. **All incomplete challenges are failed** - Any pending challenges for that player are automatically set to `FAIL`
3. **Webhook events are sent** - You'll receive webhook notifications about the deletion
:::important Deleted sessions return 400
The k-ID API only ever returns `ACTIVE` as the session status. When a parent revokes access, the session is deleted, and the API returns 400 (not found). This is intentional: once a session is deleted, it should be treated as if it no longer exists. A deleted session is effectively "not found" because the player no longer has access to your game.
:::
### Detect deleted sessions
Via webhook (server-side):
```javascript
case "Session.Delete":
await deleteStoredSession(data.id);
await markSessionAsDeleted(data.id);
break;
```
Via API when fetching session (server-side):
```javascript
const response = await fetch(
`https://game-api.k-id.com/api/v1/session/get?sessionId=${sessionId}`,
{ headers: { "Authorization": `Bearer ${process.env.KID_API_KEY}` } }
);
if (response.status === 400) {
const error = await response.json();
if (error.error === "NOT_FOUND") {
await deleteStoredSession(sessionId);
return res.json({ sessionDeleted: true });
}
}
```
:::tip Best practice for handling 400
A 400 response with `NOT_FOUND` error when querying sessions could mean:
- The session was never created
- The session was deleted
- The session ID is invalid
Your application should handle all these cases the same way: treat it as indicating the player doesn't have access, regardless of the underlying reason. Use webhooks to receive real-time notifications about session deletions rather than relying solely on polling.
:::
### Redirect to age gate
When your game client receives a `sessionDeleted: true` response from your server, restart the age gate flow:
```javascript
// Client-side handling
async function checkSession(sessionId) {
const response = await fetch(`/api/session/${sessionId}`);
const data = await response.json();
if (data.sessionDeleted || data.needsConsent) {
// Show explanation to the player
showDialog({
title: "Session Ended",
message: "Your session has ended. Please complete age verification to continue playing.",
buttons: [{
text: "Continue",
action: () => navigateToAgeGate()
}]
});
}
}
```
## Step 6: Provide upgrade paths for new permissions
Players might want to request access to features that require parental consent. The [`/session/upgrade`](/api/endpoints/upgrade-session) API enables this flow.
### Check if upgrade is possible
Before showing an upgrade prompt, check what kind of upgrade the permission needs. This logic runs client-side by using the session data your server returned:
```javascript
// Client-side check using session data from your server
function getUpgradeType(session, permissionName) {
const permission = session.permissions.find(p => p.name === permissionName);
if (!permission || permission.enabled) return null;
if (permission.managedBy === "PROHIBITED") {
// Can't be upgraded: jurisdiction ban or player is too young
return null;
}
if (permission.managedBy === "GUARDIAN") {
// Requires parental consent: will return CHALLENGE_SESSION_UPGRADE
return "guardian-consent";
}
if (permission.managedBy === "PLAYER" && permission.verifiedAgeThreshold) {
// Player-managed but needs age verification: will return CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE
return "age-assurance";
}
if (permission.managedBy === "PLAYER") {
// No challenge needed: enabled immediately on upgrade call
return "player-direct";
}
return null;
}
```
:::info PROHIBITED permissions
A permission is `PROHIBITED` when either it's banned in the player's jurisdiction or the player's age is below the `verifiedAgeThreshold` and can't be satisfied at their current age. For example, loot box permissions in Brazil require a verified age of 18. Players below this threshold see the permission as `PROHIBITED` and it can't be enabled through any upgrade flow. For more information, see [High-risk permissions and age assurance](/concepts/access-features-consent/permissions#high-risk-permissions-and-age-assurance).
:::
### Request a permission upgrade
When the player requests a feature, your game client calls your server, which then calls the k-ID session upgrade API. The response depends on the permission type:
```javascript
// Server-side endpoint
app.post("/api/request-permission", async (req, res) => {
const { sessionId, permissionName, platformAgeSignal } = req.body;
const upgradeRequest = {
sessionId: sessionId,
requestedPermissions: [{ name: permissionName }]
};
// If a platform age signal is available (e.g., from Apple iOS or Google Play),
// include it to potentially satisfy age verification thresholds without
// requiring a separate verification challenge.
if (platformAgeSignal) {
upgradeRequest.platformAgeSignal = platformAgeSignal;
}
const response = await fetch(
"https://game-api.k-id.com/api/v1/session/upgrade",
{
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KID_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify(upgradeRequest)
}
);
const result = await response.json();
if (result.status === "PASS") {
// Permission enabled immediately: either player-managed with no threshold,
// or the platform age signal satisfied the verifiedAgeThreshold
await storeSession(sessionId, result.session);
return res.json({ success: true, session: result.session });
} else if (result.status === "CHALLENGE") {
const challengeType = result.challenge.type;
if (challengeType === "CHALLENGE_SESSION_UPGRADE") {
// Guardian-managed permission: parent must approve
return res.json({
success: false,
requiresParentConsent: true,
challenge: result.challenge
});
} else if (challengeType === "CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE") {
// verifiedAgeThreshold permission: player must verify their age through AgeKit+
// Direct the player (not the parent) to challenge.url
return res.json({
success: false,
requiresAgeAssurance: true,
challenge: result.challenge
});
}
}
});
```
### Option A: Use the session upgrade widget (recommended)
The session upgrade widget provides a complete, pre-built interface for parents to review and approve permission requests. This is the simplest approach if you're building a web-based game or can display an iframe.
:::important Guardian-managed permissions only
The session upgrade widget is for `CHALLENGE_SESSION_UPGRADE` challenges, where a parent needs to approve a `GUARDIAN`-managed permission. For `CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE` challenges (high-risk permissions with `verifiedAgeThreshold`), the player is directed to `challenge.url` to complete age verification through AgeKit+: the parent widget isn't used.
:::
:::warning Parent authentication required
The manage session permissions widget must only be hosted in a parent-authenticated session. The widget doesn't provide its own parent authentication, so it should never be presented directly to a minor. Always ensure that the widget is only displayed to authenticated parents or trusted adults.
:::
When a `CHALLENGE_SESSION_UPGRADE` is returned, generate a widget URL and display it to the parent:
```javascript
// Server-side endpoint to generate the widget URL
app.post("/api/permission-widget", async (req, res) => {
const { sessionId, parentEmail } = req.body;
const response = await fetch(
"https://game-api.k-id.com/api/v1/widget/generate-manage-session-permissions-url",
{
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KID_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
sessionId: sessionId,
email: parentEmail
})
}
);
const { url } = await response.json();
res.json({ widgetUrl: url });
});
```
Display the widget in an iframe on the client:
```html
```
The widget handles the entire consent flow, including parent verification and permission approval. Listen for the [`Widget.ExitReview`](/events/dom-events/event-structures/widget-exitreview) DOM event to know when the flow completes, then refresh the session from your server.
### Option B: Handle age assurance challenges
For `CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE`, the player (not the parent) must verify their age through AgeKit+. Direct the player to `challenge.url`:
```javascript
// Client-side: handle age assurance challenge
function handleAgeAssuranceChallenge(challenge) {
showDialog({
title: "Age Verification Required",
message: "To access this feature, you need to verify your age.",
buttons: [
{
text: "Verify Age",
action: () => {
// Open the AgeKit+ age assurance flow
// On completion, refresh the session to check if the permission is now enabled
window.open(challenge.url, "_blank");
}
},
{ text: "Not Now", action: "dismiss" }
]
});
}
```
After the player completes or dismisses the flow, refresh the session via your server to pick up any permission changes.
### Option C: Build a custom consent flow
If you need full control over the UI or can't use iframes, build a custom consent flow for `CHALLENGE_SESSION_UPGRADE`. Present options for the parent to provide consent:
```javascript
// Client-side UI
function showConsentRequest(challenge) {
showDialog({
title: "Ask a Parent",
message: "A parent needs to approve this feature. How would you like to reach them?",
options: [
{
label: "Send an email",
action: () => showEmailInput(challenge.challengeId)
},
{
label: "Show QR code",
action: () => showQRCode(challenge.url)
},
{
label: "Show code",
sublabel: `Go to asktoplay.com and enter: ${challenge.oneTimePassword}`,
action: () => showCodeDisplay(challenge.oneTimePassword)
}
]
});
}
```
For more details on handling the custom consent flow, see the [Custom age gate quick start](/get-started/quickstart-guides/custom-age-gate).
## Best practices
### Performance recommendations
- **Prefer webhooks** over polling when possible because they're more efficient and provide real-time updates
- **Use the `etag` parameter** when calling [`/session/get`](/api/endpoints/get-session) to avoid unnecessary data transfer
- **Cache sessions locally** and only fetch updates when needed
- **Don't poll frequently**: if you must poll, wait at least 30 seconds between requests during gameplay, or only check on game start
### Handling edge cases
- **Offline players**: Cache the session and apply cached permissions. Check for updates when connectivity returns.
- **Multiple devices**: If players can use multiple devices, store sessions in cloud storage associated with their account to ensure consistency.
- **Age-up during gameplay**: Consider periodic session refreshes for long gameplay sessions to catch birthday-triggered changes.
## What's next?
Now that you've implemented session and permission management, explore these resources:
- **[Sessions](/concepts/access-features-consent/sessions)**: Deep dive into session lifecycle and structure
- **[Challenges](/concepts/access-features-consent/challenges)**: Complete guide to consent challenges, status handling, and best practices
- **[Permissions](/concepts/access-features-consent/permissions)**: Detailed information about permission types and management
- **[High-risk permissions and age assurance](/concepts/access-features-consent/permissions#high-risk-permissions-and-age-assurance)**: How `verifiedAgeThreshold` permissions work and how to unlock them
- **[Age assurance for high-risk features](/cdk/age-assurance)**: Full recovery flow for players below a threshold
- **[Webhooks](/webhooks)**: Complete guide to webhook implementation and validation
- **[`Session.ChangePermissions`](/events/webhooks/event-types/session-changepermissions)**: Webhook event reference
- **[Best practices](/cdk/best-practices)**: Additional implementation guidance
---
// File: get-started/quickstart-guides/vpc
# Verifiable Parental Consent (VPC)
This guide walks you through implementing the VPC widget in just a few steps, allowing you to quickly meet regulatory requirements while providing a smooth user experience for both children and their parents.
:::tip Integrate with AI coding agents
Official [Agent Skills](/get-started/agent-skills) cover VPC, sessions, webhooks, and other CDK flows for AI-assisted integration.
:::
:::tip Try the k-ID Dev Explorer
Use the [k-ID Dev Explorer](https://github.com/kidentify/k-id-dev-explorer), an open source developer sandbox, to test VPC flows and view all traffic in an event log. You can also use it as a starting point for your own VPC implementation.
:::
The **End-to-end Widget** is a comprehensive solution that handles the complete compliance flow in a single interface, covering age gate, VPC, data notices, permissions, and preferences all in one seamless experience. This widget can be used by parents either on the child's device or on their own device, providing maximum flexibility for the consent process.
:::info Mobile apps
This guide's examples are written for **web** implementations. The end-to-end widget is fully supported on mobile too: display the widget URL in a system browser surface and receive the result through a `redirectUrl` callback, as described in the [mobile apps guide](/get-started/quickstart-guides/mobile-apps). For the age gate and consent steps on mobile, building the UX natively with the [custom workflow](/cdk/custom-workflow) and the [CDK UX guidelines](/cdk/ux-guidelines) typically delivers the most seamless player experience.
:::
## What's an age gate?
An **Age Gate** is a mechanism used to collect and verify a user's age before allowing access to age-restricted content, features, or services. Age gates are required by regulations in many jurisdictions to ensure compliance with laws governing digital content access for minors.
Age gates serve several important purposes:
- **Regulatory Compliance**: Meet legal requirements for verification in different jurisdictions
- **Content Protection**: Prevent minors from accessing inappropriate content
- **Data Privacy**: Ensure proper handling of children's data according to regulations such as COPPA, GDPR-K, and others
- **Parental Control**: Enable parents to make informed decisions about their children's digital access
:::tip Jurisdiction-Aware Intelligence
By providing the user's jurisdiction, the End-to-end Widget automatically determines the best methods for collecting their age (if not already provided) and intelligently determines whether VPC is necessary based on that jurisdiction's specific regulations. This ensures compliance without requiring you to implement complex jurisdictional logic.
:::
## What's Verifiable Parental Consent (VPC)?
Verifiable Parental Consent (VPC) is a regulatory requirement that ensures parents or trusted adults can provide informed consent for children to access digital content, services, or features. When a child attempts to access age-restricted content, the system creates a **Challenge** that requires parental approval before access can be granted.
The VPC flow typically involves:
1. **Age Collection**: Determining the child's age through appropriate methods
2. **Challenge Creation**: Creating a consent challenge when parental approval is required
3. **Parental Notification**: Notifying parents through various channels (email, QR code)
4. **Consent Processing**: Parents review and approve/deny the request
5. **Session Management**: Creating or updating the child's permissions based on consent results
## Prerequisites
Before you begin, you'll need:
1. **A k-ID Product**: [Create and configure your product](/compliance-studio/creating-product) in the [k-ID Compliance Studio](https://portal.k-id.com/)
2. **API Key**: Generate your API key from the Developer Settings page of your product in the [Compliance Studio](/compliance-studio/creating-product)
3. **Webhook Endpoint** (optional but recommended): Set up a secure HTTPS endpoint to receive challenge and session events. For more detail, see [Webhooks](/webhooks).
## Step 1: Initiate the VPC flow
Call the [`/widget/generate-e2e-url`](/api/endpoints/generate-e-2-eurl) API to create an end-to-end widget URL that handles the complete VPC flow. This returns a unique URL for users to complete the age collection and parental consent process.
:::tip
Use the [API reference](/api/overview) with your API key to quickly generate your VPC widget URL.
:::
:::warning Important
For your implementation, this should be a server-to-server call to protect your API key from being exposed in client-side code.
:::
### Example request
```json
POST /api/v1/widget/generate-e2e-url
Content-Type: application/json
Authorization: Bearer your-api-key
{
"jurisdiction": "US-CA"
}
```
### Configuration flags
The optional `flags` parameter allows you to customize which parts of the flow to skip:
- `skipDataNotices`: Skip data notices and consent collection
- `skipVerification`: Skip verification step
- `skipPermissions`: Skip permission management
- `skipPreferences`: Skip preference settings
### Pass a platform age signal (optional)
If your game already has age data from the platform (Apple iOS, Google Play, Xbox, Meta Horizon, or a prior k-ID verification), include it as `platformAgeSignal` in the request body. The widget forwards the signal to the underlying age-gate check so it can skip the age gate when a verified signal indicates an adult, satisfy verified-age permissions without an extra verification step, and detect conflicts between the signal and the player's self-reported age.
```json
POST /api/v1/widget/generate-e2e-url
Content-Type: application/json
Authorization: Bearer your-api-key
{
"jurisdiction": "US-CA",
"platformAgeSignal": {
"name": "apple-ios",
"ageLow": 18,
"ageHigh": 25,
"declarationType": "governmentIDChecked"
}
}
```
For the supported platforms and field shapes, see [Platform age signals](/cdk/age-signals/overview).
### Example response
```json
{
"id": "7854909b-9124-4bed-9282-24b44c4a3c97",
"url": "https://family.k-id.com/widget?token=eyJhbGciOiJFUzM4NCIs..."
}
```
## Step 2: Display the VPC widget
For web, embed the URL in an iframe:

```html
```
For mobile (embedded browser), game engines (in-game browser), and consoles (QR-to-mobile handoff), see [Presenting the widget URL](/cdk/embedded-flow#present-widget) for the full surface options and the `options.redirectUrl` callback pattern. The available methods inside the widget adapt to jurisdictional requirements regardless of surface.
The iframe presents users with multiple verification methods such as:
- **AgeKey**: A reusable and anonymous age-proof generated after an initial verification process.
- **Facial Age Estimation**: Privacy-preserving age estimation using a device's camera
- **ID Document Verification**: Government-issued ID verification
The specific methods available depend on the jurisdiction and your product configuration in the [Compliance Studio](/compliance-studio/creating-product). For more detail, see [Verification Methods](/concepts/verification-methods)
The widget presents users with:
- **Age Collection**: Jurisdiction-appropriate age collection methods
- **Data Notices**: Data notices to accept, depending on the product's configuration in the [Compliance Studio](/compliance-studio/product-notices)
- **Permissions**: Permissions to manage, depending on the product's configuration in the [Compliance Studio](/compliance-studio/product-api-configuration#permissions)
- **Parental Consent Challenge**: If the user is determined to be a minor, a challenge is created for trusted adult approval
The specific flow depends on the jurisdiction and your product's configuration in the Compliance Studio.
## Step 3: Handle challenge and session events
When the age gate flow completes, a **session is always created** to store the player's permissions and age status. A **challenge is only created when parental consent is required** (when the user's age requires Verifiable Parental Consent (VPC) in their jurisdiction).
**Implementations should use a combination of client-side and server-side methods**: client-side events are best for controlling UI elements, while for data integrity, the actual results should come from either a webhook or a call to [`/challenge/get-status`](/api/endpoints/get-challenge-status).
### Client-side (DOM events or `redirectUrl` callback)
:::note Where DOM events reach you
`postMessage` events only reach hosts where your JavaScript can listen (iframe, pop-up, `WebView` / `WKWebView` with a JS bridge). For other hosts, use the `redirectUrl` callback. See [Where DOM events reach you](/cdk/embedded-flow#handling-events) for the full scope.
:::
When the widget is presented in a context where your JavaScript can listen for messages (iframe, pop-up, or a WebView with a bridge), listen for the [`Widget.AgeGate.Result`](/events/dom-events/event-structures/widget-agegate-result) event. This event always includes a `sessionId` when the flow completes successfully. If parental consent was required, the event also includes a `challengeId`. For detailed information about challenge events, see [`Widget.AgeGate.Challenge`](/events/dom-events/event-structures/widget-agegate-challenge).
:::important Closing the UI
Listen for the [`Widget.ExitReview`](/events/dom-events/event-structures/widget-exitreview) event to determine when to close the VPC widget UI. This event is emitted when the user clicks the 'Done' button, indicating the flow is complete and the iframe should be closed or hidden.
:::
```javascript
const handleMessage = (event) => {
const message = event.data;
if (message.eventType === "Widget.AgeGate.Result") {
if (message.data.status === "PASS") {
// Age gate completed - session is always created
const sessionId = message.data.sessionId;
// If challengeId is present, parental consent was required
if (message.data.challengeId) {
console.log("Consent approved:", sessionId);
} else {
console.log("Session created (no consent required):", sessionId);
}
updateUI(sessionId);
}
}
// Handle challenge-specific events if needed
if (message.eventType === "Widget.AgeGate.Challenge") {
if (message.data.status === "FAIL") {
// Parent denied consent - update UI immediately
console.log("Consent denied");
updateUI();
}
}
if (message.eventType === "Widget.ExitReview") {
// Close the VPC widget UI when the user clicks 'Done'
closeVPCWidget();
}
};
window.addEventListener("message", handleMessage);
```
### Server-side (webhooks, API calls)
Use webhooks or API calls for data integrity and reliable state management. For data integrity, always verify results with events from webhooks or by calling [`/challenge/get-status`](/api/endpoints/get-challenge-status) rather than relying solely on DOM Events.
#### Webhooks
The [`Challenge.StateChange`](/events/webhooks/event-types/challenge-statechange) webhook event is only sent when a challenge exists (that is, when parental consent was required). If the user's age doesn't require consent, no challenge is created and no `Challenge.StateChange` event is sent, but a session is still created.
For detailed information about webhook event structures, see [`Challenge.StateChange`](/events/webhooks/event-types/challenge-statechange) and [`Session.ChangePermissions`](/events/webhooks/event-types/session-changepermissions).
[Configure your webhook endpoint](/webhooks) to receive [`Challenge.StateChange`](/events/webhooks/event-types/challenge-statechange) and [`Session.ChangePermissions`](/events/webhooks/event-types/session-changepermissions) events:
**`Challenge.StateChange` event:**
```json
{
"eventType": "Challenge.StateChange",
"data": {
"id": "683409f1-2930-4132-89ad-827462eed9af",
"productId": 42,
"status": "PASS",
"sessionId": "0ad1641f-c154-4c2-8bb2-74dbd0de7723",
"approverEmail": "parent@example.com",
"kuid": "123456"
}
}
```
**`Session.ChangePermissions` event:**
```json
{
"eventType": "Session.ChangePermissions",
"data": {
"id": "78c299b2-5c33-4bde-84fe-8fc950fc7a96",
"productId": 42
}
}
```
#### API calls
Query the challenge status with the challenge ID with [`/challenge/get-status`](/api/endpoints/get-challenge-status):
```json
GET /api/v1/challenge/get-status?id=683409f1-2930-4132-89ad-827462eed9af
Response:
{
"id": "683409f1-2930-4132-89ad-827462eed9af",
"status": "PASS",
"sessionId": "0ad1641f-c154-4c2-8bb2-74dbd0de7723"
}
```
## Webhook configuration
For the VPC widget, ensure your webhook endpoint is configured to receive:
- **[`Challenge.StateChange`](/events/webhooks/event-types/challenge-statechange)**: Notifies when parental consent is approved or denied
- **[`Session.ChangePermissions`](/events/webhooks/event-types/session-changepermissions)**: Notifies when session permissions are modified by parents
These events are essential for maintaining proper access control and ensuring compliance with parental consent requirements.
## What's next?
Now that you've implemented the VPC widget, explore these resources to enhance your integration:
- **[API Reference Documentation](/api/overview)**: Detailed documentation of all widget and challenge APIs
- **[Webhooks Setup](/webhooks)**: Implement robust webhook handling for production systems
- **[Best Practices](/agekit-plus/best-practices)**: Implement best practices to ensure security and a reliable user experience
- **[Pre-launch Checklist](/agekit-plus/prelaunch-checklist)**: Review requirements before going live
With k-ID's VPC widget, you can quickly achieve compliance with parental consent regulations while providing families with a smooth, privacy-focused experience.