PPactDocs
Developers

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:

bash
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:

HeaderPurpose
X-Pact-Signaturet=<unix_ts>,v1=<hmac_sha256> — verify this
X-Pact-Event-IdUnique event id (use it to deduplicate)
X-Pact-Event-Typee.g. contact.created
X-Pact-Delivery-IdUnique 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.

python
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)
ts
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 (except 429) → 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.

What's next?