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, or someone might request access to additional features.
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 without parental consent | Session comparison only |
| 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") {
// Show "Ask parent" option
featureElement.setAttribute("data-disabled-reason", "parent");
featureElement.querySelector(".disabled-message").textContent =
"Ask a parent to enable this feature";
} else if (permission.managedBy === "PROHIBITED") {
// Feature is not available in this jurisdiction
featureElement.setAttribute("data-disabled-reason", "prohibited");
featureElement.querySelector(".disabled-message").textContent =
"This feature is not available";
} else if (permission.managedBy === "PLAYER") {
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 "Ask Parent" button, check if the permission can be upgraded. This logic can run client-side using the session data your server returned:
// Client-side check using session data from your server
function canRequestPermission(session, permissionName) {
const permission = session.permissions.find(p => p.name === permissionName);
if (!permission) return false;
// Can only request if currently disabled and managed by GUARDIAN
return !permission.enabled && permission.managedBy === "GUARDIAN";
}
Request a permission upgrade
When the player taps "Ask Parent," your game client calls your server, which then calls the k-ID session upgrade API:
// Server-side endpoint
app.post("/api/request-permission", async (req, res) => {
const { sessionId, permissionName } = req.body;
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({
sessionId: sessionId,
requestedPermissions: [{ name: permissionName }]
})
}
);
const result = await response.json();
if (result.status === "PASS") {
// Permission was enabled immediately (player can manage it)
// Update stored session
await storeSession(sessionId, result.session);
return res.json({ success: true, session: result.session });
} else if (result.status === "CHALLENGE") {
// Parent consent required
return res.json({
success: false,
requiresConsent: 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 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 without creating a challenge, so the widget isn't needed.
When a challenge 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 { challengeId, parentEmail } = req.body;
const response = await fetch(
"https://game-api.k-id.com/api/v1/widget/generate-session-upgrade-url",
{
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KID_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
challengeId: challengeId,
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: Build a custom consent flow
If you need full control over the UI or can't use iframes, build a custom consent flow. 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
- Permissions: More details on the upgrade flow
- Webhooks: Complete guide to webhook implementation and validation
Session.ChangePermissions: Webhook event reference- Best practices: Additional implementation guidance