From k-id-skills
Implements the k-ID age gate — entry point for any full sessioned k-ID integration (Shape A), covering every regime (COPPA, GDPR-Kids, UK AADC, UK OSA, Brazil ECA Digital, Australia Online Safety). Two approaches: Pattern A (default) builds a fully custom UI and calls /age-gate/check directly — best-looking, most brand-integrated, works on every platform (web, Unity WebGL, consoles, native); Pattern B is a fast-path fallback using the k-ID widget (/widget/generate-age-gate-url or /widget/generate-e2e-url — iframe handles age collection and auto-initiates consent). Covers null-initial age state, platform signals first, IP-based jurisdiction with timezone fallback, and /age-gate/check response shapes (session, challenge, unverified-adult). Use when adding or debugging the gate. Use EVEN IF the user says COPPA, OSA, ECA, or "age verification" but means the initial claimed-age check. Not for AgeKit+ (k-id-age-verification P1), post-gate / threshold verification (P2–3), or consent (k-id-consent-and-challenges).
How this skill is triggered — by the user, by Claude, or both
Slash command
/k-id-skills:k-id-age-gateThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
The age gate is the entry point to every **sessioned** k-ID
The age gate is the entry point to every sessioned k-ID
integration (Shape A in the router). It turns an unidentified
visitor into either a session (adult or minor), a pending challenge
(unverified minor who needs parental consent), or an
unverified-adult session (user claims adult but must still pass age
assurance) by calling /age-gate/check. Get this wrong and a
sessioned product is non-compliant — before any other feature work
matters.
It works across every jurisdiction k-ID supports: COPPA (US), GDPR-Kids (EU), UK AADC, UK Online Safety Act, Brazil ECA Digital, Australia Online Safety, and more. Jurisdictional behaviour comes from Compliance Studio, not from branching in client code.
You're doing standalone AgeKit+ (router Shape B) — the product
only needs to prove a user's age once for one decision, with no
persistent session and no permission-management UI. In that case
go straight
to k-id-age-verification
(Pattern 1). No age gate, no /age-gate/check, no session.
18+ products where the only compliance requirement is a single age proof fall into this category. 18+ products that have persistent users, accounts, permissions, or ongoing interactions still use the age gate as their entry point.
Use this skill when the user is:
/age-gate/check or deciding what to send in the body.session, challenge, or an unverified-adult
session.For the screen that appears after the gate when the user is a minor
needing consent, load k-id-consent-and-challenges.
For the screen that appears after an adult is returned unverified
(ageCategory: "adult", ageVerificationStatus: "unverified"), load
k-id-age-verification.
There are two ways to implement the age gate. Pick one per flow.
Build the UI by hand and call
POST /age-gate/check
from the server with the collected dateOfBirth (or age) and
jurisdiction. The product owns the slider, labels, copy, and the
branching on the response. The result is a native inline experience
that matches the rest of the product's look, feel, and motion.
Pattern A wins when: the product wants the best-looking, most brand-integrated entry screen, needs non-iframe rendering (Unity WebGL, consoles, native desktop apps without webviews), or has a studio-level visual-design bar to meet. This is the default path for production integrations.
Call POST /widget/generate-age-gate-url
from the server, embed the returned URL in an iframe (or open it with
a redirectUrl), and listen for the Widget.AgeGate.Result and
Widget.AgeGate.Challenge DOM events. The widget handles:
approvedAgeCollectionMethods for the
user's jurisdiction)./age-gate/check invocation.The product's job shrinks to: generate the URL, embed the iframe,
handle the result event. No custom slider, no
/age-gate/check call, no jurisdiction detection in client code.
If the product also wants parental consent, data notices, permissions,
and parental preferences handled in the same iframe, use
POST /widget/generate-e2e-url
instead — one widget covers the whole entry flow. See
k-id-consent-and-challenges for the consent-flow details.
Pattern B wins when: the integration must be small, simple, and as fast as possible to ship — proofs of concept, internal tools, early-stage games, hackathons, or products that can accept the iframe look and prefer minimum code / minimum compliance surface area.
The rest of this skill is split: the custom core steps cover Pattern A (the default), and the widget core steps cover Pattern B.
Follow these in order. Steps 1–4 are prescriptive — exact behavior matters for compliance and session correctness. Steps 5–6 are UI and can be adapted to your framework.
Intl.DateTimeFormat().resolvedOptions().timeZone
mapped to a country. Timezone alone is not enough because it's easy
to spoof and does not distinguish every jurisdiction./age-gate/check and to /age-verification/perform-access-age-verification
later.Reference: docs.k-id.com/concepts/jurisdictions.
Before showing any age gate, call the platform age range endpoint when the platform supplies one (iOS Declared Age Range, Google Play Families, Unity, etc.).
POST /age-gate/get-platform-age-range.HIGH-trust signal for a minor, the signal
itself determines the session — you do NOT show a slider gate.LOW-trust or no signal, show the age gate
and pass the platform signal in the /age-gate/check body.Reference:
See also k-id-mobile-native for OS-side collection.
The single most important rule of a k-ID integration:
const [age, setAge] = useState<number | null>(null);
const [hasInteracted, setHasInteracted] = useState(false);
// Display text:
const displayAge = age === null ? '—' : age >= 35 ? '35+' : String(age);
// Submit button:
<button disabled={age === null || !hasInteracted} onClick={onSubmit}>
Continue
</button>
// Slider onChange:
const onSlide = (value: number) => {
setAge(value);
setHasInteracted(true);
};
The age starts as null, not 0, not 18, not any number. The user must
actively set a value before the submit button enables. This is what
makes the gate a real age gate instead of a formality.
/age-gate/check with exactly one age identifierBuild the body on the server, not the client. Send either dateOfBirth
(preferred — convert age → YYYY-01-01) OR age directly — never
both. Sending both is rejected with 400.
POST /age-gate/check
Authorization: Bearer <server-only API key>
Content-Type: application/json
{
"dateOfBirth": "2012-01-01",
"jurisdiction": "US",
"platformSignal": { /* optional, from step 2 */ }
}
For the full request and response schema, see
docs.k-id.com/api/endpoints/check-age-gate.
| Response | What it means | Next action |
|---|---|---|
session.sessionId with permissions, all enabled | User is adult or old enough to proceed with nothing gated | Store session, render the signed-in app |
session.sessionId with a permission carrying verifiedAgeThreshold | Session is usable, but a specific permission requires age assurance (for example Brazil ECA Digital loot-boxes at 18, UK OSA 18+ features, Australia social media at 16) | Render the signed-in app; load k-id-age-verification when the user taps the gated feature |
challenge.challengeId (no session) | Minor — parental consent needed | Load k-id-consent-and-challenges |
session with ageCategory: "adult" and ageVerificationStatus: "unverified" | Adult whose privileged permissions are withheld pending age verification | Load k-id-age-verification |
Do not synthesize a local session ID. If the response has no
session.sessionId, the correct state is "pending challenge" — not a
fake session. Fake session IDs propagate to privileged endpoints and
cause cascading failures.
enteredAge alongside the sessionKeep the raw age the user entered in state. You will need it to re-call
/age-gate/check after an expired challenge, or to refresh a session
whose challenge was approved on another device.
Same rule as Pattern A — IP-based detection, timezone fallback.
Widgets require jurisdiction in the request body.
POST /widget/generate-age-gate-url
Authorization: Bearer <server-only API key>
Content-Type: application/json
{
"jurisdiction": "US-CA",
"kuid": "12b9fa0e-6d6d-4903-a1fc-f2233027b71d",
"options": {
"redirectUrl": "https://yourapp.example/agegate/callback"
}
}
kuid is optional — provide the k-ID user ID if the product has one
already, otherwise the widget creates a fresh identity. redirectUrl
is optional — without it, the widget runs inline in the iframe and
emits events.
Return the URL to the browser; keep the API key server-side.
<iframe
src={ageGateUrl}
allow="camera; microphone; publickey-credentials-get"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
/>
Listen for DOM events emitted by the widget:
Widget.AgeGate.Result with data.status and data.sessionId
(plus data.challengeId if a challenge was created).Widget.AgeGate.Challenge with data.status: "PENDING" and
data.challengeId.Widget.ExitReview when the user clicks "Done".For the full event shapes, see
docs.k-id.com/events/dom-events.
| Event | Meaning | Next action |
|---|---|---|
Widget.AgeGate.Result with sessionId only | User passed, session ready | Store session, render the signed-in app |
Widget.AgeGate.Result with sessionId + challengeId | Minor — widget has already kicked off a parental-consent challenge | Load k-id-consent-and-challenges to poll at the top level of the app; optionally use the e2e widget so the whole flow happens inline |
Widget.AgeGate.Challenge (interim) | Challenge is pending — user is mid-flow | Keep the iframe visible; wait for the final Result |
Widgets also surface unverified-adult outcomes in the resulting
session's ageVerificationStatus field. After the widget closes, call
GET /session/get and check for
ageVerificationStatus === "unverified" → load
k-id-age-verification.
Each item below explains why the guidance matters.
sandbox="allow-scripts allow-same-origin" (or
handle redirectUrl instead if the product cannot grant that).useState<number | null>(null) — never useState(18). A default
of 18 is a COPPA violation — the gate has no meaningful effect./age-gate/check. dateOfBirth
OR age, not both. Sending both is rejected with 400 and no
descriptive body.HIGH-trust minor signal, the age gate is skipped and the session is
minted from the signal. Showing a slider anyway asks the user to
contradict the OS.window.open or target="_blank" for age verification.
When the gate returns an unverified adult, k-id-age-verification
opens an iframe modal — do not break out to a new tab here either./age-gate/check from the browser. The API key is
server-only. If you're tempted to call it client-side, read
k-id-server-trust-boundary first.local-${Date.now()} is not a sessionId. If the response has no
session, the user is in a challenge state, not a session state. The
app renders differently (load k-id-consent-and-challenges).enteredAge at gate time. Later flows (expired challenge
retry, limited-access session refresh when
childLiteAccessEnabled is true) need to re-call
/age-gate/check(enteredAge, jurisdiction). Losing the age forces a
full re-prompt.When the user asks for a "standard k-ID age gate", default to a custom UI (Pattern A). Custom produces the best-looking, most-integrated entry screen and works on every platform the product targets. Drop to the widget (Pattern B) only when the integration must be small, simple, and as fast as possible to ship.
For a custom UI (Pattern A), use these visual defaults:
'—' before first interaction; '35+' above 35.age !== null && hasInteracted.min(720px, calc(100vh - 48px)).YYYY-01-01 based on current year minus age."US" only if both IP and timezone fail —
otherwise you mis-apply US rules to non-US users.After wiring the gate, run the checklist for the pattern you built.
age === null — button is disabled (no network request
fires).session,
challenge, or an unverified-adult session. Log the raw response
for this run.GET /session/get with the returned sessionId and confirm
ageCategory, jurisdiction, and permissions are populated.If step 3 returns an empty permissions array for what should be a
full-access session, treat it as a configuration or provisioning
issue and re-run discovery (load k-id-sessions-and-permissions).
jurisdiction and the URL loads in an iframe without CSP errors.Widget.AgeGate.Result with a sessionId after
a successful age entry.GET /session/get with that sessionId returns the expected
ageCategory, jurisdiction, and permissions.Widget.AgeGate.Challenge with a challengeId
— the product does not need to call /age-gate/check separately.k-id-consent-and-challenges.k-id-age-verification.k-id-sessions-and-permissions.k-id-server-trust-boundary.k-id-mobile-native.Canonical references:
npx 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.