Webhooks
Outbound HTTP events for audit and publish lifecycle. Slack, Discord, or any signed HTTPS endpoint.
Events
Webhook endpoints can subscribe to the event types below. Events marked as direct producers are emitted by their feature area; audit.event_recorded is the workspace-wide SIEM stream for every audit write.
consent.recorded | Direct producer. Fires when the banner records a consent receipt for a site in the workspace. |
consent.revoked | Accepted subscription type. Reserved for explicit consent revocation flows. |
scanner.completed | Accepted subscription type. Reserved for scan runs that finish successfully. |
scanner.failed | Accepted subscription type. Reserved for scan runs that fail after processing or timeout. |
domain.verified | Accepted subscription type. Reserved for domain verification events. |
banner.published | Accepted subscription type. Reserved for banner publishes outside the Review & Release approval lifecycle. |
banner.publish.requested | Direct producer. A reviewer requested approval to publish a banner draft (Enterprise Review & Release). |
banner.publish.approved | Direct producer. The final approver voted; the request hit its quorum and published. |
banner.publish.rejected | Direct producer. An approver rejected the request. |
banner.publish.cancelled | Direct producer. The author cancelled their own request before it was decided. |
audit.event_recorded | Direct producer. Fires on every successful AuditService.record() write. Carries the action code, actor, resource, occurredAt, and metadata. System-scope events (tenantId NULL) are not published. |
Create an endpoint
Open /dashboard/settings/webhooks, click Add endpoint, paste the receiver URL. The transport is auto-detected from the URL:
hooks.slack.com | Slack adapter. Payloads are reformatted to Slack's { text, blocks } shape. |
discord.com/api/webhooks/... | Discord adapter. Payloads are reformatted to Discord's { embeds } shape. |
Anything else | Generic transport. Raw JSON envelope, signed with HMAC-SHA256. |
Generic endpoints get a signing secret on create; store it and use it to verify each delivery. Slack and Discord endpoints do not get a secret since their receivers do not honour custom signature headers.
Payload envelope
Every generic delivery is a POST with a JSON body in this canonical shape:
{
"id": "evt_01H8X7...",
"type": "audit.event_recorded",
"createdAt": "2026-05-29T14:22:00.000Z",
"tenantId": "...",
"data": {
// shape depends on the event type
"action": "BANNER_CONFIG_PUBLISHED",
"actor": { "userId": "...", "email": "alice@acme.com" },
"resourceType": "banner_config",
"resourceId": "...",
"occurredAt": "2026-05-29T14:21:58.000Z",
"metadata": {}
}
}Delivery headers include x-cookielint-event-id, x-cookielint-event-type, x-cookielint-tenant, and (for generic endpoints) x-cookielint-signature.
Verify the signature
The signature header for generic endpoints is HMAC-SHA256 of the raw request body, base64-encoded, using your endpoint secret. Compare in constant time:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(rawBody, signatureHeader, secret) {
const expected = createHmac('sha256', secret)
.update(rawBody)
.digest();
const received = Buffer.from(signatureHeader, 'base64');
if (expected.length !== received.length) return false;
return timingSafeEqual(expected, received);
}express.raw({type: 'application/json'}).Slack and Discord adapters
When the endpoint URL matches Slack or Discord, we reformat the payload to the receiver's native shape. Slack endpoints receive a compact message with a header block (event type) and fields (actor, resource); Discord endpoints receive an embed with the same fields. In both cases, the HMAC signing header is omitted because neither receiver honours custom headers.
The original event envelope is still kept on our side, so a Slack or Discord delivery that failed can be replayed against a generic endpoint later.
Replaying failed deliveries
Every send is recorded in webhook_deliveries, along with the canonical envelope. Failed deliveries (non-2xx response, timeout, transport error) can be replayed from the dashboard delivery log or via the API:
curl -X POST \
"https://api.cookielint.com/admin/v1/tenants/$TENANT_ID/webhooks/$ENDPOINT_ID/deliveries/$DELIVERY_ID/replay" \
-H "Authorization: Bearer $JWT" \
-H "x-cookielint-tenant: $TENANT_ID"Replay is OWNER + AAL2 only and is itself audited as WEBHOOK_REPLAY_FIRED. Replays use the same transport-specific formatting as the original send, so a Slack failure replayed renders the same Slack message shape.
Plan availability
Webhooks are available on Pro II and above. The per-tenant endpoint cap scales by plan; see pricing. Enterprise tenants can request a higher cap via custom quotas.

