Webhook subscriptions — get notified when X happens
Subscribe a URL to platform events, verify the signature, and respond correctly.
Webhooks push events to your endpoint the moment they happen, so you don't have to poll. Pact signs every delivery, retries failures on a schedule, and dead-letters anything that never succeeds so you can replay it.
Step 1 — Create a subscription
Create one in the app under Admin → Webhooks (/admin/webhooks) or Settings → Webhooks, or via the API:
curl -X POST https://app.pact.place/v1/webhooks/subscriptions \
-H "Authorization: Bearer $PACT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "CRM sync",
"url": "https://api.example.com/pact-webhook",
"event_types": ["contact.created", "contact.updated", "deal.*"]
}'
The response includes a signing secret, shown once. Save it — you need it to verify deliveries.
Pick exact event names (contact.created), a domain wildcard (contact.*), or everything (*). Browse the full catalog at GET /v1/webhooks/event-types.
Step 2 — Receive and verify the delivery
Pact POSTs JSON to your URL with these headers:
| Header | Purpose |
|---|---|
X-Pact-Signature | t=<unix_ts>,v1=<hmac_sha256> — verify this |
X-Pact-Event-Id | Unique event id (use it to deduplicate) |
X-Pact-Event-Type | e.g. contact.created |
X-Pact-Delivery-Id | Unique per delivery attempt |
Verify the signature before trusting the body. The signed value is "{timestamp}.{raw_body}", HMAC-SHA256 with your secret. Reject anything older than 5 minutes.
import hashlib, hmac, time
def verify(secret: str, raw_body: bytes, header: str, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
ts, sig = parts.get("t", ""), parts.get("v1", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > tolerance:
return False
signed = f"{ts}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)
import crypto from "node:crypto";
function verify(secret: string, rawBody: string, header: string, tolerance = 300): boolean {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=") as [string, string]));
const ts = Number(parts.t);
if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > tolerance) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
Verify against the raw body
Compute the HMAC over the exact bytes you received, before any JSON parsing or re-serialization. Re-encoding the body changes whitespace and breaks the signature.
Step 3 — Respond fast
Return a 2xx within 5 seconds. Do slow work asynchronously — acknowledge first, process after.
- Non-2xx or timeout → Pact retries: ~10s, 1m, 10m, 1h, 6h, 24h (7 attempts over ~31 hours), then dead-letters.
- A
4xx(except429) → Pact stops immediately and dead-letters, assuming the request is malformed.
Step 4 — Test and replay
Use Send test event in the dashboard (or POST /v1/webhooks/subscriptions/{id}/test) to fire a synthetic webhook.test_fire and confirm connectivity. Inspect every attempt in the delivery log and replay failed or dead-lettered deliveries from Admin → Webhooks.