Skip to content
WhatIsUp.dev

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:

FieldNotes
eventThe event name. See list below.
event_idULID, stable across retries. Use as your dedupe key.
api_versionDate-stamped version of the event schema. We won't break shape within a version.
channel_idThe channel that produced the event. Always present.
occurred_atWall-clock time at the gateway when the event was emitted.
dataEvent-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. Includes phone_number.
  • channel.disconnected — The session ended. Includes reason (network, logout, session_invalidated, kicked_other_device).
  • message.received — Inbound message from a contact. Includes from, body (text or media reference), and received_at. See the LID resolution note below.
  • message.sent — Your outbound message reached WhatsApp. Includes message_id.
  • message.status — A delivery tick for a message you sent: sentdeliveredread/played, or failed. 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
}
FieldNotes
statusOne of sent (reached WhatsApp), delivered (reached the device, ✓✓), read (chat opened, blue ✓✓), played (voice/video note played), failed (undeliverable).
participantIn 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:

FieldNotes
fromBest-known recipient JID. <phone>@s.whatsapp.net when resolved, otherwise the raw <digits>@lid.
from_resolvedtrue when from is a deliverable phone JID. false when only the LID is known so far.
from_lidThe 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_phoneThe 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.