PPactDocs
Administration

Webhooks

Register outbound webhook subscriptions, verify the X-Pact-Signature HMAC, and manage delivery retries and replays from Settings → Webhooks.

Webhooks

Outbound webhooks push Pact domain events to your endpoints. Subscriptions are managed under /v1/webhooks/subscriptions (api/routes/webhooks_subscriptions.py) and are tenant-scoped — a subscription is only ever visible to its owning tenant. The dispatcher (core/webhooks/dispatcher.py) signs each payload, POSTs it, and records the outcome in webhook_deliveries.

Registering a subscription

POST /v1/webhooks/subscriptions takes a name, an https:///http:// url, and at least one entry in event_types. It auto-generates an HMAC signing secret and returns it once — save it, there is no reveal endpoint (rotate to get a new one).

bash
curl -X POST https://api.pact.place/v1/webhooks/subscriptions \
  -H "Authorization: Bearer pact_live_…" \
  -H "Content-Type: application/json" \
  -d '{"name":"CRM sync","url":"https://example.com/hook","event_types":["contact.created","contact.updated"]}'
# → { "id": "...", "secret": "...", "warning": "Save this secret …" }

Other management routes: PATCH/DELETE /subscriptions/{id}, POST /subscriptions/{id}/rotate-secret, POST /subscriptions/{id}/test (fires a synthetic webhook.test_fire delivery), and GET /v1/webhooks/event-types (the canonical event catalog that powers the event picker).

Verifying the signature

Every delivery carries an X-Pact-Signature header (core/webhooks/signer.py):

code
X-Pact-Signature: t=<unix_ts>,v1=<hex_hmac>

<hex_hmac> is HMAC-SHA256(secret, "<unix_ts>.<raw_body>"). To verify, recompute the HMAC over "{t}.{body}" with your stored secret, compare with a constant-time check, and reject when |now − t| exceeds the tolerance — 300 seconds by default. The v1= prefix reserves room for future signature schemes.

Rotate carefully

rotate-secret returns the new secret once and takes effect on the next delivery. Update your receiver to verify the new secret before rotating, or signature verification will start failing.

Retries and dead-lettering

On failure the dispatcher consults RETRY_SCHEDULE, which schedules the next attempt at these delays:

code
attempt 2: +10s
attempt 3: +1m
attempt 4: +10m
attempt 5: +1h
attempt 6: +6h
attempt 7: +24h   (last)
past the schedule → dead-lettered   (~31h total)

A 4xx response bypasses retries entirely (a client error won't fix itself). Network errors and 5xx responses use the schedule. A delivery is considered successful on any 2xx.

Delivery log, replay, and bulk retry

GET /v1/webhooks/deliveries returns a paginated, filterable delivery log (by subscription, status, event type, URL substring, and date range), including dead-lettered rows.

  • POST /v1/webhooks/deliveries/{id}/replay copies a delivery into a fresh row with attempt_number = 1; the original stays for audit.
  • POST /v1/webhooks/deliveries/bulk-retry re-enqueues up to limit deliveries matching your filters (retryable statuses: failed, dead_lettered).

Replay is bounded by a 7-day window (WEBHOOK_REPLAY_WINDOW_DAYS, default 7). Deliveries older than the window are retained for audit but return 410 Gone on replay; set the env var to 0 to disable the bound.

Event catalog

The catalog (core/webhooks/event_catalog.py) currently ships these families: contact.*, account.*, opportunity.* (including stage_changed, won, lost), sequence.enrolled/unenrolled and sequence_event.* (opened/clicked/replied/bounced/unsubscribed), consent.granted/withdrawn/ expired, dsar.requested/fulfilled, api_key.created/revoked, webhook.test_fire, and form.submitted.