Skip to main content

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.

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 or Custom age gate quick start
  2. A k-ID Product: Create and configure your product in the k-ID Compliance Studio
  3. API Key: Generate your API key from the Developer Settings page of your product in the Compliance Studio
  4. Webhook endpoint (recommended): Set up a secure HTTPS endpoint to receive 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 typeDescriptionDetection method
Parent modifies permissionsA parent uses Family Connect to enable or disable featuresWebhook or session comparison
Player ages upA 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 consentSession comparison only
Session deletedA 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.

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.

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, configure your webhook URL under Developer Settings for your product. Ensure your endpoint can receive:

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.

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):

// 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;
}
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, 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.

UX Guidelines

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");
}
}
}
}
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, 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
Deleted sessions return 400

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 });
}
}
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:

// 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
});
}
});

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.

Guardian-managed sessions only

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.

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 etag parameter when calling /session/get 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: