From k-id-skills
Implements the parental-consent flow for k-ID minors — the GUARDIAN-managed path where a trusted adult grants permissions for a child (COPPA parental consent, EU GDPR-Kids verifiable parental consent, UK AADC equivalent). Supports both approaches: Pattern A (default) builds a fully custom consent screen (QR + OTP + email + direct link) and calls /challenge/send-email and /challenge/generate-otp directly with top-level polling — a brand-fit, inline experience; Pattern B is a fast-path fallback using the end-to-end widget (/widget/generate-e2e-url — iframe handles age gate, consent, data notices, permissions) or the manage-permissions widget (/widget/generate-manage-session-permissions-url — post-gate consent only). Use when building the consent screen, wiring challenge send / status endpoints, handling `challengeId` without a session, or diagnosing why approvals don't reach the app. Not for age assurance or threshold verification (k-id-age-verification), not for the age gate itself (k-id-age-gate).
How this skill is triggered — by the user, by Claude, or both
Slash command
/k-id-skills:k-id-consent-and-challengesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
When the age gate concludes the user is a minor in a jurisdiction that
When the age gate concludes the user is a minor in a jurisdiction that
requires verifiable parental consent (VPC) — COPPA in the US, GDPR-Kids
in the EU, UK AADC, and similar regimes — k-ID returns a challenge.
A guardian approves it out-of-band, and your app must detect the
approval and continue. This skill covers the canonical consent screen,
the challenge endpoints, and the polling lifecycle that keeps it
reliable.
This skill is the GUARDIAN-managed path only — a trusted adult is
approving permissions on behalf of a child. It is not the path for
age assurance, threshold verification, or any scenario where the user
themselves must prove their age (UK Online Safety Act 18+ features,
Brazil ECA Digital loot-boxes, Australia social media minimum age).
Those flows live in k-id-age-verification.
Use this skill when the user is:
/challenge/send-email, /challenge/generate-otp,
/challenge/get, or /challenge/get-status.challengeId that arrives without a sessionId.There are two ways to run the consent flow. Pick one per product.
Build the QR + OTP + email + direct link screen by hand, call the underlying challenge endpoints, and run polling at the top level of the app (outside the consent modal). The result is a parental-consent flow that matches the product's visual identity exactly, renders inline (no iframe), and stays consistent with the age gate and permission UI built the same way.
Pattern A wins when: the product wants a polished, brand-fit consent experience that feels native to the rest of the app; has already built a custom age gate; or targets platforms where iframes aren't practical (Unity WebGL, consoles, native desktop). This is the default path for production integrations.
If the integration must be small, simple, and as fast as possible to
ship, use the end-to-end widget: one iframe covers age gate +
parental consent + data notices + permissions + parental preferences.
Parental consent is initiated automatically when the age entered
triggers it, and the widget handles QR, OTP, email, and direct link
natively. Call
POST /widget/generate-e2e-url.
If the product already has a session and only needs consent for a
specific permission upgrade, use the manage-permissions widget:
POST /widget/generate-manage-session-permissions-url.
In both cases the product listens for DOM events
(Widget.AgeGate.Result, Widget.DataNotices.ConsentApproved,
Widget.ExitReview) or handles the redirect callback. No polling is
required. The widget surfaces the final state; the product just needs
to call /session/get after the event to materialize the session
client-side.
Pattern B wins when: the integration must be small, simple, and fast — proofs of concept, internal tools, early-stage games — or the product can accept the iframe look in exchange for minimum code and minimum compliance surface area.
The rest of this skill covers Pattern A step by step. Pattern B users can stop here and jump to the relevant widget event documentation.
Parents approve in whichever lane is easiest for them — present all four in parallel. Offering only one lane (typically email) measurably lowers approval rates.
challenge.url.asktoplay.com. Pull the code from
challenge.oneTimePassword (returned by /challenge/get and
/challenge/generate-otp)./challenge/send-email.challenge.url in the SAME window
(or iframe — never window.open).OTP copy must say "Enter at asktoplay.com" — not family.k-id.com.
asktoplay.com is the memorable, kid-friendly domain that redirects
to Family Connect and is much easier to read aloud.
| Lane | Endpoint | Reads |
|---|---|---|
| QR / Direct link | Already in challenge.url from the age-gate response | challenge.url |
| OTP | POST /challenge/generate-otp (or GET /challenge/get) | challenge.oneTimePassword |
POST /challenge/send-email | body: { challengeId, email } |
The endpoint is /challenge/send-email, NOT /challenge/send-consent-email.
send-consent-email returns 404. For full shapes see
docs.k-id.com/api/endpoints/send-email
and sibling endpoints under "Challenges".
Mount the polling loop at the top level of the signed-in app — above any modal, gated control, or route component. The modal can close for many reasons (user dismissal, re-render on approval, navigation) and polling that is bound to the modal's lifecycle stops with it, leaving the approval undetected.
The top-level loop:
challengeId
(including a minor in limited-access mode, i.e. a pending
challenge with challenge.childLiteAccessEnabled: true).visibilitychange and focus events.useEffect(() => {
if (!pendingChallengeId) return;
let cancelled = false;
const tick = async () => {
if (cancelled) return;
const status = await getChallengeStatus(pendingChallengeId);
if (status.state === 'PASS' && !cancelled) {
await handleChallengePass(status);
}
};
const id = setInterval(tick, 10_000);
const wake = () => tick();
document.addEventListener('visibilitychange', wake);
window.addEventListener('focus', wake);
void tick();
return () => {
cancelled = true;
clearInterval(id);
document.removeEventListener('visibilitychange', wake);
window.removeEventListener('focus', wake);
};
}, [pendingChallengeId]);
PASS does not always include a sessionId. Code must handle
both cases:
if (status.sessionId) {
// Swap whatever placeholder sessionId the app was holding
// (for the pending-challenge / limited-access state) for the
// real one returned by /challenge/get-status, then hydrate.
setSessionId(status.sessionId);
await fetchSession();
} else {
// Re-run /age-gate/check with the age the user originally entered;
// the minor now has an approved challenge, so a session will mint.
await checkAge(enteredAge, jurisdiction);
}
This is why k-id-age-gate insists on
persisting enteredAge — without it this recovery path is impossible.
After swapping in the sessionId from /challenge/get-status,
GET /session/get may return 404 for 1–2 seconds while the session
propagates. Retry up to 3 times with 1-second delays. A single 404
is not a failure.
When the product has opted into limited-access mode in Compliance
Studio AND the challenge response has
challenge.childLiteAccessEnabled: true, the minor can continue
into the app with a reduced permission set before the parent has
approved. This is a valid end state, not an error. Render the main
app UI with the reduced permissions (or with pendingChallenge
lock labels on gated controls) and keep polling. See
k-id-sessions-and-permissions for the lock labels this state
uses ("Waiting for parent", "Get parent permission"). If the
product has NOT opted into limited-access mode, or the challenge
response has childLiteAccessEnabled: false, the minor must wait
on the consent screen until consent completes.
verifiedAgeThreshold
permissions — those are user-self-verified and live in
k-id-age-verification./challenge/send-email, not /challenge/send-consent-email.
The second form looks more descriptive but does not exist. 404.PASS does not always include sessionId. When it does,
swap it in and call /session/get. When it doesn't, re-call
/age-gate/check with the stored enteredAge so a session is
minted against the now-approved challenge./session/get after swapping in the real sessionId. 1–2s
propagation delay is normal. A single 404 is not an error.asktoplay.com, not family.k-id.com. Parents and
kids read the OTP aloud — asktoplay.com is shorter and easier.window.open or target="_blank". Use
an in-page iframe or a same-window navigation. New-tab flows lose
the referrer and break approval attribution on some browsers.childLiteAccessEnabled: true) is a valid
end state, not an error. Do not block the app with a "waiting
for parent" spinner — render it with a reduced permission set.challengeId explicitly in app state until /challenge/get-status
returns a real sessionId.visibilitychange, focus.<input type="email">, no strict regex —
/challenge/send-email validates.Run the checklist for the pattern you built.
Confirm all of the following end-to-end with a test parent account:
Widget.AgeGate.Result (or Widget.ExitReview) with the final
sessionId; a follow-up /session/get returns full permissions.challenge → you arrived here correctly.session → go to k-id-sessions-and-permissions.k-id-age-verification.k-id-webhooks.k-id-server-trust-boundary.Canonical references:
POST /widget/generate-e2e-urlPOST /widget/generate-manage-session-permissions-urlPOST /challenge/send-emailPOST /challenge/generate-otpGET /challenge/getGET /challenge/get-statusparentalconsent.granted webhookchallenge.statechange webhooknpx claudepluginhub kidentify/skills --plugin neimo-skillsGuides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.