Webhooks
Each webhook endpoint gets a whsec_... secret shown exactly once, when it is created. The body of every delivery is signed with no later normalization — the signature covers the exact bytes that were sent.
Signature headers
Every delivery arrives with three headers:
X-Hub-Delivery-Id— unique UUID for this attempt, used to deduplicate.X-Hub-Timestamp— the moment it was sent, in seconds since the Unix epoch.X-Hub-Signature-256— HMAC-SHA256 signature of the raw body, in the formatsha256=HEX.
Verifying in Node
Compute the HMAC of the raw body with the endpoint secret and compare it with timingSafeEqual, never with === — a plain string comparison leaks execution time per byte position, which gives an attacker a way to discover the correct signature by trial and error.
import { createHmac, timingSafeEqual } from 'node:crypto'
const expected = createHmac('sha256', process.env.WEBHOOK_SECRET).update(rawBody).digest('hex')
const received = request.headers['x-hub-signature-256']?.replace('sha256=', '')
const valid = !!received &&
/^[0-9a-f]{64}$/.test(received) &&
timingSafeEqual(Buffer.from(received, 'hex'), Buffer.from(expected, 'hex'))Receiving rules
- Read the raw body before parsing JSON — reserializing the JSON can change the expected signature.
- Reject any delivery without the signature header, or with an invalid signature.
- Deduplicate by
X-Hub-Delivery-Id: a retry resends the same id. - Respond
2xxwithin 10 seconds — process the event asynchronously if it takes longer.
Retries and pausing
A delivery that fails (timeout, a response outside 2xx, connection error) is resent up to ten times. After 20 consecutive failures the endpoint pauses automatically — re-enable it in the dashboard once you have fixed what was blocking delivery.
Blocked destinations
A webhook endpoint URL goes through a destination check. These are blocked:
- Plain HTTP — only HTTPS is accepted.
- Internal networks (loopback, link-local and private IP ranges).
- Redirects — delivery does not follow an HTTP redirect.
- Private DNS — a domain that resolves to an internal IP is refused.
Payload modes
Each endpoint picks one of two payload modes:
normalized(default) — the CRPRO Hub envelope:{"id", "event", "channel_id", "occurred_at", "data"}, identical for every event.meta_raw— Meta’s raw payload for the event; with no raw counterpart, it falls back to the normalized envelope.
Event types
An endpoint subscribes to one or more of the following events:
- channel.created
- channel.connected
- channel.degraded
- channel.disconnected
- message.received
- message.sent
- message.status
- message.echo
- template.status
- phone.quality
- account.update
- webhook.test