Astro Consent API
Client API
One runtime per page load
The c15t() integration starts one consent runtime per page load and keeps
it across ClientRouter navigation. Every script and island on the page
shares it. There is no provider to wrap your components in.
c15t/astro/client is how browser code reaches that runtime.
getConsentClient() returns the page's client, or null on the server and
before the runtime starts. The runtime starts from a module script, and module
scripts run in document order, so a script of yours can run first. Try again
on DOMContentLoaded, which fires after every module script has run:
The exports that take no client, such as getConsent() and openDialog(),
look the client up for you and do nothing before it starts.
Read a permission, not a recorded choice
A consent snapshot holds both. They answer different questions:
| Field | Answers | Use it to |
|---|---|---|
effectivePermissions | May this run now? | Load a script, render an embed, send an event |
explicitChoice | What did the visitor decide? | Show the visitor's choice, report consent rates |
Under an opt-out policy, effectivePermissions.measurement can be true
before the visitor has done anything, while explicitChoice is null. A
Global Privacy Control signal can turn a permission off with no recorded
choice at all. Gate code on effectivePermissions. See
How consent works and the
consent state reference.
Read consent
| Export | Returns |
|---|---|
getConsentClient() | The page's AstroConsentClient, or null |
getConsent() | The current snapshot, or null before the runtime starts |
subscribe(listener) | An unsubscribe function. listener receives every new snapshot. Before the runtime starts, it returns a no-op |
The client has the same two methods, client.getConsent() and
client.subscribe(listener), which never return null.
The snapshot fields you are most likely to read:
| Field | Holds |
|---|---|
effectivePermissions | true or false for each category, right now |
explicitChoice | The visitor's recorded decision per category, or null |
activeUI | 'banner', 'dialog' or 'none', and null before the policy resolves |
model | The policy's model: 'opt-in', 'opt-out', 'iab' or 'none' |
policyRule | The matched rule, with its id, prompt and rights |
policyPending | true while the first /init answer is outstanding |
location | Country and region the backend resolved, or null |
translations | The active language and its translation bundle |
A listener runs synchronously on each change. Keep it quick, and read only the fields you need.
Read vendor consent
When the integration declares vendors, the client also answers per vendor:
| Member | Returns |
|---|---|
client.getDeclaredVendors() | The vendors from vendors, the backend manifest and the slugs on scripts and iframes. Empty under an IAB policy |
client.getVendorChoice() | The recorded vendor decision, whose denied lists the vendors the visitor switched off, or null |
client.isVendorAllowed(id) | true when the vendor is declared, its category is allowed and the visitor has not switched it off |
isVendorAllowed() returns false for an id nothing declares, such as a
typo or a vendor missing from vendors. In development it also logs a
console warning that names the id.
Open and close dialogs
| Member | Does |
|---|---|
openDialog(kind?, tab?) | Opens 'preferences' (default) or 'iab'. For 'iab', tab is 'purposes' or 'vendors' |
client.closeDialog() | Closes the open dialog. The banner comes back while the policy still owes a choice |
preloadDialog() | Downloads the dialog without opening it |
openDialog() downloads and mounts the dialog island the first time. While
the first /init answer is outstanding, it waits for it, then opens nothing
if the policy owes no consent interface.
ConsentDialogLink needs no script. For a control you render yourself,
call openDialog() from its click handler:
Save consent
| Member | Does |
|---|---|
client.acceptAll() | Without IAB, saves a grant for every category in scope. Under an IAB policy, an optional category is granted only if a listed or custom vendor declares a consent purpose mapped to it after publisher restrictions apply. The CMP sets vendor and purpose consent for declared consent purposes, separate legitimate-interest signals for declared legitimate-interest purposes, and opt-ins for declared special features. The TC string records these signals. See categories under an IAB policy |
client.rejectAll() | Saves a rejection of everything except necessary, through the CMP under an IAB policy |
client.save(consents) | Saves the categories you pass, such as { measurement: true }. A vendors map, such as { vendors: { posthog: false } }, saves vendor switches too |
client.identify(user) | Links the consent record to your own user, as { externalId } |
Each applies the choice in the browser at once, closes the open banner or dialog, and returns a promise that settles when the backend has answered. A failed request keeps the choice in the browser and retries later. See when a choice is saved for which surface shows next.
Save only from a visitor's action, such as a button click. Calling
acceptAll() or save() when a page loads records a choice the visitor never
made. A save that turns off a category that was allowed reloads the page,
unless the integration sets reloadOnConsentRevoked: false.
Call identify() after your visitor signs in. externalId is your own user
ID. identityProvider, properties and an
identityToken that verifies the link are optional.
Run gated inline scripts
activateGatedScripts(snapshot, root?) runs every inline script marked
type="text/plain" and data-c15t-category that the snapshot allows, inside
root or the whole document. A script that also carries data-c15t-vendor
waits until the visitor has not switched that vendor off. It returns the
number of scripts it ran.
c15t calls it after every consent change and every ClientRouter
navigation. Call it yourself only for markup you insert from your own code:
Use the page runtime directly
The client exposes two properties for advanced use:
| Member | Holds |
|---|---|
client.runtime | The underlying consent runtime. Pass it to a React, Vue or Svelte island. See your own islands |
client.options | The integration options as the browser received them |
client.runtime.kernel.events.on(type, listener) subscribes to individual
consent events, such as 'choice:recorded' and 'permissions:changed'. For
most sites, the callbacks in the client entrypoint are simpler. See
Callbacks.
Do not call client.dispose() in your own code. The page owns the runtime,
and a disposed runtime stops every consent surface on the page.
Keep scripts working across ClientRouter
With ClientRouter, module scripts run once per page load, not once per
navigation. The consent runtime survives each swap, so getConsentClient()
keeps returning the same client and your subscriptions keep firing.
Markup your script changed is replaced on a swap. Render it again on
astro:page-load:
A custom element, like the video component above, reconnects on its own when the new page contains it.
Send analytics events after consent
createEventDispatcher from @c15t/integrations/events sends your own events to
the vendors you registered, and only while their consent allows it. Pass the
same scripts and the page's getConsent:
Events sent while consent is denied are dropped, not queued. Keep search text and other personal data out of event properties unless you have set up their collection on purpose.
Advanced exports
c15t/astro/client also exports what the integration's page script uses:
boot, attachBannerActions, registerDialogAdapter,
registerDialogSurface, registerDialogStyles, syncBannerVisibility and
syncSurfaceVisibility, plus the attribute names ACTION_ATTRIBUTE,
DIALOG_ATTRIBUTE and DIALOG_TAB_ATTRIBUTE. The integration calls them for
you. You need them only to run c15t's client without the integration, as a
test harness does. The script loader, the network blocker and a
consentSource connection come from the integration's page script, so
boot() on its own throws when the options configure scripts,
networkBlocker or consentSource.
Check the client API
- Load a page with the video component above in a fresh private window. The
placeholder shows, and DevTools Network has no request to
youtube-nocookie.com. - Allow and deny measurement from Privacy settings. The video appears after you allow it and disappears after you withdraw it, when the page reloads.
- Navigate with
ClientRouterand confirm your script still reacts. Scripts that change markup should listen forastro:page-load.