Skip to main content

Next.js Consent API

Callbacks

Put callbacks in c15t.config.ts

Callbacks are functions, and a Server Component cannot pass functions to a Client Component. React serializes every prop a layout passes across that boundary. c15t.config.ts is bundled into the browser, so put the callbacks there instead, under options. Define them in a module:

lib/consent-callbacks.ts
import type { ConsentProviderCallbacks } from 'c15t/next';

export const callbacks = {
	// Right before the page reloads because a save withdrew a permission.
	onBeforeConsentRevocationReload: ({ preferences }) => {
		console.info('Reloading with', preferences);
	},
	// The visitor clicked Accept all, Reject all or Save.
	onChoiceRecorded: ({ confirmed, snapshot }) => {
		console.info('Choice recorded', confirmed, snapshot.explicitChoice);
	},
	// A consent command failed, such as a save the backend rejected.
	onError: ({ error }) => {
		console.error('c15t', error);
	},
	// Permissions changed for any reason: a choice, an expired choice, a new
	// policy or a privacy signal.
	onPermissionsChanged: ({ previous, snapshot }) => {
		console.info('Permissions', previous, snapshot.effectivePermissions);
	},
} satisfies ConsentProviderCallbacks;

Then pass them in the config:

c15t.config.ts
import { defineConsentConfig } from 'c15t/next';

import { callbacks } from './lib/consent-callbacks';

export default defineConsentConfig({ options: { callbacks } });

ConsentRoot reads the config in the App Router layout and in the Pages Router _app.tsx alike, so the same file covers both routers. Keep your existing scripts and other options in the same call.

ConsentRoot subscribes the callbacks after it mounts in the browser. Server rendering never calls them. The provider calls the latest callbacks object it received, so a new object on a later render takes effect without a remount.

Available callbacks

CallbackRuns whenReceives
onChoiceRecordedThe visitor clicks Accept All, Reject All or Save Settings, in the stock UI or through useSaveConsents().snapshot, the state after the choice; confirmed, the categories this action recorded; actionAt, the time as a millisecond timestamp
onPermissionsChangedAny effective permission changes value.snapshot, the state after the change; previous, the permissions before it
onErrorA consent command fails in the browser, such as a failed /init request or a save the backend rejects.error, a message string
onBeforeConsentRevocationReloadA save withdrew a granted category or vendor, right before c15t reloads the page.preferences, the permissions after the save

The type for the whole object is ConsentProviderCallbacks from c15t/next. Callbacks run after c15t updates its state, so snapshot and hooks such as useConsent() already hold the new values.

onChoiceRecorded or onPermissionsChanged

onChoiceRecorded runs only for a visitor's action. onPermissionsChanged runs whenever effective permissions change, whatever the cause:

EventonChoiceRecordedonPermissionsChanged
Visitor accepts, rejects or savesYesYes, if a permission changed
A policy resolves in the browser and allows categories before any choiceNoYes
Global Privacy Control turns off a grantNoYes
A recorded choice expiresNoYes
Visitor dismisses a noticeNoNo

Use onChoiceRecorded for consent analytics and audit trails, because it fires only when the visitor chose. Use onPermissionsChanged to start or stop your own code. Never treat it as proof that the visitor agreed to anything, and never save consent from it. How consent works explains the difference.

Dismissing a notice records no choice and changes no permission. Read it with useNoticeDismissal() instead. For consent inside one component, use the hooks, which re-render the component when consent changes.

Report server-side failures

The provider's onError runs in the browser. resolveConsent has its own onError option for the server. It runs when the manifest or /init request fails or runs out of timeoutMs during rendering. The page still renders with the request-only state and the browser resolves consent after hydration. Without the option, resolveConsent logs a warning outside production.

app/layout.tsx
const state = resolveConsent({ onError: (error) => reportToMonitoring(error) });

This option is fine in the layout, because it runs on the server and never crosses to the browser. reportToMonitoring stands for your own error reporter.

Before a revocation reload

Removing a script tag cannot stop code that already ran, so c15t reloads the page after an accept, reject or save turns off a category or vendor that was granted. The reload waits for the save request to finish. Right before it, onBeforeConsentRevocationReload runs synchronously. Use it to call a vendor's shutdown API or flush queued events. Keep it short, because the reload does not wait for promises.

A choice that expires, a new policy and a privacy signal change permissions without a visitor action, so they do not reload the page.

To handle revocation yourself, set reloadOnConsentRevoked: false in options on ConsentRoot. Code the visitor turned off then keeps running until the next full page load, and onBeforeConsentRevocationReload never runs.

Verify

  1. Add the callbacks from this page, open the console and DevTools Network, clear site data and reload under a policy that shows a banner.
  2. Click Accept All. onChoiceRecorded and onPermissionsChanged both log, and the save request to your backend appears in Network.
  3. Reload. onChoiceRecorded does not log again.
  4. Open Privacy settings, turn off a category you allowed and save. onBeforeConsentRevocationReload logs, then the page reloads. After the reload, that category's vendor requests are absent from Network.