Set up and verify webhooks
Webhooks send an HTTP POST to your server when contacts, conversations or messages change, so you don't have to poll the API.
Each workspace has one webhook endpoint, and every delivery is signed with Standard Webhooks headers.
Webhooks are for code you run. To build WhatsApp replies and follow-ups in the app instead, see the WhatsApp automation tool.
Which events can I subscribe to?
| Event | Sent when |
|---|---|
contact.created | A contact is created. |
contact.updated | A contact changes. |
contact.archived | A contact is archived. |
conversation.started | A session starts in a conversation. |
conversation.updated | An open session's status or owner changes. |
conversation.closed | A session is closed. |
message.created | A message is sent or received. |
message.updated | A message changes, for example its delivery status or reactions. |
GET /webhooks returns the event types your workspace can subscribe to in availableEventTypes. For each event's payload, see the Webhooks reference.
Set up the endpoint
Your endpoint must use https. Setting it up takes three requests.
1. Get the signing key
You need the signing key to verify every delivery, and later calls return metadata only. The first call in a workspace creates the key and returns it once in plaintextKey, in the form whsec_….
Store it in your secret manager.
curl "https://api.flownally.com/v1/webhooks/signing-keys" \
-H "Authorization: Bearer $FLOWNALLY_API_TOKEN"2. Save the URL and events
curl -X PATCH "https://api.flownally.com/v1/webhooks" \
-H "Authorization: Bearer $FLOWNALLY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"url": "https://example.com/flownally/webhooks",
"eventTypes": ["contact.created", "contact.updated", "message.created"]
}'3. Verify the endpoint
Flownally sends a signed webhook.test event to the saved URL. When your endpoint answers with a 2xx status, the response has "delivered": true and the webhook is marked verified.
curl -X POST "https://api.flownally.com/v1/webhooks:verify" \
-H "Authorization: Bearer $FLOWNALLY_API_TOKEN"Verify again after every URL change
Changing url clears verified. Events that happen before verification succeeds aren't delivered later.
Handle a delivery
Every delivery has the same envelope:
{
"id": "whevt_…",
"type": "contact.created",
"timestamp": "2026-04-30T09:20:00Z",
"data": { "contact": { "id": "con_…", "name": "Ada Lovelace" } }
}The same event can arrive more than once, and an attempt that times out is retried. Handle each delivery in this order:
Verify → Skip duplicates → Respond → Process
- Verify the signature against the raw request body, before you parse it.
- Skip the event if you've already processed its
webhook-id. The header equals the payloadidand stays the same on every retry. - Return any
2xxstatus within 15 seconds. Do slow work after you respond, for example from a queue.
Deliveries can arrive out of order, even within one conversation. For message.updated, keep the version with the highest message.revision.
Verify signatures
Each delivery has three headers:
webhook-id: the event ID.webhook-timestamp: the Unix time of this attempt, in seconds.webhook-signature: one or more signatures in the formv1,<base64>, separated by spaces.
The signature is an HMAC-SHA256 of webhook-id.webhook-timestamp.body, keyed with the base64-decoded part of the signing key after whsec_.
Use a Standard Webhooks library rather than writing the check yourself. The libraries also reject stale timestamps, which protects you from replayed requests.
During a key rotation, the header carries two signatures. Accept the delivery if any signature in the header matches.
For code in Node.js, Python and PHP, see Verify webhook signatures.
Retries
Flownally makes up to 4 attempts per event: the first, then retries about 30 seconds, 5 minutes and 30 minutes later, with up to 20% random jitter.
- What gets retried. Timeouts, connection errors,
3xx,408,429and5xxresponses are retried. Redirects aren't followed. - What stops retries.
410and any other4xxresponse stop retries for that event. Retry-Afteris honored after a429. A value in seconds delays the next attempt. If that's more than 30 minutes after the first attempt, the event isn't retried.- The last attempt is final. An event that fails its last attempt isn't sent again. Flownally never disables your endpoint automatically.
To debug deliveries, list the attempts from the last 7 days with GET /webhooks/attempts. Filter by eventType or status.
Rotate the signing key
Call POST /webhooks/signing-keys:rotate. The response returns the new key once in plaintextKey.
Rotation has an overlap, so your receiver keeps working while you deploy. For 24 hours after rotation, deliveries carry signatures from both the old and the new key.
Deploy the new key to your receiver within that window. The old key then expires.
Stop deliveries
To pause deliveries, send PATCH /webhooks with "enabled": false. To remove the webhook, call DELETE /webhooks.
Delivery attempts stay listed for the rest of their 7 days.
FAQ
Why aren't events arriving?
Check that the webhook is enabled and verified. Any change to url resets verification, so call POST /webhooks:verify again; events from before that call are never sent.
Can the same event arrive twice?
Yes. Retries carry the same webhook-id, which equals the payload id. Skip events whose webhook-id you've already processed.
Why do deliveries arrive out of order?
Flownally doesn't promise an order, not even within one conversation. Compare message.revision and keep the highest one you've seen.
Will Flownally disable my endpoint after failures?
No. Flownally never disables your endpoint automatically. An event that fails its last attempt isn't sent again, so list recent attempts with GET /webhooks/attempts to find what you missed.