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
// 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 }

consent.recordedLive

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"
  }
}
consent.recordedLive

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"
  }
}
consent.withdrawnLive

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"
  }
}
notice.publishedComing soon

Will fire when a new consent notice version is published to your SDK.

webhook.test

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:

HeaderValue
X-DPDPAShield-Signaturesha256=<hmac-sha256 hex digest>
X-DPDPAShield-TimestampUnix timestamp in milliseconds
X-DPDPAShield-EventEvent 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.

AttemptDelay before attemptApprox. elapsed
1-0s (immediate)
25s~5s
310s~15s
420s~35s
540s~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

GET/api/v1/webhooks/:id/deliveries

Returns 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

βœ…Always verify the signature before processing any event
βœ…Return 200 immediately - offload slow processing to a queue
βœ…Store raw event payloads in your own logs for debugging
βœ…Implement idempotency - in edge cases an event may be delivered more than once
βœ…Handle CONSENT_WITHDRAWN immediately - DPDPA Section 8(7) requires ceasing processing without delay
❌Don't rely on event order - each event should be processed independently
❌Don't expose your webhook secret in client-side code or version control
❌Don't ignore delivery failures - review the delivery history regularly

Need help setting up webhooks?

We can walk you through the integration on a call or over email.

hello@dpdpashield.in β†’