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

  1. Go to Settings → Webhooks and add an endpoint.
  2. Pick which events to subscribe to (or "all events").
  3. Copy the whsec_… signing secret. We show it once.
  4. Verify the pad-signature header on every request.

Event types

  • profile.created, profile.updated
  • event.tracked
  • send.delivered, send.bounced, send.opened, send.clicked, send.complained, send.unsubscribed
  • insight.created
  • campaign.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:

  1. Parse t and v1 from the header.
  2. Reject if |now - t| > 5 minutes (replay defense).
  3. Compute HMAC_SHA256(secret, `t.rawBody`) and compare to v1 using 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 above
  • pad-event — event type (e.g. send.delivered)
  • pad-delivery-id — unique per attempt; use for idempotency
  • user-agent: pad-webhooks/1.0