Event payloads
Every webhook body has the same envelope:
{
"event": "message.received",
"event_id": "evt_01J...",
"api_version": "2026-04",
"channel_id": "inst_01J...",
"occurred_at": "2026-05-01T12:34:56.000Z",
"data": { /* event-specific shape, see below */ }
}The envelope fields:
| Field | Notes |
|---|---|
event | The event name. See list below. |
event_id | ULID, stable across retries. Use as your dedupe key. |
api_version | Date-stamped version of the event schema. We won't break shape within a version. |
channel_id | The channel that produced the event. Always present. |
occurred_at | Wall-clock time at the gateway when the event was emitted. |
data | Event-specific payload, schemas below. |
Event types
The full list of events:
qr.updated— A new QR PNG is available for a channel. The dashboard uses this; webhook subscribers usually filter it out.channel.connected— The channel finished pairing. Includesphone_number.channel.disconnected— The session ended. Includesreason(network,logout,session_invalidated,kicked_other_device).message.received— Inbound message from a contact. Includesfrom,body(text or media reference), andreceived_at. See the LID resolution note below.message.sent— Your outbound message reached WhatsApp. Includesmessage_id.message.status— A delivery tick for a message you sent:sent→delivered→read/played, orfailed. See below.contact.resolved— Fires the first time we learn the phone JID behind a previously-LID-only contact. See below.
message.status payload
Tracks the checkmarks for a message you sent — use it to build a delivered/read indicator or an "awaiting reply" view.
{
"message_id": "wamid.HBgM...",
"to": "558585218491@s.whatsapp.net",
"status": "read",
"is_group": false,
"timestamp": 1748550000
}| Field | Notes |
|---|---|
status | One of sent (reached WhatsApp), delivered (reached the device, ✓✓), read (chat opened, blue ✓✓), played (voice/video note played), failed (undeliverable). |
participant | In group chats, the member whose ack this is — each member acks independently. Omitted for 1:1 chats. |
The same message_id fires multiple times as the status advances. read only arrives when the recipient has read-receipts enabled — its absence is not proof the message is unread.
LID (Linked-Device ID) resolution
WhatsApp's multi-device protocol exposes contacts to linked sessions as Linked-Device IDs (<digits>@lid) instead of phone JIDs (<digits>@s.whatsapp.net). Replies addressed to an @lid JID are silently dropped on WhatsApp's server, so we resolve them gateway-side before emitting the webhook.
Each message.received payload carries three traceability fields:
| Field | Notes |
|---|---|
from | Best-known recipient JID. <phone>@s.whatsapp.net when resolved, otherwise the raw <digits>@lid. |
from_resolved | true when from is a deliverable phone JID. false when only the LID is known so far. |
from_lid | The original @lid JID, present whenever WhatsApp delivered the message as a LID — even after resolution. Lets you correlate prior LID-keyed records with the resolved phone. |
from_phone | The resolved <phone>@s.whatsapp.net JID, present when from_resolved is true. Always equals from in that case — exposed separately so you can grab it unconditionally without checking from's suffix. |
Recommended persistence key: from_phone when present, falling back to from_lid. When from_resolved flips to true for a contact you previously saw as LID-only, we also emit a contact.resolved event so you can rewrite the key deterministically.
Reply addressing: POST /v1/channels/:id/messages accepts either form on the to field. Phone JIDs (<digits>@s.whatsapp.net) are preferred. LIDs (<digits>@lid) are routed via the per-channel LID↔phone cache populated as inbound traffic flows; an unknown LID returns 400 undeliverable_recipient.
contact.resolved payload
{
"channel_id": "inst_01J...",
"lid": "47064251658474@lid",
"phone_jid": "558585218491@s.whatsapp.net",
"first_seen_at": "2026-05-22T21:17:11.576Z"
}You can also list every (LID → phone JID) pair we've observed since the session opened:
GET /v1/channels/{id}/contacts
→ { "data": [{ "lid": "...@lid", "phone_jid": "...@s.whatsapp.net" }], "count": 1 }Headers your endpoint will see
POST /your/endpoint HTTP/1.1
Host: api.acme.dev
Content-Type: application/json
User-Agent: whatisup-webhooks/0.0.1
X-WhatIsUp-Signature: t=1700000000,v1=...
X-WhatIsUp-Event: message.received
X-WhatIsUp-Event-Id: evt_01J...
X-WhatIsUp-Correlation-Id: req_01J...
X-WhatIsUp-Event-Id and X-WhatIsUp-Event are conveniences — they mirror what's in the body, but let you route or short-circuit before you parse the JSON.
X-WhatIsUp-Correlation-Id traces the event back to whatever triggered it. For an outbound message.sent it's the request ID of the POST /v1/messages call. For a message.received it's a generated ID stable across retries. Useful in logs.
Compatibility
We versioned the schemas via api_version. Within 2026-04, we'll only add optional fields — never rename, never remove. New incompatible shapes get a new version string; you opt in by upgrading your endpoint at your pace. (We don't have multiple versions live yet — just one.)
The wire payloads here are the same shapes that come down /v1/events. The SSE stream is just an unsigned, untoaster real-time view of the same bus.