Managing sessions and permissions
Once a player and their parent have completed the consent flow (whether through the VPC widget or a 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.
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:
- A completed consent flow: Players should already have active sessions from completing the VPC quick start or Custom age gate quick start
- A k-ID Product: Create and configure your product in the k-ID Compliance Studio
- API Key: Generate your API key from the Developer Settings page of your product in the Compliance Studio
- Webhook endpoint (recommended): Set up a secure HTTPS endpoint to receive session events. For more detail, see 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 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:
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:
Step 2: Implement webhook-based detection
If your game has a server, implement webhook-based detection for real-time permission updates.
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, configure your webhook URL under Developer Settings for your product. Ensure your endpoint can receive:
Session.ChangePermissions: Fired when a parent changes permissionsSession.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:
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:
// 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.
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):
// 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:
// 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:
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;
}
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, 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.
For detailed design recommendations on communicating permission changes, displaying disabled features, and handling permission requests, see the 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):
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):
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");
}
}
}
}
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, 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:
- 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
- All incomplete challenges are failed - Any pending challenges for that player are automatically set to
FAIL - Webhook events are sent - You'll receive webhook notifications about the deletion
The k-ID API only returns ACTIVE or HOLD as valid session statuses. 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):
case "Session.Delete":
await deleteStoredSession(data.id);
await markSessionAsDeleted(data.id);
break;
Via API when fetching session (server-side):
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 });
}
}
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:
// 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 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:
// 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;
}
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.
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:
// 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.
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.
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:
// 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:
<iframe
id="permission-widget"
src="WIDGET_URL"
width="100%"
height="600"
allow="camera;payment;publickey-credentials-get;publickey-credentials-create"
frameborder="0">
</iframe>
The widget handles the entire consent flow, including parent verification and permission approval. Listen for the 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:
// 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:
// 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.
Best practices
Performance recommendations
- Prefer webhooks over polling when possible because they're more efficient and provide real-time updates
- Use the
etagparameter when calling/session/getto 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: Deep dive into session lifecycle and structure
- Challenges: Complete guide to consent challenges, status handling, and best practices
- Permissions: Detailed information about permission types and management
- High-risk permissions and age assurance: How
verifiedAgeThresholdpermissions work and how to unlock them - Age assurance for high-risk features: Full recovery flow for players below a threshold
- Webhooks: Complete guide to webhook implementation and validation
Session.ChangePermissions: Webhook event reference- Best practices: Additional implementation guidance