# KIDIGLOO Integration Guide

**Audience:** an engineer *or AI agent* integrating a tenant app (e.g. **Nook**) with
KIDIGLOO. **Version:** 0.1 · **Updated:** 2026-06-21 · **Canonical URL:**
`https://kidigloo.com/developers/kidigloo-integration.md` · **Human page:**
`https://kidigloo.com/developers/docs`

> This document is **AI-first**: self-contained, exact, and copy-pasteable. If you are an
> AI agent, read §0 then §7 (the rules) before writing any code.

## Status legend

- 🟢 **Live** — callable today against `https://kidigloo.com`.
- 🟡 **Contract** — the committed target surface, being implemented in KIDIGLOO Phase 4–5.
  **Build against it now**; shapes will not change. Until it is 🟢, calls will 404/501.

```json
{
  "provider": "KIDIGLOO",
  "base_url": "https://kidigloo.com",
  "role": "identity + consent + subscription control plane for kid brands",
  "you_are": "a tenant app (Nook) that federates login and asks about entitlements",
  "integrations": ["sign_in_with_kidigloo_oidc", "entitlements_api", "webhooks"],
  "you_never_receive": ["child PII", "date_of_birth", "guardian password", "raw consent records"],
  "you_receive": ["guardian pairwise sub", "household_id", "locale", "entitlement booleans", "age_band", "consent_status"],
  "default": "deny — entitled=false unless KIDIGLOO affirmatively says true",
  "status": { "core_auth": "live", "families_v1": "internal", "oidc_provider": "live", "entitlements_api": "live", "webhooks": "planned", "sdk": "planned" },
  "discovery": "https://kidigloo.com/.well-known/openid-configuration",
  "raw_doc": "https://kidigloo.com/developers/kidigloo-integration.md",
  "llms": "https://kidigloo.com/llms.txt",
  "support": "support@quarlabs.com"
}
```

---

## 0. TL;DR (read this first)

KIDIGLOO owns **identity, parental consent, and subscription** for a family of kid brands.
Your app (Nook) does **three** things and nothing else:

1. **"Sign in with KIDIGLOO"** (OIDC) — the **guardian** signs in; you never store passwords.
2. **Ask the Entitlements API** — *"is this child entitled to `nook.plus`?"* You never run billing.
3. **Honor webhooks** — when consent/entitlement changes, KIDIGLOO tells you; you re-check.

The mental model: **the guardian holds the only credential. A child is a data-minimized
profile you reference by `child_profile_id` — never a login.** You receive *booleans and
reason codes*, never the child's personal data. **Default-deny**: treat anything that isn't
an explicit `entitled: true` as no access.

**One golden path** (pseudo):

```ts
// 1. guardian federates in
const { guardianSub, householdId } = await kidigloo.auth.getSession(req);
// 2. guardian picks which child is reading (KIDIGLOO-hosted)
//    → you get child_profile_id back on return
// 3. gate every protected read on a fresh entitlement check
const { entitled, reason } = await kidigloo.entitlements.check({ childProfileId, feature: "nook.plus" });
if (!entitled) return redirect(kidigloo.subscribeUrl({ feature: "nook.plus", returnTo }));
// 4. webhook handler re-checks on consent.revoked / entitlement.revoked / profile.graduated
```

---

## 1. The model you're integrating with

- **Guardian** — an adult Better Auth user at KIDIGLOO. The authenticated principal. The only
  party with a credential.
- **Household** — the billing + entitlement + residency unit. One guardian is the *owner* and
  holds the Stripe subscription. Entitlements attach **to the household**.
- **Child profile** — a *data-minimized* record inside a household: nickname, **birth year**
  only, locale, avatar, reading level. **No login, email, phone, or full date of birth.** You
  reference it by `child_profile_id`.
- **Entitlement** — a household's access to a tenant feature (e.g. `nook.plus`), usually
  from a Stripe subscription.
- **Consent** — per-child, per-tenant, **revocable**. Releasing a child claim to *you* is a
  third-party disclosure, so it requires **high-assurance** verifiable parental consent (for a
  **paid** tenant like Nook, the Stripe charge itself is that consent). **You never collect
  parental consent yourself** — KIDIGLOO does, and records a tamper-evident receipt.

**The resolution conjunction** (how KIDIGLOO answers `check`):

```
entitled = household has an active entitlement for (tenant, feature)
           AND the guardian has GRANTED per-child consent for this tenant
           AND the child's age_band satisfies the entitlement's minimum
           AND no blocking guardian request
           ELSE false        // default-deny, even if the household pays
```

**What you receive vs. never receive:**

| You receive | You never receive |
| --- | --- |
| guardian pairwise `sub` (unique per tenant) | the guardian's real user id / email (unless scoped) |
| `household_id` (pairwise), `locale` | child name, **date of birth**, precise age |
| `child_profile_id` (you pass it; opaque) | child email / password / phone (they don't exist) |
| `entitled`, `reason`, `age_band`, `consent_status` | Stripe internals, sibling data, consent records |

---

## 2. What's live vs. planned (today)

| Capability | Status | Notes |
| --- | --- | --- |
| KIDIGLOO accounts, 2FA, sessions | 🟢 | `https://kidigloo.com` |
| Families v1 (households, child profiles, consent ledger, entitlement resolution) | 🟢 internal | logic exists; no public API yet |
| **Sign in with KIDIGLOO** (OIDC provider) | 🟢 | discovery: `/.well-known/openid-configuration` (PKCE, opaque tokens) |
| **Entitlements API** (`/api/entitlements/check` · `/resolve`) | 🟢 | Bearer opaque token; default-deny |
| **Tenant webhooks** (consent/entitlement change push) | 🟡 | until shipped, **re-check per request** (the API is the source of truth) |
| **`@kidigloo/sdk`** package | 🟡 | spec in §5; use raw REST until published |

> **For the Nook AI session:** OIDC + the Entitlements API are **live now** — integrate against
> them today. Tenant-facing webhooks aren't live yet, so rely on a short-cache per-request
> `check` (the API is revocable truth). The SDK is a typed wrapper over the same REST.

---

## 3. The three integrations

### 3.1 Sign in with KIDIGLOO (OIDC) 🟢

Standard OAuth2 / OIDC **authorization code + PKCE**. KIDIGLOO is the IdP; Nook is the client.

- **Use OIDC discovery — do not hardcode endpoints:**
  `GET https://kidigloo.com/.well-known/openid-configuration`
- **The principal is the guardian.** `sub` is **pairwise** (a stable id unique to *your* client,
  so sibling tenants can't correlate the same family).
- **Scopes:**
  - `openid` (required)
  - `kidigloo.household` → adds `household_id` (pairwise) + `locale` to the ID token
  - `kidigloo.entitlements` → authorizes the **opaque access token** you use to call the
    Entitlements API
  - `profile` / `email` → guardian PII (request only if you truly need it; Nook does not)
- **The ID token carries identity only** — `sub`, `household_id`, `locale`. It carries **no**
  child / consent / entitlement claims (those are revocable and live behind the API).
- **Choosing the active child:** after login, send the guardian to the KIDIGLOO-hosted picker
  `https://kidigloo.com/select-profile?return_to=<your_url>`; on return you receive the active
  `child_profile_id`. Persist it in *your* session; pass it to every entitlement check.

### 3.2 Entitlements API 🟢

The single source of revocable truth. Authenticate with the **opaque** access token from the
`kidigloo.entitlements` scope (Bearer). The token is introspected per request and is revocable.

```
GET  https://kidigloo.com/api/entitlements/check?feature=<feature_key>&child_profile_id=ch_...
POST https://kidigloo.com/api/entitlements/check   { "feature": "...", "child_profile_id": "ch_..." }
Authorization: Bearer <opaque_access_token>     # GET (query) or POST (JSON body) both work
```

```jsonc
// 200 OK — minimal by design
{
  "entitled": true,
  "reason": "ok",                 // see §10 for all reason codes
  "feature_key": "nook.plus",
  "household_id": "hh_pairwise_...",
  "child_profile_id": "ch_...",
  "age_band": "under_13",         // under_13 | 13_17 | 18_plus
  "consent_status": "granted"
}
```

```
GET https://kidigloo.com/api/entitlements/resolve?child_profile_id=ch_...
→ { "features": [ { "feature_key": "nook.plus", "entitled": true, "reason": "ok" }, ... ] }
```

**Rules:** the token's audience is bound to your client; querying another tenant's feature →
`403`. **Re-check per gating decision** (cache ≤ a few seconds). **Default-deny** on any error,
non-`ok` reason, or timeout.

### 3.3 Webhooks 🟡

KIDIGLOO POSTs signed events to your `webhook_url` on every consent / entitlement / graduation
change, so you react in real time instead of polling.

```
POST <your webhook_url>
X-Kidigloo-Signature: t=<unix>,v1=<hmac_sha256(secret, `${t}.${rawBody}`)>
Content-Type: application/json
```

```jsonc
{
  "id": "evt_...",
  "type": "consent.revoked",       // see taxonomy below
  "created": 1781968566,
  "data": { "household_id": "hh_...", "child_profile_id": "ch_...", "feature_key": "nook.plus" }
}
```

**Event taxonomy:** `consent.granted`, `consent.revoked`, `consent.reconfirmed`,
`entitlement.activated`, `entitlement.revoked`, `entitlement.expired`, `profile.graduated`
(account-merge / `subject_migrated`), `profile.archived`.

**You must:** verify the signature (constant-time), be **idempotent** by `id`, return `2xx`
fast, and **re-resolve** entitlement/consent on `*.revoked|expired|granted` rather than trusting
the payload. Handle `profile.graduated` by merging the child's old pairwise `sub` into the new
adult account (see §7).

---

## 4. Concepts & glossary

| Term | Meaning |
| --- | --- |
| `feature_key` | dot-namespaced capability you gate on, e.g. `nook.plus`, `nook.archive` |
| pairwise `sub` | guardian id unique to *your* client; stable for you, uncorrelatable across tenants |
| `household_id` | the billing/entitlement unit; entitlements attach here and fan out per child |
| `child_profile_id` | opaque handle for a child; you store and pass it; never PII |
| `age_band` | `under_13` \| `13_17` \| `18_plus` — coarse, never an exact age or DOB |
| `consent_status` | `granted` \| `pending` \| `revoked` \| `none` for *your* tenant |
| high-assurance VPC | verifiable parental consent; for a paid tenant the Stripe charge *is* the method |

---

## 5. The KIDIGLOO SDK — AI-first design 🟡

The SDK exists to make integration a **trivial, predictable, autocomplete-driven** task — for
humans and AI agents alike. Until `@kidigloo/sdk` is published, use the raw REST in §3/§9; the
SDK is a thin, typed wrapper over exactly those endpoints.

**Design principles (what "AI-first" means here):**

1. **One client, fully typed, JSDoc'd** — an agent gets the whole surface via autocomplete.
2. **Verb-first, namespaced methods** — `kidigloo.entitlements.check`, `kidigloo.auth.getSession`,
   `kidigloo.webhooks.verify`. No cleverness, no magic.
3. **Discriminated-union returns, never throw for expected states** — `check` returns
   `{ entitled, reason }`; it does not throw because a child isn't consented.
4. **Safe by default** — `entitled` defaults to `false`; helpers fail closed.
5. **Explicit, no hidden global state** — config is passed in; nothing reads ambient singletons.
6. **Web-standard `Request`/`Response`** — runs on Node, Edge, Bun, Workers.
7. **Self-describing errors** — every error has `code` + `hint` (a `next_action` for the agent).
8. **Machine-readable companions** — OpenAPI at `/developers/openapi.json`, an `llms.txt`, and
   this doc served raw as markdown.

```ts
// lib/kidigloo.ts
import { Kidigloo } from "@kidigloo/sdk";

export const kidigloo = new Kidigloo({
  clientId: process.env.KIDIGLOO_CLIENT_ID!,
  clientSecret: process.env.KIDIGLOO_CLIENT_SECRET!,
  webhookSecret: process.env.KIDIGLOO_WEBHOOK_SECRET!,
  baseUrl: process.env.KIDIGLOO_BASE_URL ?? "https://kidigloo.com",
});
```

```ts
// app/api/auth/kidigloo/[...route]/route.ts — federated login (handles callback + PKCE)
import { kidigloo } from "@/lib/kidigloo";
export const { GET, POST } = kidigloo.auth.nextHandler({
  scopes: ["openid", "kidigloo.household", "kidigloo.entitlements"],
  afterSignIn: "/select-child", // we send the guardian to the KIDIGLOO picker, then here
});
```

```ts
// anywhere you gate content
const result = await kidigloo.entitlements.check({
  childProfileId,            // from your session, set after /select-profile
  feature: "nook.plus",
});

if (!result.entitled) {
  // result.reason is a typed union: "consent_pending" | "no_entitlement" | ...
  redirect(kidigloo.subscribeUrl({ feature: "nook.plus", returnTo: req.url }));
}
```

```ts
// app/api/webhooks/kidigloo/route.ts
import { kidigloo } from "@/lib/kidigloo";

export async function POST(req: Request) {
  const event = await kidigloo.webhooks.verify(req); // throws KidiglooSignatureError on bad sig
  switch (event.type) {
    case "consent.revoked":
    case "entitlement.revoked":
    case "entitlement.expired":
      await invalidateAccess(event.data.child_profile_id, event.data.feature_key);
      break;
    case "profile.graduated":
      await mergeAccount(event.data.old_sub, event.data.new_sub); // subject_migrated
      break;
  }
  return Response.json({ received: true });
}
```

**Error model:**

```ts
try { await kidigloo.entitlements.check(...) }
catch (e) {
  // KidiglooError { code, message, hint, status, requestId }
  // code ∈ "unauthorized" | "forbidden_audience" | "rate_limited" | "unavailable"
  // On ANY error → fail closed (treat as not entitled).
}
```

---

## 6. Quickstart — wire Nook in 5 steps

1. **Register Nook as a tenant.** Email support@quarlabs.com (or via the tenant console when
   live) to get `KIDIGLOO_CLIENT_ID` / `KIDIGLOO_CLIENT_SECRET`, set your **redirect URI** and
   **webhook URL**, declare your `feature_key`s, and **sign the DPA** (required before any child
   data flows). Set the env vars from §8.
2. **Add the login route** (`/api/auth/kidigloo/[...route]`) — SDK `nextHandler` or the raw OIDC
   flow (§3.1). Replace your local password login with "Sign in with KIDIGLOO".
3. **Add the child picker** — after login, redirect to
   `https://kidigloo.com/select-profile?return_to=...`; store the returned `child_profile_id`.
4. **Gate every protected route** on `kidigloo.entitlements.check({ childProfileId, feature })`.
   Default-deny. Send non-entitled users to `kidigloo.subscribeUrl(...)`.
5. **Add the webhook handler** (`/api/webhooks/kidigloo`) — verify, be idempotent, re-resolve on
   revocation, merge on graduation.

---

## 7. Rules for AI agents (the integration contract)

These are **non-negotiable** — they are how the privacy/consent guarantees survive contact with
your code. Most are enforced by the DPA, not technically.

1. **Default-deny.** Anything not an explicit `entitled: true` = no access. Fail closed on errors,
   timeouts, and unknown reasons.
2. **Re-check per gating decision.** Never cache an entitlement/consent answer longer than a few
   seconds. Honor revocation webhooks immediately.
3. **Never store child PII.** You don't get any, and you must not infer/derive a DOB or precise
   age. Store only `child_profile_id` + your own content state keyed to it.
4. **Never collect parental consent yourself.** KIDIGLOO owns VPC. Don't build an age gate as a
   substitute — it doesn't discharge your child-privacy obligations for a child-directed app.
5. **Keep history migration-safe.** Key a child's data on the stable household-scoped external id
   KIDIGLOO provides, with `sub` secondary, so `profile.graduated` (account-merge) doesn't orphan it.
6. **Verify webhook signatures** (constant-time) and be **idempotent** by event `id`.
7. **Bind tokens to your tenant.** Never replay an access token against another tenant's feature.
8. **Don't put child/consent/entitlement data in your own JWTs/cookies** as durable truth — it's
   revocable; resolve it live.

---

## 8. Environment variables (Nook side)

| Var | Required | Example | Notes |
| --- | --- | --- | --- |
| `KIDIGLOO_BASE_URL` | no | `https://kidigloo.com` | defaults to prod |
| `KIDIGLOO_CLIENT_ID` | yes | `cl_nook_...` | from tenant registration |
| `KIDIGLOO_CLIENT_SECRET` | yes | `••••` | secret — env only, never commit |
| `KIDIGLOO_WEBHOOK_SECRET` | yes | `••••` | HMAC key to verify webhook signatures |
| `KIDIGLOO_REDIRECT_URI` | yes | `https://nook.kidigloo.com/api/auth/kidigloo/callback` | must match what's registered |

---

## 9. API reference (precise)

> All 🟡 until Phase 4–5. Discover OIDC endpoints — don't hardcode them.

- `GET /.well-known/openid-configuration` → OIDC discovery document.
- `GET /api/entitlements/check?feature=<key>&child_profile_id=<id>` (Bearer opaque token)
  → `200 { entitled, reason, feature_key, household_id, child_profile_id, age_band, consent_status }`;
  `401` bad token, `403` wrong audience, `429` rate-limited.
- `GET /api/entitlements/resolve?child_profile_id=<id>` (Bearer) → `{ features: [...] }`.
- `GET /select-profile?return_to=<url>` (browser redirect) → returns active `child_profile_id`.
- `GET /subscribe?feature=<key>&return_to=<url>` (browser redirect) → KIDIGLOO-hosted subscribe +
  high-assurance VPC; on success, consent + entitlement are written and a webhook fires.
- `POST <your webhook_url>` ← KIDIGLOO → you, signed (§3.3).

## 10. Reason & error codes

**Entitlement `reason`:** `ok` (entitled) · `no_entitlement` (household has no active plan) ·
`consent_pending` (no per-child consent yet) · `consent_revoked` · `age_band_blocked` ·
`unknown_child` · `unknown_tenant`. **Only `ok` means access.**

**Error `code`:** `unauthorized` (401) · `forbidden_audience` (403) · `rate_limited` (429) ·
`unavailable` (5xx / timeout). **All → fail closed.**

## 11. Security & compliance notes

- Releasing a child claim to Nook is a **third-party disclosure** → **high-assurance VPC**
  required. For paid Nook, the **Stripe charge is the VPC method**; consent is written on the
  charge-success webhook, decoupled from the billing intent. You never see the card or the
  consent record — only the resulting `consent_status`.
- KIDIGLOO pins household data to a **single region** and you must not replicate child PII into your
  own DB. **Children's-privacy and data-protection law** governs this; the DPA allocates roles.
- Secrets in env only; the integration is **server-to-server** for the Entitlements API.

## 12. Versioning & support

- This doc is versioned; breaking changes bump the version and are announced to registered
  tenants. **Raw markdown:** `https://kidigloo.com/developers/kidigloo-integration.md` ·
  **`llms.txt`:** `https://kidigloo.com/llms.txt` · **Support:** support@quarlabs.com (Quarlabs).
- Full internal design (for context, not required to integrate):
  `docs/design/families-and-child-accounts.md` in the KIDIGLOO repo.
