Skip to content

NewThe DPDP Rules phase in by May 2027.Check your website’s consent readiness

Home

Webhooks

Webhooks tell your systems the moment something changes: a consent is granted or withdrawn, a rights request arrives, an erasure is requested. Every delivery is signed, so you can check it came from OptProof.

Starter and above

Set up an endpoint#

  1. In the console, open Integrations, then Webhooks, and choose Add endpoint.
  2. Enter an https URL that is reachable from the internet, and pick the events you want.
  3. Copy the signing secret (it starts with whsec_). It is shown once. Store it in your server's secrets.
  4. Send a test event from the console and check it arrives.

Respond with any 2xx status within 10 seconds; slower answers count as a failure and are retried. Redirects are not followed. Do slow work, such as updating your CRM, in a background job after you have answered.

Events#

EventSent when
consent.grantedSomeone agrees to one or more purposes, on any channel.
consent.withdrawnSomeone withdraws consent for one or more purposes. Stop using their data for those purposes.
consent.expiredA consent reaches the end of its retention period and is marked expired.
request.createdA rights request is filed in the portal or added by your team.
request.due_soonAn open request passes a reminder point before its due date.
request.resolvedA request is closed with a resolution.
erasure.requestedAn erasure request needs action in your systems. Delete or anonymise the person, or record why you must keep the data.
incident.createdYour team opens a data breach incident.
notice.publishedA new notice version is published for a property.

Payload#

Every delivery is a JSON envelope. data holds references only: a pseudonymous principal_ref, receipt codes and ids. Webhooks never contain phone numbers, emails or names.

json
{  "id": "evt_01JBTT8000E008000000000093",  "type": "consent.withdrawn",  "created_at": "2026-10-22T11:02:09.412Z",  "tenant_id": "ten_01JBTT8000E008000000000001",  "test": false,  "data": {    "event_id": "evt_01JBTT8000E008000000000093",    "property_id": "prp_01JBTT8000E008000000000002",    "principal_ref": "3f9a0c...e1",    "device_ref": null,    "action": "withdraw",    "purpose_keys": ["whatsapp_reminders"],    "notice_version": 3,    "channel": "portal",    "receipt_code": "OP-4D8H-2M6Q",    "occurred_at": "2026-10-22T11:02:09.004Z"  }}

Data by event type#

EventsFields in data
consent.*event_id, property_id, principal_ref, device_ref, action, purpose_keys, notice_version, channel, receipt_code, occurred_at
request.*, erasure.requestedrequest_id, reference, type, status, principal_ref, due_at
incident.createdincident_id, title, severity, aware_at
notice.publishednotice_id, property_id, version, material_change

To find the person behind a principal_ref, look it up in your own records (store the principal_ref returned when you record consent), or call GET /v1/principals/{ref}/consents.

Request headers#

HeaderValue
OptProof-Signaturet=…,v1=…, explained below.
OptProof-EventThe event type, for example consent.withdrawn.
OptProof-DeliveryThe delivery id. The same id is sent again on retries.
User-AgentOptProof-Webhooks/1.0
Content-Typeapplication/json

Verify the signature#

Each request carries a signature header:

http
OptProof-Signature: t=1793000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t is the Unix time (seconds) when OptProof signed the request.
  • v1 is the lowercase hex HMAC-SHA256 of the string `${t}.${raw body}`, keyed with your endpoint secret.
  • During secret rotation the header may carry more than one v1= value. Accept the request if any of them matches.
  • Reject requests whose t is more than 300 seconds from your clock, to stop replays.

Always compute the HMAC over the raw bytes you received. Parsing and re-serialising the JSON changes the bytes and breaks the signature. Compare in constant time.

js
import { createHmac, timingSafeEqual } from 'node:crypto';import express from 'express';​const TOLERANCE_SECONDS = 300;​function isValidSignature(rawBody, header, secret) {  if (!header) return false;  const parts = header.split(',').map((p) => p.trim().split('='));  const t = Number(parts.find(([k]) => k === 't')?.[1]);  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;  const expected = Buffer.from(    createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest('hex'),    'hex',  );  return parts    .filter(([k]) => k === 'v1')    .some(([, v]) => {      const given = Buffer.from(v ?? '', 'hex');      return given.length === expected.length && timingSafeEqual(given, expected);    });}​const app = express();app.post('/webhooks/optproof', express.raw({ type: 'application/json' }), (req, res) => {  if (!isValidSignature(req.body, req.header('OptProof-Signature'), process.env.OPTPROOF_WEBHOOK_SECRET)) {    return res.status(400).send('Invalid signature');  }  const event = JSON.parse(req.body.toString('utf8'));  res.sendStatus(200);  // queue event.type / event.data for processing});

With the Node.js SDK this is one call: optproof.verifyWebhook({ body, signature, secret }). See SDKs.

Retries and the delivery log#

If your endpoint does not answer 2xx, OptProof retries with exponential backoff for about 24 hours: after 1 minute, 5 minutes, 15 minutes, 30 minutes, then 1, 2, 4, 8 and 8 hours, with some random jitter. After the last attempt the delivery is marked failed.

  • Every attempt is listed in the console's delivery log with the status code and timing.
  • You can replay any delivery from the log, for example after fixing a bug.
  • Deliveries can arrive more than once and out of order. Use the envelope id (or the OptProof-Delivery header) to ignore duplicates, and the occurred_at time to decide which change is newest.

Test events#

Events produced with test keys are delivered with "test": true. Point a staging endpoint at them, or ignore them in production.