Webhooks
Webhooks deliver real-time HTTP POST notifications to your endpoint when events happen on your domain. Create and manage webhooks in the dashboard under API & Webhooks, in the Webhooks tab (up to 5 webhooks per domain).
Event types
| Event | Description |
|---|---|
consent.created | A new consent record was created |
consent.updated | Reserved. Not currently emitted |
consent.revoked | Reserved. Not currently emitted |
consent.expired | Consents expired (daily check at 02:00 CET) |
dsar.created | A data subject access request was submitted |
scan.completed | A cookie scan finished |
webhook.test | Test event sent from the dashboard |
Endpoint requirements
- HTTPS URL, up to 2048 characters.
- Public address: localhost, private IP ranges and internal hostnames are rejected.
- Respond with a
2xxstatus within 10 seconds to acknowledge the delivery.
Payload format
Every delivery has this structure:
{
"event": "consent.created",
"timestamp": "2026-04-07T14:30:00.000Z",
"data": {
// event-specific data
}
}
Example consent.expired payload (visitors capped at 100 per delivery):
{
"event": "consent.expired",
"timestamp": "2026-04-07T02:00:05.000Z",
"data": {
"expired_count": 42,
"visitors": [
"a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"b2c3d4e5-f6a7-8901-bcde-f12345678901"
],
"period": {
"from": "2026-04-06T02:00:00.000Z",
"to": "2026-04-07T02:00:05.000Z"
}
}
}
Verify the signature
Every delivery is signed with HMAC-SHA256 using your webhook secret. The secret is shown once on webhook creation and can be regenerated in the dashboard.
Headers sent with each delivery:
| Header | Description |
|---|---|
X-OptSens-Signature | v1=<hex digest> |
X-OptSens-Timestamp | Unix timestamp (seconds) when the payload was signed |
X-OptSens-Event | The event type |
Content-Type | application/json |
User-Agent | OptSens-Webhook/1.0 |
The signature is computed as
HMAC-SHA256(secret, "{timestamp}.{json_body}").
Verify against the raw request body exactly as received. Re-serializing parsed JSON can produce different bytes and fail verification.
const crypto = require('crypto');
const express = require('express');
const app = express();
// Keep the raw bytes: the signature covers the body exactly as sent.
app.use(express.json({
verify: (req, res, buf) => { req.rawBody = buf.toString(); },
}));
function verifyWebhook(secret, signature, timestamp, body) {
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${body}`)
.digest('hex');
return signature === `v1=${expected}`;
}
app.post('/webhooks/optsens', (req, res) => {
const sig = req.headers['x-optsens-signature'];
const ts = req.headers['x-optsens-timestamp'];
if (verifyWebhook(WEBHOOK_SECRET, sig, ts, req.rawBody)) {
// signature valid: process req.body
res.sendStatus(200);
} else {
// reject: tampered payload or wrong secret
res.sendStatus(401);
}
});
Reject deliveries whose timestamp is far in the past to protect against replay.
Retries
Failed deliveries (non-2xx or timeout) retry with exponential backoff:
| Attempt | Delay | Total elapsed |
|---|---|---|
| 1 | Immediate | 0 |
| 2 | 1 minute | 1 minute |
| 3 | 5 minutes | 6 minutes |
| 4 | 30 minutes | 36 minutes |
| 5 | 2 hours | 2 hours 36 minutes |
After 5 failed attempts the delivery is abandoned.
Auto-disable and delivery logs
- A webhook that accumulates 50 consecutive failures is disabled automatically. Any successful delivery resets the counter, and re-enabling from the dashboard also resets it.
- Delivery logs (status, response, attempts) are visible in the dashboard and retained for 30 days.