Webhooks
PAD POSTs JSON to your URL when events happen in your workspace. Receive every signup, send, click, and AI insight in your own systems in near real time.
Quick start
- Go to
Settings → Webhooksand add an endpoint. - Pick which events to subscribe to (or "all events").
- Copy the
whsec_…signing secret. We show it once. - Verify the
pad-signatureheader on every request.
Event types
profile.created,profile.updatedevent.trackedsend.delivered,send.bounced,send.opened,send.clicked,send.complained,send.unsubscribedinsight.createdcampaign.completed
Payload shape
{
"id": "evt_a1b2c3d4...",
"event": "event.tracked",
"createdAt": "2026-05-20T16:00:00.000Z",
"workspaceId": "ws_...",
"data": {
"eventId": "...",
"profileId": "...",
"externalId": "user_42",
"type": "checkout.completed",
"properties": { "amount": 99 },
"timestamp": "2026-05-20T16:00:00.000Z"
}
}Signature verification
Every request carries a pad-signature header in the format t=<unix-seconds>,v1=<hex-hmac-sha256>. To verify:
- Parse
tandv1from the header. - Reject if
|now - t| > 5 minutes(replay defense). - Compute
HMAC_SHA256(secret, `t.rawBody`)and compare tov1using a constant-time comparison.
// Node.js example
import { createHmac, timingSafeEqual } from 'node:crypto';
const verifyPadSignature = (rawBody, header, secret) => {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
const a = Buffer.from(parts.v1, 'hex');
const b = Buffer.from(expected, 'hex');
return a.length === b.length && timingSafeEqual(a, b);
};Retries
PAD retries failed deliveries on this schedule: 1m, 5m, 30m, 2h, 12h. A 2xx response is success; anything else (or a 10s timeout) counts as failure. After 6 attempts the delivery is marked exhausted. After 20 consecutive failures the endpoint is automatically disabled; you can re-enable it from the dashboard.
Manual retry
Every delivery row at /dashboard/settings/webhooks/[id] has a Retrybutton. It re-runs the same dispatch path the cron uses, so the endpoint's attempt counter and disable-after-N-failures budget stay consistent. Re-firing a succeeded delivery is allowed for replay, but counts against the budget. Every retry writes a webhook.delivery_retried audit row with the resulting status code.
Headers
pad-signature— see abovepad-event— event type (e.g.send.delivered)pad-delivery-id— unique per attempt; use for idempotencyuser-agent: pad-webhooks/1.0