Every change on your numbers, signed and sent to you.
Ten events cover messages, statuses, reactions, chats, groups and the numbers themselves. Each one arrives as a signed JSON POST, retried for 24 hours until your endpoint answers.
- Signature
- HMAC-SHA256
- Answer within
- 10 seconds
- Retries
- 24 hours, up to 10 attempts
- Delivery
- At least once
Ten events
Filter each endpoint by event and by number. An empty filter receives everything, including events and numbers added later.
Messages
Chats and groups
Numbers
message.received
A message arrived on one of your numbers, in a one-to-one chat or a group.
data is the full Message object, the same one GET /v1/messages/{id} returns.
webhook.testis sent only byPOST /v1/webhooks/endpoints/{id}/testor the dashboard's Send test event, never by a real change.- Events describe live activity. Messages imported from a number's history when it's linked, or recovered after an outage, are readable through the API but aren't sent as events.
Verify every request
Check the signature on the raw body before you parse it, and drop anything older than five minutes.
Headers
Content-TypeAlways JSON.application/jsonUser-AgentIdentifies our sender.relay-webhooks/1X-Relay-EventThe event name, the same as event in the body.message.receivedX-Relay-DeliveryThe delivery id, as shown in the delivery log.dlv_01a1cf082f8a…X-Relay-TimestampUnix seconds when this attempt was signed.1794474782X-Relay-Signaturev1= and the hex HMAC-SHA256.v1=5f2b…The v1 scheme
- Read the raw body as bytes, before any JSON parsing.
- Join
X-Relay-Timestamp, a dot and the raw body. - HMAC-SHA256 it with the endpoint's secret (the whole
whsec_…string), hex-encode it and prefixv1=. - Compare the result with
X-Relay-Signaturein constant time. - Reject a timestamp more than 300 seconds (5 minutes) away from your clock.
The secret (whsec_…) is shown when you add the endpoint and each time you rotate it. Rotating stops the old secret at once, so update your receiver right after.
Delivery
At least once, signed fresh on every attempt, retried with backoff for 24 hours.
Answer any 2xx within 10 seconds, then do the work. Timeouts, network errors, 408, 429 and 5xx are retried; any other status is final, and redirects aren't followed.
Every attempt posts the same body. Only X-Relay-Timestamp and X-Relay-Signature change.
- Attempt 109:12:44X-Relay-Timestamp: 1794474764Next attempt in 30 s
http_503Your endpoint answered 503. - Attempt 209:13:14X-Relay-Timestamp: 1794474794Next attempt in 2 min
timeoutNo answer within 10 seconds. - Attempt 309:15:24X-Relay-Timestamp: 1794474924Delivered
200Your endpoint answered 200.
evt_ id on every attempt.Retry schedule
Each wait varies by up to 10% either way. When the next attempt would fall more than 24 hours after the event, the delivery ends failed.
Duplicates
Deliveries are at least once. The body's id (evt_…) and the X-Relay-Delivery header (dlv_…) stay the same across retries and redeliveries: keep the ids you've handled and skip repeats. Two endpoints that receive the same change each get their own delivery, with its own id.
Auto-disable
After 10 deliveries in a row end failed, each after its full 24 hours of retries, the endpoint is disabled and you get a notification in the app. A shorter outage never trips it, and any success resets the count.
Fix the endpoint, then re-enable it from the dashboard or with enabled: true. Nothing is sent while it's off: redeliver the events you missed from the delivery log.
Redeliver and test
The delivery log, in the dashboard or at GET /v1/webhooks/deliveries, shows every attempt with your endpoint's answer. Redelivering a finished delivery sends the same body and the same id, with a fresh 24-hour window; it answers 409 webhook_delivery_pending while the delivery is still being attempted.
A test sends a signed webhook.test event to one endpoint, even a disabled one.
Endpoint rules
An https:// URL on the public internet. Private, loopback, link-local and cluster-internal hosts are refused when you save (422 invalid_webhook_url) and checked again before every send (destination_not_public, not retried). We connect only to the address we checked, and redirects aren't followed.
Ordering
A webhook tells you something changed. The order they arrive in isn't guaranteed.
- Events can arrive out of order, especially after retries. Compare
created_at, or read the resource again, before you overwrite newer state. - A message status only moves forward:
pending → sent → delivered → read, orfailed. Ignore a status that would move it back. message.reactionandgroup.participantscarry the whole current list;chat.updated,group.updatedandnumber.statuscarry the state after the change. Keep the newest bycreated_at.
Send yourself a test event.
Add your endpoint in Settings › Developers › Webhooks, send a test event and watch it land in the delivery log.