Backend — Events
createConsenti() returns an eventBus — a standard Node.js EventEmitter — that emits typed events at every data lifecycle step. Subscribe to hook into consent saves, profile changes, and the server ready signal without polling the database or modifying routes.
eventBus and making the HTTP call yourself — see the Webhooks guide for a detailed walkthrough.import { createConsenti } from '@consenti/api'
const { eventBus, ready } = createConsenti({ /* ... */ })
eventBus.on('consent:created', (record) => {
console.log('New consent from visitor', record.visitorId)
})
// Wait for storage + bootstrap to complete before accepting requests
await readyEvent reference
| Event | Emitted when | Payload |
|---|---|---|
ready | Storage connected and admin user bootstrapped | none |
consent:created | A new consent record is saved for the first time | ConsentDbRecord |
consent:updated | An existing consent record is updated | { previous: ConsentDbRecord, current: ConsentDbRecord } |
consent:erased | A visitor's data is erased (GDPR right to erasure) | { visitorId: string } |
visitor:created | A new visitor record is created | Visitor |
profile:created | A consent profile is created | Profile |
profile:updated | A consent profile is updated (version bumped) | { previous: Profile, current: Profile } |
profile:deleted | A consent profile is deleted | { id: string, previous: Profile } |
cache:warm | Locale JSON files are written to disk (profile create or activate) | { paths: string[], version: number } |
cache:purge | Locale JSON files are removed (profile update, deactivate, or delete) | { paths: string[], version: number } |
ready
Emitted once after the storage adapter connects, migrations run, and the bootstrap admin user is created (if needed). This maps to the ready Promise returned by createConsenti() — they resolve at the same time.
Use the Promise form when you need to await startup before accepting requests. Use the event form when you want a fire-and-forget side effect.
// Promise form (preferred for startup sequencing)
const consenti = createConsenti({ /* ... */ })
await consenti.ready
server.listen(3000)
// Event form (fire-and-forget side effects)
consenti.eventBus.on('ready', () => {
console.log('[consenti] backend ready')
})consent:created
Fires after a new ConsentDbRecord is persisted — after plugins' afterConsentSave hooks run. Useful for webhooks, audit side-cars, or real-time analytics.
import type { ConsentDbRecord } from '@consenti/api'
eventBus.on('consent:created', async (record: ConsentDbRecord) => {
await fetch('https://webhook.example.com/consent-created', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
visitorId: record.visitorId,
profileId: record.profileId,
consent: record.consentJson,
gpc: record.gpcDetected,
timestamp: record.createdAt,
}),
})
})consent:updated
Fires when a visitor submits new consent choices that differ from their existing record. The payload includes both the previous and current state so you can compute a diff.
import type { ConsentDbRecord } from '@consenti/api'
eventBus.on('consent:updated', ({ previous, current }: {
previous: ConsentDbRecord
current: ConsentDbRecord
}) => {
const prev = previous.consentJson
const next = current.consentJson
for (const cookieId of Object.keys(next)) {
if (prev[cookieId] !== next[cookieId]) {
console.log(`${cookieId}: ${prev[cookieId]} → ${next[cookieId]}`)
}
}
})ConsentAction / CategoryAction
Server-side counterparts to the widget's ConsentAction/CategoryAction — thin wrappers around consent:created/consent:updated that fire onGrant/onDenyonly on an actual transition (not on every event), across every visitor's submissions. Prefer these over listening to the raw events directly when you only care about one parameter or category — they handle the previous-vs-current comparison for you.
There is no server equivalent of the widget's ConsentScript/CategoryScript — injecting a <script>tag only makes sense in a browser DOM the server doesn't have.
import { createConsenti, ConsentAction, CategoryAction } from '@consenti/api'
const { eventBus, services } = createConsenti({ /* ... */ })
// Watch a single cookie parameter
new ConsentAction({
id: 'analytics_storage',
eventBus,
onGrant: ({ visitorId }) => crm.optIn(visitorId),
onDeny: ({ visitorId }) => crm.optOut(visitorId),
})
// Watch a whole category (granted only when every cookie in it is granted)
new CategoryAction({
categoryId: 'marketing',
eventBus,
profiles: services.profile, // resolves the category's cookie ids per profile
onGrant: ({ visitorId }) => adsPlatform.optIn(visitorId),
onDeny: ({ visitorId }) => adsPlatform.optOut(visitorId),
})Both classes expose destroy() to remove their event listeners when the hook is no longer needed.
consent:erased
Fires after a visitor exercises their GDPR right to erasure. The visitor's consent record and visitor row are deleted from storage before this event fires.
eventBus.on('consent:erased', ({ visitorId }: { visitorId: string }) => {
// Mirror the deletion to a data warehouse or external DMP
myDmpClient.deleteVisitor(visitorId)
})visitor:created
Fires when Consenti creates a new visitor row — typically on first consent submission from that visitor. IPs are never available here; the record stores only the SHA-256 hash (ipHash).
import type { Visitor } from '@consenti/api'
eventBus.on('visitor:created', (visitor: Visitor) => {
console.log('New visitor in region:', visitor.region ?? 'unknown')
})profile:created / profile:updated / profile:deleted
Profile lifecycle events. profile:updated fires whenever a profile version is bumped — useful for cache invalidation in edge deployments. The previous profile is included so you can compare changes.
import type { Profile } from '@consenti/api'
eventBus.on('profile:created', (profile: Profile) => {
console.log('New profile:', profile.id, 'v' + profile.version)
})
eventBus.on('profile:updated', ({ previous, current }: {
previous: Profile
current: Profile
}) => {
console.log('Profile', current.id, 'bumped from v' +
previous.version + ' to v' + current.version)
})
eventBus.on('profile:deleted', ({ id, previous }: { id: string; previous: Profile }) => {
console.log('Profile deleted:', id, '(was v' + previous.version + ')')
})cache:warm / cache:purge
Fired after Consenti writes or removes locale JSON files on disk. Use these to integrate with a CDN, nginx cache, or any edge layer that needs to be invalidated or pre-warmed when a profile changes.
cache:warm fires when new files are written (profile created or activated). cache:purge fires when files are removed (profile updated, deactivated, or deleted). Both mirror the handleCache config callback — use whichever suits your integration.
eventBus.on('cache:warm', ({ paths, version }: { paths: string[]; version: number }) => {
// paths — array of locale file paths written to disk, e.g.
// ['./consenti-data/profiles/default/opt-in/en.json', '...fr-FR.json']
for (const p of paths) {
cdnClient.warmPath(p)
}
})
eventBus.on('cache:purge', ({ paths, version }: { paths: string[]; version: number }) => {
for (const p of paths) {
cdnClient.purge(p)
}
})handleCache config option is equivalent to listening on both cache:warm and cache:purge:isPurge: false → cache:warm, isPurge: true → cache:purge. Use whichever style fits your integration..catch() to avoid crashing the process.