POST /v1/webhooks (scope webhooks:manage).
What a delivery looks like
Every delivery is aPOST with a JSON body in this shape:
Alongside these headers:
Verify the signature
When you create a webhook, the response returns a signing secret that looks likewhsec_…. It is shown exactly once — Paige never returns it again, there is no read-back, and there’s no rotate. Lose it and you delete the webhook and register a new one.
Each delivery is signed as HMAC-SHA256(secret, "<timestamp>.<raw body>"), hex-encoded, and sent as:
1
Recompute the HMAC over the raw bytes
Sign
"<t>.<rawBody>" using the exact bytes you received. If your framework parsed the JSON and you re-serialise it, whitespace and key order shift and the HMAC will not match.2
Compare in constant time
Use a timing-safe comparison, not
===. A naive string compare leaks information about the expected value.3
Reject stale timestamps
If
t is more than 300 seconds away from now, reject it. That’s what stops someone replaying a delivery they captured earlier.Reference verifier
Events you can subscribe to
Platform events
Four events come from Paige itself. Each needs its own scope in addition towebhooks:manage, and the check happens when you subscribe, not when a delivery is attempted.
message.received fires for inbound messages only — your own outbound sends don’t trigger it.
Nothing is delivered unless you explicitly subscribed to that exact event name. There is no “all events” subscription.
Custom events from your bot
Your bot code can raise its own events, which is how you get business signals rather than just messaging plumbing — an order confirmed, a booking taken, a lead qualified.event, and the payload becomes data. Names may use letters, numbers, dots, underscores and hyphens, up to 100 characters. Subscribing to a custom event needs only webhooks:manage — no events:* scope.
paige.emit never throws into your handler. It returns true when Paige accepted the event and false otherwise, so a webhook problem can’t break a customer conversation.
Registering one
secret in that response is the only time you will ever see it. Store it before you do anything else.
Requirements worth knowing up front:
- The URL must be HTTPS and at most 512 characters.
- Between 1 and 50 events per registration.
- The destination must resolve to a public address. Private, loopback, and link-local targets are rejected — so
localhostand your laptop won’t work. Use a tunnelling service while developing. - Redirects are not followed. A
3xxcounts as a failed delivery. Register the final URL.
GET, PATCH and DELETE /v1/webhooks/{id} manage registrations. None of them ever return the secret, and updating a webhook does not rotate it.
Retries and failures
Paige retries a failed delivery 5 times in total with exponential backoff — roughly 5s, 10s, 20s, then 40s after the first attempt, so about 75 seconds end to end. Each attempt has a 10-second timeout. A delivery is a success only on a2xx. Anything else — 3xx, 4xx, 5xx, a timeout, a connection failure — is a failure and gets retried.
Auto-disable
Each registration keeps afailure_count. A single successful delivery resets it to zero. After 10 consecutive failed events, the webhook is automatically set to is_active: false and stops receiving anything.
Check failure_count and is_active under Settings → API → Webhooks or via GET /v1/webhooks if deliveries have gone quiet. Re-activate with a PATCH once your endpoint is healthy.
Writing a good consumer
Acknowledge fast
Acknowledge fast
Return
200 as soon as you’ve stored the event. Ten seconds is the whole budget, and slow handlers turn into retries — which means duplicates.Dedupe on id
Dedupe on id
Retries reuse the same
id, and there are legitimate reasons you might see one twice. Keep the ids you’ve processed for a while and skip repeats. Note that two registrations subscribed to the same event get different ids for it — dedupe per webhook.Be tolerant of new fields
Be tolerant of new fields
Ignore event names you don’t recognise and fields you weren’t expecting. New ones get added.
Never trust an unverified body
Never trust an unverified body
Verify the signature before parsing anything into your system. Reject with
401 when it fails.