POST to it when
something happens in your workspace. Every delivery is signed, retried on
failure, and identified so you can deduplicate.
You can register an endpoint two ways, and both manage the same endpoints:
- in the dashboard, under Settings → Developer → Webhooks, with no API key needed (see Developer settings);
- through the API, with a key that has the
webhooks:managescope (see Register an endpoint below).
Events
An endpoint must subscribe to at least one event, and an unknown event name is
refused when you register.
Some things do not fire an event today, so do not wait for one:
conversation.createddoes not fire for a conversation that Watx opens itself, such as the firstPOST /messagesto a new number, or for website chats.contact.createddoes not fire for contacts added in the dashboard, imported from a file, created by a form, a store order or a booking, or for website visitors.- Cal.com bookings do not fire the
booking.*events.
form.submitted and the three booking.* events could not be subscribed to
before 19 September 2026. If you tried and were refused, try again.The delivery
Each delivery is aPOST with this envelope:
ididentifies the event, not the attempt. A retry carries the sameid, so deduplicate on it.occurred_atis when the thing happened, not when we last tried to tell you.account_idis the workspace. It is the same for every delivery to a given endpoint.
The same three values are also sent as
X-Converse360-Event,
X-Converse360-Webhook-Id and X-Converse360-Signature. They are the retired
names, kept so receivers written before the rename keep working. Read either
pair, not both; new receivers should read X-Watx-*.data per event
message.received carries channel on Instagram and website events and omits
it on WhatsApp, where the platform message id is whatsapp_message_id. A
contact created from an Instagram thread has phone: null and carries
instagram_username instead. Treat data as an open object: read the keys you
need and ignore the rest.
On Instagram, conversation.created also fires when someone opens the
conversation from an ad or a link, and then carries Meta’s referral object as
received. That can happen for a conversation that already existed, so use
conversation_id to tell whether you have seen it before.
message.status_updated reports WhatsApp delivery only — Instagram has no
delivery receipt. The same status can arrive more than once and statuses can
arrive out of order, because that is how the platform reports them.
form.submitted carries the answers in data, keyed by each field’s key. Its
source says where the form was filled in: hosted, embed, widget,
whatsapp_flow, api or automation. contact_id is null when the
submission could not be matched to a contact.
booking.cancelled and booking.rescheduled carry the same keys as
booking.created; after a reschedule, starts_at and ends_at are the new
time. status is the booking’s status at the moment of the event.
Register an endpoint
From the dashboard
Open Settings → Developer → Webhooks and press Add endpoint. Enter the URL, tick the events, and copy the signing secret from the confirmation. The same screen pauses and resumes an endpoint, sends a test event, and removes it. See Developer settings.Through the API
All five management calls need thewebhooks:manage scope.
url must be https:// and must resolve to a public address. Loopback,
private network and link-local targets are refused — at registration and again
on every delivery attempt, because DNS can change underneath us.
The rest of the management surface:
GET /webhooks— list every endpoint in the workspace. Never returns secrets.GET /webhooks/{id}— read one.PATCH /webhooks/{id}— changeurl,eventsoris_active.DELETE /webhooks/{id}— remove one. Deliveries stop immediately.
api.test
straight away and shows you what your endpoint answered.
Zapier and n8n are the same endpoints
Connecting Zapier or n8n from the dashboard registers exactly this kind of endpoint, delivered and signed the same way. Each row carries a read-onlyprovider of api, zapier or n8n, saying
which surface created it.
GET /webhooks returns all of them, so you will see endpoints you did not
create through the API — and deleting one here disconnects that integration.
provider has no effect on delivery; it only decides which dashboard page
manages the endpoint. Settings → Developer → Webhooks lists only api
endpoints, and the Zapier and n8n pages list only their own.
The Zapier and n8n pages offer the four messaging and contact events. Their
test buttons send zapier.test and n8n.test respectively.
Verify the signature
v1 is HMAC-SHA256(secret, "<t>.<raw request body>"), hex-encoded. Compute it
over the raw bytes of the request — a body that has been parsed and
re-serialised will not match — compare in constant time, and reject a timestamp
older than a few minutes so an old delivery cannot be replayed at you.
t it carries is the time that attempt was made.
Delivery, retries and auto-disable
- Success is any 2xx. Anything else is a failure. Answer quickly and do your work afterwards: a delivery that takes longer than 5 seconds is abandoned and treated as a failure.
- Redirects are not followed. A
301or302counts as a failure, so register the final URL. - Retries. A timeout, a connection error, a
5xx, a408or a429is retried — up to 5 attempts, with exponential backoff starting at about 3 seconds. Every other4xxis treated as permanent and not retried: your receiver has said the request is wrong, and it will be just as wrong later. - One endpoint at a time. Each endpoint gets its own delivery, so one receiver being down does not delay or duplicate anyone else’s.
- Auto-disable. A failed event — after its retries are spent — counts once
against the endpoint. After 15 consecutive failures the endpoint is set
is_active: falseand deliveries stop. Any successful delivery resets the count to zero. The dashboard shows the count as N failed in a row before that happens. - Re-enable. Switch the endpoint back on in Settings → Developer →
Webhooks, or send
{"is_active": true}in aPATCH. Either one turns the endpoint back on and clears the failure counter.
failure_count and last_delivery_at on each endpoint are how you check this
from the outside: a climbing count with an old last_delivery_at means your
receiver is refusing deliveries.