Every webhook is a POST with a JSON body in the same envelope:
{
"id": "evt_9f2c1b7e4a6d4c0f8e3b2a1d5c6f7e80",
"type": "message.received",
"createdAt": "2026-10-01T10:15:02.000Z",
"data": {}
}| Field | Meaning |
|---|---|
id | Unique per event, and the same on every retry. |
type | One of the events below. |
createdAt | When the event happened. |
data | The event's content. |
Events#
Webhooks are sent for API numbers only. Inbox numbers are used from the WaConnect inbox and never send webhooks.
| Event | When |
|---|---|
message.received | A customer sent a message to one of your numbers. |
message.queued | A message you sent through the API was accepted and is waiting for its turn (numbers send at a human pace). |
message.sent | A message went out — through the API, or typed on the number's phone. |
message.status | A sent message was delivered, read, or failed. |
number.connected | A number was linked. |
number.disconnected | A number was logged out from the phone. |
number.removed | A number was deleted, or disconnected after 7 days without credits. |
webhook.test | You pressed Send test event in the dashboard. |
You choose which events each webhook receives when you add or edit it.
Choose numbers#
Each webhook also covers either All numbers (the default, including numbers connected later) or only the numbers you pick. A webhook limited to some numbers only receives events whose number is one of them: messages sent or received by those numbers, and number.* events about them.
When two apps share one business, give each app its own webhook limited to its numbers, and its own API key limited to the same numbers. Neither app then sees the other's conversations.
Every event still says which number it's about (data.message.numberId or data.number.id), so one webhook for several numbers can tell them apart. webhook.test goes to the webhook you test, whatever its numbers.
Message events#
message.received, message.queued, message.sent and message.status all carry the message object in data.message — the same shape the API returns.
Store messages on your side#
WaConnect doesn't store messages of API numbers, so save each event when it arrives:
- Save
data.messageas a row in your database, keyed bydata.message.id. Every webhook carries the whole message: the number, the contact's phone and name, the text, the status and the time. - On
message.status, update that row'sstatus(anderrorwhen it'sFAILED). - For a photo or document, download the file from
GET /v1/messages/:id/mediaas soon as the webhook arrives, and store it in your own storage. Files are kept for about 24 hours. - Skip events whose
idyou've already handled: a retry sends the same event again.
{
"id": "evt_1b2c3d4e5f60718293a4b5c6d7e8f901",
"type": "message.sent",
"createdAt": "2026-10-01T10:00:01.000Z",
"data": {
"message": {
"id": "01a0f4c2-8b1e-7a0c-9d3f-2e6b5a4c1d00",
"numberId": "01a0f44d-c969-75b1-955d-3841d817f711",
"direction": "OUTBOUND",
"contact": { "phone": "+14155550123", "name": "Emily Carter" },
"kind": "TEXT",
"text": "Your order #1042 is ready for pickup.",
"media": null,
"status": "SENT",
"error": null,
"sentVia": "api",
"sentAt": "2026-10-01T10:00:00.000Z"
}
}
}In message.status, data.message.status is the step the event reports: DELIVERED, READ or FAILED (with error). Events can arrive slightly out of order, so treat a status as final only if it's further along than the one you already have: SENT → DELIVERED → READ.
For photos and documents, data.message.media describes the file. The webhook is sent once the file is stored, a few seconds after the message arrives, so you can download it straight away from GET /v1/messages/:id/media.
sentVia tells you who sent an outgoing message: api (your software), inbox (a team member in WaConnect) or phone (typed on the phone).
Number events#
{
"id": "evt_0a1b2c3d4e5f60718293a4b5c6d7e8f9",
"type": "number.connected",
"createdAt": "2026-10-01T10:01:12.000Z",
"data": {
"number": { "id": "01a0f44d-c969-75b1-955d-3841d817f711", "label": "Front desk", "phoneNumber": "+12125550147" }
}
}number.disconnected has the same shape. It means the user removed WaConnect from Linked devices on the phone, or WhatsApp logged the device out. The number still exists with the same id: connect it again with a new connect link to resume.
Short network drops don't send an event: the number reconnects by itself.
{
"id": "evt_2c3d4e5f60718293a4b5c6d7e8f9012a",
"type": "number.removed",
"createdAt": "2026-10-08T00:00:04.000Z",
"data": {
"number": { "id": "01a0f44d-c969-75b1-955d-3841d817f711", "label": "Front desk", "phoneNumber": "+12125550147" },
"reason": "out_of_credits"
}
}number.removed means the number no longer exists: its id stops working, and its chats stay in your history. reason is deleted (removed from the dashboard or with DELETE /v1/numbers/:id) or out_of_credits (disconnected after 7 days at $0). To use WhatsApp again, create a number and scan it.
number.disconnected | number.removed | |
|---|---|---|
The number's id | Keeps working | Stops working |
| To resume | New connect link for the same id | Create a new number, then scan |
Test event#
{
"id": "evt_test_4c5d6e7f8091a2b3c4d5e6f708192a3b",
"type": "webhook.test",
"createdAt": "2026-10-01T09:59:00.000Z",
"data": { "message": "This is a test event from WaConnect. Your webhook is working." }
}