Verified Parent Linking (VPL)
Verified parent linking (VPL) lets a player invite their own parent into your product. The player sends the invite, the parent verifies their identity, and from then on the parent can see and set that player's controls. Nothing in your product is blocked while the invite is outstanding, and nothing breaks if the parent never accepts.
This guide walks through the whole flow: creating the session, sending the invite, tracking its state, canceling it, and handling an unlink from either side.
Verified parent linking (VPL) explains why VPL is a link rather than a gate, and why it's mutually exclusive with VPC. The eligibility rules described there decide whether the calls in this guide succeed.
Prerequisites
Before you begin, you'll need:
- A k-ID product: Create and configure your product in Compliance Studio.
- An API key: Generate one from the Developer Settings page of your product. All calls in this guide are server to server. See Authentication.
- A webhook endpoint: An HTTPS endpoint to receive
Challenge.StateChangeandSession.Unlink. See Webhooks. - A subscription to the events you handle: An endpoint receives only the event types it's subscribed to, and k-ID drops the rest without a delivery attempt or an error. On the product's Developer Settings page in Compliance Studio, select
Challenge.StateChangeandSession.Unlinkfor your endpoint. - VPL enabled for your organization: Please contact k-ID to have this capability turned on for your organization.
All examples use the live base URL https://game-api.k-id.com/api/v1, Content-Type: application/json, and an Authorization: Bearer <api-key> header. In test mode, use https://game-api.test.k-id.com/api/v1.
Step 1: Create the player's session
Call POST /age-gate/check to create a session for the player.
POST /age-gate/check
{
"jurisdiction": "US-CA",
"age": 17
}
Pass at most one of age, dateOfBirth, or kuid (for a returning player whose kuid you cached). Passing more than one returns a 400. See /age-gate/check for the other age signals the endpoint accepts.
A successful response carries the session:
{
"status": "PASS",
"session": {
"sessionId": "b1a6482d-5242-4b4a-aa88-3fa52595a672",
"status": "ACTIVE",
"managedBy": "PLAYER",
"hasApproverEmail": false
}
}
Store the sessionId against your own user record. Every call in this guide is keyed on it, and it stays the same across link, unlink, and relink.
If the status is CHALLENGE instead of PASS, the player is below the access age for their jurisdiction and needs parental consent. Route them through VPC and don't call the invite endpoint, because no session exists to invite a parent to.
A session that came back PASS can still be ineligible for VPL if it carries a GUARDIAN-managed permission. Check permissions[] on the session before you show the invite option, or handle the FEATURE_DISABLED error in Step 3.
Step 2: Show the invite option
Put the option wherever it fits your product, such as settings, the player's profile, or onboarding. When the player taps it, collect the parent's email address.
Because VPL is opt-in and non-blocking, this is a normal piece of product surface rather than an interstitial. The player can ignore it indefinitely.
Step 3: Send the invite
Call POST /challenge/invite-parent.
POST /challenge/invite-parent
{
"sessionId": "b1a6482d-5242-4b4a-aa88-3fa52595a672",
"parentEmail": "parent@example.com",
"playerName": "Alex",
"locale": "en"
}
| Field | Required | Notes |
|---|---|---|
sessionId | yes | The session from Step 1 |
parentEmail | yes | Where the invite is sent |
playerName | no | The player's display name, 1 to 63 characters. Shown to the parent in the invite email. Omit it and the parent sees a generic placeholder |
locale | no | IETF BCP 47 tag for the invite email. Omit it and k-ID uses the parent's own stored language if it already knows them, then your product's primary language, then English |
{
"status": "CHALLENGE",
"challenge": {
"challengeId": "ae6d4729-af32-42ea-8ef2-ff46c7664802",
"type": "CHALLENGE_PARENT_INVITE"
}
}
One call creates the challenge, binds it to the session, and sends the email. There's no follow-up call to trigger the email, and /challenge/send-email rejects parent-invite challenges.
Store the challengeId next to the session. It identifies this invite in the webhook, in the status endpoint, and in the cancel call.
While you hold a challengeId for a pending invite, avoid sending another invite to a different email for the same session.
The link in the invite email is valid for 14 days in live mode, and for 7 minutes in test mode so you can exercise expiry without waiting.
Errors to handle
Errors return HTTP 400 with the code in the response body's error field.
| Code | When | What to do |
|---|---|---|
FEATURE_DISABLED | Verified parent linking is not enabled. VPL is off for your organization | Contact k-ID |
FEATURE_DISABLED | Verified parent linking is not available for this player. The player is below the access age, or the session carries a GUARDIAN-managed permission | Fall back to VPC. Don't show the invite option for this session |
NOT_FOUND | The session doesn't exist | Recheck the sessionId you stored |
INVALID_INPUT | The session's jurisdiction isn't configured on your product | Fix the product configuration in Compliance Studio |
INVALID_EMAIL | Session is not eligible for a new invite. The session already has a linked parent | Show the current link state instead |
Step 4: Wait, without blocking
Keep your product fully available while the invite is outstanding. The player already passed the age gate, and nothing about the invite changes what they can do.
When the parent completes the flow, k-ID fires Challenge.StateChange to your webhook endpoint:
{
"eventType": "Challenge.StateChange",
"data": {
"id": "ae6d4729-af32-42ea-8ef2-ff46c7664802",
"productId": 12345,
"type": "CHALLENGE_PARENT_INVITE",
"status": "PASS",
"sessionId": "b1a6482d-5242-4b4a-aa88-3fa52595a672",
"approverEmail": "parent@example.com",
"kuid": "12b9fa0e-6d6d-4903-a1fc-f2233027b71d"
}
}
On receipt:
- Match
data.idanddata.sessionIdback to your stored player record. - Cache the
kuid. This is what tells you that a parent is linked to this session. - Update your UI to show that a parent is connected.
The same event fires with status: FAIL when the invite is canceled, but a FAIL payload carries no sessionId. Match it on data.id against the challengeId you stored, not on data.sessionId, or your handler drops every cancel. Treat it as no parent being connected. The player can send another invite.
If the parent never accepts, no further event arrives and nothing changes.
Step 5: Check the current state on-demand
The webhook is the source of truth for transitions, but you'll often need the current state without waiting for one, such as when the player reopens your app.
Is a parent linked now?
Call GET /session/get with the sessionId and read two fields:
hasApproverEmailistruewhile a parent is linked, andfalseotherwise. This is the signal to branch on.kuididentifies the child profile the session is attached to.
What happened to the last invite?
Call GET /challenge/get-status with the stored challengeId.
| Status | Meaning | Suggested UI |
|---|---|---|
PENDING | The invite exists and the email was sent, but the parent hasn't opened it | Show the invite as pending, naming the parentEmail |
IN_PROGRESS | The parent opened the link and started the flow | Show the invite as pending, naming the parentEmail |
PASS | The parent finished, and the session is linked | Show the parent as connected |
FAIL | The invite was canceled or otherwise ended | Show no parent as connected, and offer another invite |
Putting it together
When you render the player's profile:
- Call
/session/get. IfhasApproverEmailistrue, show "Parent connected" and offer the unlink option from Step 7. - Otherwise call
/challenge/get-statuswith your most recentchallengeId:PENDINGorIN_PROGRESS: show the invite as pending and offer cancel.PASSwhile/session/getstill shows no parent: a brief race. Treat it as pending and check again shortly.FAIL, or no storedchallengeId: show the invite option.
Step 6: Cancel a pending invite
If the player wants to revoke an invite while the parent is still mid-verification, call POST /challenge/cancel-invite.
POST /challenge/cancel-invite
{
"challengeId": "ae6d4729-af32-42ea-8ef2-ff46c7664802"
}
{ "cancelled": true }
The challenge moves to FAIL with failureReason: "cancelled-by-initiator", you receive a Challenge.StateChange with status: FAIL, and a parent already looking at the verification widget sees the canceled state.
Canceling an already-canceled invite is safe and returns cancelled: true again.
| Code | When |
|---|---|
NOT_FOUND | The challenge doesn't exist for this product |
INVALID_INPUT | Invalid challenge type. Only parent-invite challenges can be canceled here |
INVALID_INPUT | Invite already accepted. The parent finished before the cancel arrived |
FEATURE_DISABLED | VPL is off for your organization |
After a FAIL, the session accepts a fresh invite right away.
Step 7: Handle an unlink
Either side can end the relationship at any time. The parent can unlink from Family Connect, and you can offer the player an unlink option in your product.
An unlink returns the session to its unattached state: the kuid and the parent link are cleared, and the allowances and controls the parent set are removed. The session itself stays ACTIVE, and the player's permissions and pushed data is left intact. The player carries on with the same sessionId.
Unlink from your product
POST /session/unlink-parent
{
"sessionId": "b1a6482d-5242-4b4a-aa88-3fa52595a672",
"locale": "en"
}
{
"unlinked": true,
"unlinkedAt": "2026-04-20T18:42:11Z"
}
unlinked is true when this call, or a previous one, ended a parent link. It's false when the session never had a linked parent, and unlinkedAt is then omitted. locale sets the language of the notification email k-ID sends the parent to tell them the link ended. Omit it and k-ID uses the parent's own stored language if it already knows them, then your product's primary language, then English.
The call is idempotent on sessionId. A second call after the link is already gone returns 200 with the original unlinkedAt, not the time of the repeat call.
| Code | When |
|---|---|
NOT_FOUND | The session doesn't exist |
INVALID_INPUT | The session's jurisdiction isn't configured on your product |
FEATURE_DISABLED | Unlink is only available for VPL-linked sessions. The session isn't on the VPL path, or VPL is off for your organization |
The Session.Unlink webhook
k-ID fires Session.Unlink whenever a link ends, from either side.
{
"eventType": "Session.Unlink",
"data": {
"sessionId": "b1a6482d-5242-4b4a-aa88-3fa52595a672",
"productId": 12345,
"unlinkedBy": "player",
"unlinkedAt": "2026-04-20T18:42:11Z"
}
}
unlinkedBy is player when the unlink came through your API, and parent when the parent unlinked from Family Connect.
On receipt, stop showing the parent as connected. The player can send a new invite whenever they want.
What's next?
Now that verified parent linking is wired up, explore these resources to go deeper:
- Verified parent linking (VPL): The concept behind this guide: link versus gate, and why VPL and VPC are mutually exclusive
- Sessions: How the
sessionIdandkuidbehave across link, unlink, and relink Session.Unlink: The webhook that tells you when either side ends a link- Webhooks: Complete guide to webhook subscription, delivery, and signature validation
- Pre-launch checklist: Review requirements before going live