COPPA Compliance Guide
general-privacy-consent — full-flexibility mode; configure your profile to show age-gated consent for children under 13. Use compliance: { type: 'general-privacy-consent' } in your ConsentiSetup config.COPPA (Children's Online Privacy Protection Act) applies to websites and online services directed at children under 13 in the United States.
Configuration
Age gate is a per-profile, per-locale dashboard setting, not acreateConsenti()/ConsentiSetup()config option — one global age doesn't fit every jurisdiction a multi-region deployment serves. In the dashboard's Profile Editor:
- Step 1 → enable "Age gate", set minimum age to 13, check "Require parental consent".
- Main Banner content step → author the age-gate modal's heading, body text, and Yes/No button labels for each locale you support.
The widget reads this straight off the resolved profile (profile.ageGate/profile.ageGateModal) — no separate widget-side config needed. For a standalone/local profile (registerProfile()), set ageGate/ageGateModal directly on the EmbeddedProfile/EmbeddedTranslations you register instead.
How it works
When the active profile's ageGate.enabled: true, the widget shows a Yes/No age-confirmation prompt before anything else — banner, GPC, CCPA opt-out all wait behind it on first visit:
- Confirmed (visitor is
minimumAgeor older) → normal banner/GPC/CCPA flow proceeds; every consent submission from then on carriesageVerified: true. - Declined,
requireParentalConsent: false→ a deny-all consent is submitted immediately (mandatory/strictly-necessary cookies still granted),ageVerified: false, no banner shown. - Declined,
requireParentalConsent: true→ same deny-all submission, plus the widget requests aparentalConsentTokenfrom the backend (signed withcompliance.dataSigningHash, auto-generated if not set) and fires aconsenti:parentalConsentRequiredevent carrying it — see UI Events. - The
age_verifiedandparental_consent_tokencolumns are stored on the consent record either way.
API fields
{
"visitorId": "uuid",
"consentJson": { "analytics": "denied" },
"ageVerified": false,
"parentalConsentToken": "pcon_..."
}Parental consent flow
Consenti mints a stateless, signed parentalConsentToken and emits it — no server-side persistence of the token itself, and no email-sending infrastructure in this zero-runtime-dependency package. Both the request and the resolve step are plumbing for your own out-of-band verification process, for example:
- Widget declines → calls
POST /consent/:visitorId/parental-consent-request, firesconsenti:parentalConsentRequiredwith the returned token - Your
eventBus.on('consent.parentalConsentRequired', ...)listener sends the parent an email with a verification link containing the token - Parent clicks the link → your backend verifies them however you choose
- Once verified, call
POST /consent/parental-consent-resolvewith{ token }— either from the parent's page (viaresolveParentalConsent()from@consenti/ui) or server-side. Consenti emitsconsent.parentalConsentGranted; your owneventBuslistener decides what "granted" means for stored consent (e.g. callPUT /consent/:visitorIdyourself) — Consenti doesn't update the record automatically.
compliance.dataSigningHashis auto-generated in memory if you don't set it, so tokens are always signed — but with an ephemeral, unpersisted key unless you set it explicitly, meaning a server restart between issuing and resolving a token invalidates it. Set compliance.dataSigningHash before relying on this in production.Dashboard filtering
In the admin dashboard, consent records with age_verified: true are flagged so compliance officers can audit them separately.