---
title: Callbacks
description: React to consent lifecycle events - initialization, consent changes, errors, and revocation reloads.
---
Callbacks let you run custom code at key points in the consent lifecycle. Define them in the provider or runtime `callbacks` option, or register them dynamically after initialization.

For analytics SDKs and other change-only integrations, prefer `subscribeToConsentChanges()` or `onConsentChanged`. Use `onConsentSet` when you want the broader lifecycle signal, including initialization, automatic defaults, and replay-aware registration.

> ℹ️ **Info:**
> subscribeToConsentChanges() is the recommended API for analytics SDKs and consent-mode integrations. It only emits future saves that actually changed persisted preferences.

## Configuration

```tsx
import { type ReactNode } from 'react';
import { ConsentManagerProvider } from '@c15t/nextjs';

export function ConsentManager({ children }: { children: ReactNode }) {
  return (
    <ConsentManagerProvider
      options={{
        mode: 'hosted',
        backendURL: '/api/c15t',
        callbacks: {
          onBannerFetched: ({ jurisdiction, location, translations }) => {
            console.log('Jurisdiction:', jurisdiction);
            console.log('Country:', location.countryCode);
            console.log('Language:', translations.language);
          },
          onConsentSet: ({ preferences }) => {
            console.log('Consent lifecycle event:', preferences);
          },
          onConsentChanged: ({ allowedCategories, deniedCategories }) => {
            analytics.syncConsent({ allowedCategories, deniedCategories });
          },
          onError: ({ error }) => {
            errorReporter.captureMessage(error);
          },
          onBeforeConsentRevocationReload: ({ preferences }) => {
            // Flush pending analytics before page reloads
            analytics.flush();
          },
        },
      }}
    >
      {children}
    </ConsentManagerProvider>
  );
}
```

## Choose the Right Surface

| Surface                       | Replays when registered late?                             | Fires on init / hydration / auto-grants? | Best for                                                                                                            |
| ----------------------------- | --------------------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `onBannerFetched`             | Yes, via `setCallback('onBannerFetched', ...)` after init | Yes                                      | Logging resolved policy, location, and translations                                                                 |
| `onConsentSet`                | Yes, via `setCallback('onConsentSet', ...)`               | Yes                                      | Broad lifecycle hooks, debugging, and integrations that want the latest full state regardless of how it was reached |
| `onConsentChanged`            | No                                                        | No                                       | Declarative change-only integrations                                                                                |
| `subscribeToConsentChanges()` | No                                                        | No                                       | Canonical change-only subscriptions after mount                                                                     |

> ℹ️ **Info:**
> Script.onConsentChange is a script-scoped lifecycle hook. It is not the global consent change API for analytics SDKs or other app-wide integrations.

## Available Callbacks

### `onBannerFetched`

Called when the consent banner data is fetched from the backend (or loaded from SSR data). The payload includes jurisdiction info, location data, and resolved translations.

```tsx
onBannerFetched: ({ jurisdiction, location, translations }) => {
  // jurisdiction: 'GDPR' | 'CCPA' | { code: 'GDPR', message: '...' } | ...
  // location: { countryCode: 'DE', regionCode: 'BY' }
  // translations: { language: 'de', translations: {...} }
}
```

### `onConsentSet`

Called whenever c15t broadly settles consent state: store initialization, automatic defaults during init, explicit saves, and replay via `setCallback('onConsentSet', ...)`.

```tsx
onConsentSet: ({ preferences }) => {
  // preferences: { necessary: true, measurement: true, marketing: false, ... }
  console.log('Latest consent state:', preferences);
}
```

### `onConsentChanged`

Called only after an explicit `saveConsents()` or `setConsent()` that actually changes the saved consent state. It never fires on store creation, hydration, automatic grants, unchanged saves, or `setCallback('onConsentChanged', ...)`.

```tsx
onConsentChanged: ({
  preferences,
  previousPreferences,
  allowedCategories,
  deniedCategories,
  previousAllowedCategories,
  previousDeniedCategories,
}) => {
  analytics.syncConsent({
    allowedCategories,
    deniedCategories,
    previousAllowedCategories,
    previousDeniedCategories,
  });
}
```

### `onError`

Called when an error occurs during consent operations (e.g., API request failure). If no `onError` callback is provided, errors are logged to `console.error`.

```tsx
onError: ({ error }) => {
  // error: string describing what went wrong
  Sentry.captureMessage(`Consent error: ${error}`);
}
```

### `onBeforeConsentRevocationReload`

Called synchronously before the page reloads due to consent revocation. This is your last chance to run cleanup before the reload. Keep this callback fast - avoid async operations.

```tsx
onBeforeConsentRevocationReload: ({ preferences }) => {
  // Flush any pending data
  navigator.sendBeacon('/api/flush', JSON.stringify({ session: sessionId }));
}
```

## Change-Only Subscriptions

Use `subscribeToConsentChanges()` when you want a stable listener for real preference changes after mount:

```tsx
import { useEffect } from 'react';
import { useConsentManager } from '@c15t/nextjs';

function ConsentAnalytics() {
  const { subscribeToConsentChanges } = useConsentManager();

  useEffect(() => {
    return subscribeToConsentChanges(({ allowedCategories, deniedCategories }) => {
      analytics.syncConsent({ allowedCategories, deniedCategories });
    });
  }, [subscribeToConsentChanges]);

  return null;
}
```

## Runtime Callback Registration

Register or update callbacks at runtime using `setCallback()`:

```tsx
import { useEffect } from 'react';
import { useConsentManager } from '@c15t/nextjs';

function ConsentAnalytics() {
  const { setCallback } = useConsentManager();

  useEffect(() => {
    setCallback('onBannerFetched', ({ jurisdiction, location }) => {
      console.log('Resolved init data:', { jurisdiction, location });
    });

    setCallback('onConsentSet', ({ preferences }) => {
      console.log('Broad consent lifecycle event:', preferences);
    });

    return () => {
      setCallback('onBannerFetched', undefined);
      setCallback('onConsentSet', undefined);
    };
  }, [setCallback]);

  return null;
}
```

`setCallback('onConsentSet', ...)` immediately replays the current consent state. For change-only logic, prefer `subscribeToConsentChanges()` or `onConsentChanged`.
