---
title: Callbacks
description: Run code in a React app when a visitor records a consent choice,
  when permissions change, when a request fails and before the revocation
  reload, with the ConsentProvider callbacks option.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## Pass callbacks to the provider

Keep the callbacks in their own module:

```ts title="src/consent-callbacks.ts"
import type { ConsentProviderCallbacks } from 'c15t/react';

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;
```

Pass them in the `ConsentProvider` options, in the file that renders the
provider, such as `src/consent.tsx` from the
[quickstart](/docs/frameworks/react/quickstart):

```tsx title="src/consent.tsx"
<ConsentProvider options={{ mode, scripts, callbacks }}>
```

The provider subscribes the callbacks after it mounts, so they run in the
browser. If you render on the server, 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. Defining the object at module
level, as above, keeps it the same across renders.

When you pass your own `runtime` to `ConsentProvider`, the provider does not
wire `callbacks` or `reloadOnConsentRevoked` from its options. Pass them to
`createConsentRuntime` instead.

## Available callbacks

|Callback|Runs when|Receives|
|--|--|--|
|`onChoiceRecorded`|The 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|
|`onPermissionsChanged`|Any effective permission changes value.|`snapshot`, the state after the change; `previous`, the permissions before it|
|`onError`|A consent command fails, such as a failed `/init` request or a save the backend rejects.|`error`, a message string|
|`onBeforeConsentRevocationReload`|A 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/react`.
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:

|Event|`onChoiceRecorded`|`onPermissionsChanged`|
|--|--|--|
|Visitor accepts, rejects or saves|Yes|Yes, if a permission changed|
|A policy resolves and allows categories before any choice|No|Yes|
|Global Privacy Control turns off a grant|No|Yes|
|A recorded choice expires|No|Yes|
|Visitor dismisses a notice|No|No|

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](/docs/concepts/how-consent-works#a-permission-is-not-a-recorded-choice)
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](/docs/frameworks/react/hooks), which re-render the component when
consent changes.

## 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 the
provider options. 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.
