Webhooks
Receive real-time notifications when consent events occur on your platform. Webhooks let your backend systems react immediately to consent decisions without polling the API.
Use cases
- βStop sending marketing emails the moment consent is withdrawn
- βLog consent decisions to your own database for compliance records
- βTrigger user-preference updates in your CRM when consent changes
- βAlert your team when a breach-related withdrawal is detected
Setting Up a Webhook
Two steps to start receiving events.
1Register your endpoint
POST /api/v1/webhooks
Authorization: Bearer <token>
Content-Type: application/json
{
"label": "Production webhook",
"url": "https://yourdomain.com/webhooks/dpdpa",
"events": [
"consent.recorded",
"consent.withdrawn"
],
"secret": "your-webhook-signing-secret-min-16-chars"
}label
A human-readable name for this endpoint
secret
16β256 chars. Used to sign every delivery.
events
Array of event type strings. At least one required.
2Receive events in your server
// webhook-handler.js
const express = require('express');
const crypto = require('crypto');
const app = express();
// IMPORTANT: Use raw body middleware - not json()
app.use(express.raw({ type: 'application/json' }));
app.post('/webhooks/dpdpa', (req, res) => {
// 1. Verify the signature
const signature = req.headers['x-dpdpashield-signature'];
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(req.body) // raw bytes - not parsed
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)) {
return res.status(401).send('Invalid signature');
}
// 2. Parse and handle the event
const event = JSON.parse(req.body);
switch (event.type) {
case 'consent.recorded':
handleConsentRecorded(event.data);
break;
case 'consent.withdrawn':
handleConsentWithdrawn(event.data);
break;
case 'webhook.test':
console.log('Test event received - handler is working');
break;
default:
console.log('Unhandled event type:', event.type);
}
// 3. Return 200 immediately - process async if needed
res.status(200).send('OK');
});Return 200 within 10 seconds
If your handler takes longer, process the event asynchronously and return 200 immediately. Timeouts are treated as failures and will trigger retries.
Event Reference
Every delivery is a POST with a JSON body in this envelope:{ id, type, timestamp, tenantId, data }
Fired when a Data Principal accepts consent for the first time.
{
"id": "evt_01hwzk3m4p0000000000000000",
"type": "consent.recorded",
"timestamp": "2026-05-18T10:30:00.000Z",
"tenantId": "tenant-uuid",
"data": {
"consentRecordId": "record-uuid",
"dataPrincipalHash": "a665a45920422f9d417e4867ef...",
"consents": {
"purpose-id-1": true,
"purpose-id-2": false
},
"noticeId": "notice-uuid",
"noticeVersionId": "version-uuid",
"recordedAt": "2026-05-18T10:30:00.000Z"
}
}Also fired when a Data Principal updates their preferences after initial consent. Use the consentRecordId to distinguish records.
{
"id": "evt_01hwzk3m4p0000000000000001",
"type": "consent.recorded",
"timestamp": "2026-05-18T11:00:00.000Z",
"tenantId": "tenant-uuid",
"data": {
"consentRecordId": "new-record-uuid",
"dataPrincipalHash": "a665a45920422f9d417e4867ef...",
"consents": {
"purpose-id-1": true,
"purpose-id-2": false
},
"noticeId": "notice-uuid",
"noticeVersionId": "version-uuid",
"recordedAt": "2026-05-18T11:00:00.000Z"
}
}Fired when a Data Principal withdraws consent via the widget or rights portal.
Legal obligation - DPDPA Section 8(7)
Upon receiving this event, your system must cease all processing of the data principal's personal data. Handle this event immediately - do not defer it.
{
"id": "evt_01hwzk3m4p0000000000000002",
"type": "consent.withdrawn",
"timestamp": "2026-05-18T12:00:00.000Z",
"tenantId": "tenant-uuid",
"data": {
"withdrawalId": "withdrawal-uuid",
"consentRecordId": "record-uuid",
"dataPrincipalHash": "a665a45920422f9d417e4867ef...",
"dataPrincipalEmail": "user@example.com",
"isFullWithdrawal": true,
"purposeIds": [],
"withdrawalMethod": "WEB_PORTAL",
"withdrawalReason": null,
"withdrawnAt": "2026-05-18T12:00:00.000Z"
}
}Will fire when a new consent notice version is published to your SDK.
Fired when you click Test on a webhook endpoint. Use this to verify your handler is working correctly.
{
"id": "evt_01hwzk3m4p0000000000000003",
"type": "webhook.test",
"timestamp": "2026-05-18T09:00:00.000Z",
"tenantId": "tenant-uuid",
"data": {
"message": "This is a test event from DPDPA Shield",
"timestamp": "2026-05-18T09:00:00.000Z"
}
}Verifying Webhook Signatures
Every delivery includes three security headers:
| Header | Value |
|---|---|
X-DPDPAShield-Signature | sha256=<hmac-sha256 hex digest> |
X-DPDPAShield-Timestamp | Unix timestamp in milliseconds |
X-DPDPAShield-Event | Event type, e.g. consent.withdrawn |
The HMAC is computed over the raw request body bytes - before JSON parsing. Always use a raw body middleware.
const crypto = require('crypto');
function verifySignature(rawBody, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody) // Buffer or string - raw bytes before JSON.parse
.digest('hex');
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express usage:
// app.use(express.raw({ type: 'application/json' }))
// verifySignature(req.body, req.headers['x-dpdpashield-signature'], secret)Always use timingSafeEqual /hmac.compare_digest /hash_equals. A plain === comparison is vulnerable to timing attacks.
Delivery and Retries
DPDPA Shield uses BullMQ with exponential backoff. A delivery is successful when your endpoint returns any 2xx status within 10 seconds. Non-2xx responses and timeouts trigger retries.
| Attempt | Delay before attempt | Approx. elapsed |
|---|---|---|
| 1 | - | 0s (immediate) |
| 2 | 5s | ~5s |
| 3 | 10s | ~15s |
| 4 | 20s | ~35s |
| 5 | 40s | ~75s |
Delay formula: 5000ms Γ 2^(attemptsMade) - exponential with a 5 second base.
Delivery status values
PENDING
Queued for delivery
SUCCESS
Received 2xx response
FAILED
All 5 retries exhausted
RETRYING
Awaiting next retry
Viewing Delivery History
/api/v1/webhooks/:id/deliveriesReturns the last 50 delivery attempts for a webhook endpoint - newest first. Requires Bearer token with DPO, ANALYST, or VIEWER role.
{
"data": [
{
"id": "delivery-uuid",
"event": "consent.withdrawn",
"statusCode": 200,
"attemptCount": 1,
"succeededAt": "2026-05-18T12:00:01.000Z",
"failedAt": null,
"createdAt": "2026-05-18T12:00:00.000Z"
},
{
"id": "delivery-uuid-2",
"event": "consent.recorded",
"statusCode": 503,
"attemptCount": 3,
"succeededAt": null,
"failedAt": "2026-05-18T10:32:00.000Z",
"createdAt": "2026-05-18T10:30:00.000Z"
}
]
}statusCode
HTTP response code from your server
attemptCount
Which attempt number succeeded or last failed
succeededAt
Non-null when delivery succeeded
failedAt
Non-null when all retries exhausted
createdAt
When the delivery was first queued
Testing Your Webhook
Method 1 - Dashboard test button
Go to Settings β API Keys & Webhooks, find your endpoint, and click Test. A webhook.test event is sent immediately.
Method 2 - API
POST /api/v1/webhooks/:id/test
Authorization: Bearer <token>
// Response: { "data": { "queued": true } }
// A webhook.test event will be delivered to your endpoint immediately.Local testing with ngrok
Expose your local server to receive webhooks during development:
# Expose your local server for webhook testing
ngrok http 3000
# Copy the https forwarding URL:
# https://abc123.ngrok.io
# Register it as your webhook endpoint:
POST /api/v1/webhooks
{
"url": "https://abc123.ngrok.io/webhooks/dpdpa",
"events": ["consent.recorded", "consent.withdrawn"],
"secret": "dev-secret-at-least-16-chars"
}Webhook Best Practices
CONSENT_WITHDRAWN immediately - DPDPA Section 8(7) requires ceasing processing without delayNeed help setting up webhooks?
We can walk you through the integration on a call or over email.
hello@dpdpashield.in β