Skip to content
WhatIsUp.dev
Esta página está disponible solo en inglés por ahora.

Event payloads

Every webhook body has the same envelope:

{
  "api_version": "v1",
  "event_id": "9bc38acc-2edc-482e-93d4-0482b432ffa6",
  "event": "message.received",
  "customer_id": "2696bc9b-2e37-43d9-a33b-5a79f81daeba",
  "channel_id": "8d653c66-e4ff-43ee-97da-3de5ad5680d4",
  "created_at": "2026-05-01T12:34:56.000Z",
  "data": { /* event-specific shape, see below */ }
}

The envelope fields:

FieldTypeNotes
api_versionstringAlways "v1" today. Match on it exactly if you gate on version.
event_idUUID v4Stable across retries of the same delivery. Use as your dedupe key.
eventstringThe event name. See list below.
customer_idUUIDYour account id. Always present.
channel_idUUIDThe channel that produced the event. Always present.
created_atISO-8601 UTCWall-clock time at the gateway when the event was emitted.
dataobjectEvent-specific payload, schemas below.

All five scalar fields are required — none are nullable. data is required and always an object; its shape is decided by event.

There is no occurred_at field, and ids are UUIDs, not ULIDs — earlier revisions of this page documented both. A receiver written from that will read undefined for the timestamp.

Event types

All 25 events. Subscribing to a name not on this list is rejected at endpoint-create time with 400 invalid_request.

Messages

  • message.received — Inbound message from a contact. Carries from, to, chat_id, timestamp, is_group, body, optional push_name. See the LID resolution note below.
  • message.sent — Your outbound message reached WhatsApp. Carries message_id, to, timestamp, body, and your client_ref.
  • message.status — A delivery tick for a message you sent: sentdeliveredread/played, or failed. See below.
  • message.reaction — Someone reacted in a conversation your channel is in. Carries message_key, reactor, emoji, timestamp. An empty emoji means the reaction was cleared.

Channel lifecycle

  • channel.statusEvery lifecycle transition, as one event. Carries status, previous_status, changed_at, simple (qr / connected / logged_out / stopped), live and can_send. If you are driving pairing headlessly, subscribe to this one and ignore the three below. simple is a pairing signal, not a health signal — a disconnected channel reports simple: "connected" on purpose, because its credentials are still good and the socket returns on its own. Read live for "is the socket up right now" and can_send for "will a send be accepted".
  • channel.connected — The channel finished pairing. Carries phone_number, connected_at.
  • channel.disconnected — The session ended. Carries reason, will_reconnect, disconnected_at.
  • qr.updated — A new QR is available. Carries qr_png_base64 (bare base64) and expires_at (epoch ms). The dashboard uses this; webhook subscribers usually filter it out.

Groups

  • group.created — Carries group_id, by, timestamp, subject, participants.
  • group.updated — Group metadata changed.
  • group.participant_added / group.participant_removed — Membership changed.
  • group.admin_promoted / group.admin_demoted — Admin rights changed.

All six carry group_id, the actor by (nullable), and timestamp.

Chats

  • chat.updated — Chat state changed (mute, unread count, …).
  • chat.archived — Chat archived or unarchived.
  • chat.pinned — Chat pinned or unpinned.

Commerce (WhatsApp Business accounts only)

  • order.placed / order.cancelled — An inbound order changed state.
  • cart.updated — A customer's cart changed.

Other

  • story.viewed — Someone viewed a story you posted.
  • call.offered / call.terminated — Incoming call lifecycle.
  • presence.updated — A contact's presence changed. Carries chat_id, contact_id, presence (available / unavailable / composing / recording / paused) and last_seen (Unix seconds, or null when privacy hides it).
  • contact.resolved — Fires the first time we learn the phone JID behind a previously-LID-only contact. See below.

There is no message.delivered and no message.failed event. Both are message.status frames discriminated by the status field. Older revisions of these docs listed them as separate events — subscribing to those names fails validation.

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": "3EB0C9A17F2B4D8E1A05F3D77C10B4E2A991",
  "to": "558585218491@s.whatsapp.net",
  "status": "read",
  "is_group": false,
  "timestamp": 1779425257573
}

message_id is the id we returned from your send — WhatsApp format, 3EB0 plus 36 uppercase hex characters. It is not a wamid.… handle; that is the Meta Cloud API's format, not ours. timestamp is epoch milliseconds.

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 phone JIDs only. Any to ending in @lid is rejected up front with 400 undeliverable_recipient — known or unknown, cached or not. WhatsApp silently drops messages addressed to a LID, so we refuse them rather than report a delivery that never happens.

So: when from_resolved is false you cannot reply yet. Store the message against from_lid, wait for contact.resolved, then send to the phone JID.

contact.resolved payload

{
  "channel_id": "8d653c66-e4ff-43ee-97da-3de5ad5680d4",
  "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/lids
 { "data": [{ "lid": "...@lid", "phone_jid": "...@s.whatsapp.net" }] }

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: 9bc38acc-2edc-482e-93d4-0482b432ffa6
X-WhatIsUp-Correlation-Id: req-3f7a1c02

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/channels/{channel_id}/messages call (the same value returned as the x-request-id response header). For a message.received it's a generated ID stable across retries. Useful in logs.

Compatibility

We version the schemas via api_version, currently v1. Within a version 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 SSE stream at /v1/events carries the same data payloads, but it is not the same envelope and not the same event vocabulary — SSE frames are unsigned, unwrapped, and use a few different event names. Read that page before you write one handler for both.