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).
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):
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:
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}/replaycopies a delivery into a fresh row withattempt_number = 1; the original stays for audit.POST /v1/webhooks/deliveries/bulk-retryre-enqueues up tolimitdeliveries 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.