What this is for
A withdrawal that only reaches this dashboard leaves your CRM, your warehouse and your analytics acting on a consent the visitor has taken back. Point an endpoint at your own service and every decision arrives there too. Add one under Developer in the dashboard; the workspace owner manages them.
| Event | Sent when |
|---|---|
| consent.recorded | A visitor records or changes a decision, including a withdrawal. The highest volume by a wide margin. |
| configuration.published | An administrator publishes a new configuration version for a site. |
| ping | Only the “Send test” button. Subscribe a new endpoint to this while you are building against it. |
What arrives
Every delivery is a POST with a JSON body. The payload carries what the consent record itself carries — nothing is collected for webhooks that is not already collected, and the visitor key is the same pseudonymous identifier your exports use.
consent.recorded
{
"event": "consent.recorded",
"siteId": "site_3f2a…",
"occurredAt": "2026-09-21T10:15:04.019Z",
"data": {
"consentId": "consent_9b1c…",
"decisionId": "decision_2f8a…",
"visitorKey": "v_7d41c2e9a0b4",
"configurationVersion": "cfg_12",
"categories": {
"necessary": true,
"analytics": false,
"marketing": false
},
"region": "DE",
"source": "banner",
"givenAt": "2026-09-21T10:15:03.884Z"
}
}No personal data is added
Verifying a delivery
Check the signature before you trust a payload. Anyone can POST JSON at your endpoint; only we can sign it with your secret.
| Header | Meaning |
|---|---|
| X-StrongPrivacy-Signature | HMAC-SHA256, hex, of {timestamp}.POST.{path}.{body} using your signing secret. |
| X-StrongPrivacy-Timestamp | Milliseconds since the epoch. Reject anything more than five minutes old. |
| X-StrongPrivacy-Signature-Version | Currently 2. A future format will raise this rather than change what 2 means. |
| X-StrongPrivacy-Delivery | Stable across retries. Store it and ignore a repeat to make your handling idempotent. |
| X-StrongPrivacy-Event | The event name, so you can route without parsing the body. |
Verifying in Node
import { createHmac, timingSafeEqual } from 'node:crypto';
// The raw body, before any JSON parsing. Re-serialising changes the bytes
// and every signature check then fails.
export function verify(rawBody, headers, secret, path) {
const timestamp = headers['x-strongprivacy-timestamp'];
const signature = headers['x-strongprivacy-signature'];
if (!timestamp || !signature) return false;
// Reject anything older than five minutes, so a captured request cannot
// be replayed at you later.
if (Math.abs(Date.now() - Number(timestamp)) > 5 * 60_000) return false;
const expected = createHmac('sha256', secret)
.update(`${timestamp}.POST.${path}.${rawBody}`)
.digest('hex');
// Constant time: comparing with === leaks the answer one byte at a time.
return signature.length === expected.length &&
timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}The secret is shown once
Retries, and being switched off
Answer 2xx once you have stored the event. Anything else is treated as a failure and retried.
- Answer quickly. A delivery is abandoned if it takes more than ten seconds, so acknowledge first and do your own work afterwards.
- A failure is retried with a widening gap — about one minute, then two, four, eight and sixteen — and abandoned after six attempts.
- Redirects are not followed. Point the endpoint at its final address.
- An endpoint that fails twenty times in a row is disabled automatically and the reason is shown beside it in the dashboard. Fix the destination, then re-enable it — that also resets the count.
- Deliveries are queued and sent by a background job, so expect events within minutes rather than instantly. The test button delivers immediately.
Destinations must be public HTTPS