Skip to main content
This contract applies only to v2 subscriptions. Existing legacy outbound webhooks keep their own payloads and signatures; do not replace a legacy verifier with the code below.

Register and verify a receiver

The credential needs webhooks:manage and the read scope for every selected resource event. Persist a subscription UUID, then PUT /webhooks/{id} with an idempotency key and this shape:
The response contains the subscription, not a signing secret. The subscription is initially inactive. Complete setup:
  1. POST /webhooks/{id}/rotate-secret returns {id,signingSecret,active:false}. Store this one-time secret securely and configure your receiver with it.
  2. POST /webhooks/{id}/verify sends a signed {type:"webhook.verification",challenge:"random value"} request.
  3. Verify the raw-body signature, then return 2xx JSON containing the identical challenge within 15 seconds. Successful verification enables the subscription.
Receivers must use public HTTPS on port 443 without URL credentials or fragments. Private destinations and redirects are rejected. Secret rotation pauses delivery and immediately replaces the old secret; configure the replacement and verify again. Never automatically retry rotation or verification after a timeout. PUT configuration changes pause the subscription, cancel queued deliveries and require verification again. PATCH {active:false} pauses; PATCH {active:true} requires prior verification. Requests already in flight may still arrive.

Verify signatures before parsing

PowerDialer-Signature has the format t=<Unix milliseconds>,v1=<hex HMAC-SHA256>. Sign timestamp + . + the exact raw request bytes with the literal signing-secret string. Do not parse and reserialize JSON before verification. Compare in constant time and reject timestamps more than five minutes past or future.
Read the raw bytes before your framework’s JSON middleware. After verification, parse the payload. Challenge requests have no event ID: respond with the challenge. For resource events, atomically deduplicate and durably enqueue processing before acknowledging 2xx. Clock synchronization and a persistent inbox are your receiver’s responsibility; this verifier alone does not prevent duplicate processing.

Event identity and payload

Events contain references, not full contact or evidence snapshots. Fetch current state using the resource type and ID. Deduplicate by event id, or subscription plus event ID for per-subscription processing. PowerDialer-Delivery-Id stays stable per subscription/event; PowerDialer-Attempt-Id changes on each send. The catalog includes contact/list create, update, delete and access_revoked events; list.membership_updated; call create, update and delete events; call.completed, call.disposition_updated, recording.available, and transcript.available. Treat deletes and access revocations as removals from the corresponding owner’s local view. Re-fetch permitted current state when processing other events. Generic and specialized events can describe the same mutation. Delivery order is not a resource version, and a completed call need not have all recordings or transcripts ready.

Delivery history and replay

Transient network errors, 408, 429 and 5xx retry automatically up to eight attempts. Exponential delay starts at 30 seconds plus jitter, capped at 3,600 seconds. Other non-2xx responses fail without automatic retry. Requests time out at 15 seconds and do not follow redirects. A timeout may occur after the receiver accepted work.
  • GET /webhooks/{id}/deliveries: latest 100 deliveries and current states.
  • GET /webhooks/{id}/deliveries/{deliveryId}/attempts: latest 100 attempts.
  • POST /webhooks/{id}/deliveries/{deliveryId}/replay: requires an idempotency key and active verified subscription. Returns 202 {id,eventId,state:"pending"}. This means queued, not delivered. An active attempt can return 409; wait before explicitly retrying.
Replay keeps the event ID. Never use a replay as a reason to place another call, create another invoice or duplicate a downstream task.

Recover with the change feed

GET /changes?after=0&limit=50 requires changes:read and the resource read scopes for the events you need. It returns {data,nextCursor,hasMore}. Each change has a publication sequence represented as a decimal string, plus event ID, workspace (orgId), owner, type, resource reference and timestamp. Never convert the cursor to a JavaScript number. Persist it only after all events in its page are durably processed or queued. Cursors are assigned after commit; a late transaction receives a later cursor. Keep polling after hasMore:false: that only describes currently published visible events. Unlike collection pages, nextCursor remains present when a change page is empty. For initial sync, record changes from cursor zero, enumerate visible resources, then drain changes and continue polling. Merge by stable resource ID. A new webhook subscription starts after the latest published workspace cursor; earlier pending events may still publish afterward. Webhooks do not backfill published history. Event capture requires a non-revoked, unexpired workspace API key at mutation time. After a period with no active keys, enumerate again and reconcile local removals: missing historical events cannot be replayed. Historical record availability may also require a controlled backfill; see coverage. No automatic event pruning is currently configured. That is not an unlimited retention commitment; a retention/resynchronization policy must accompany any future pruning.