Documentation menu

Guides

Receiving messages

Every inbound message, vote, button tap, reaction, edit and delete is delivered to your HTTPS endpoint as one normalised JSON event.

Register a webhook

Pass it in POST /start when pairing, or change it any time — no restart, persisted:

curl -X POST "https://api.meowsapp.com/set-webhook" \
  -H "Authorization: Bearer $SN_TOKEN" -H "Content-Type: application/json" \
  -d '{
    "clientId": "acmemain",
    "webhookUrl": "https://example.com/whatsapp/incoming",
    "webhookToken": "9f3b…a-long-random-secret",
    "pollLookupUrl": "https://example.com/whatsapp/poll-lookup"
  }'
  • webhookUrl — absolute https:// URL. Without one, inbound messages are dropped (logged, not stored). Set "" to pause delivery.
  • webhookToken — sent back to you on every call. Choose something long and random.
  • pollLookupUrl — optional; lets us resolve poll votes to option names (below).

The receiver contract

We send one event per HTTP call:

POST /whatsapp/incoming HTTP/1.1
Content-Type: application/json
Authorization: Bearer <webhookToken>
User-Agent: Meowsapp-WhatsApp/1
X-SN-Event: text
You returnWe do
2xxDelivered. Return this once you have durably stored (or deliberately dropped) the event.
4xx (except 429)Permanently rejected — no retry. Use for unauthenticated or malformed calls.
429 / 5xx / timeoutRetried with exponential backoff — 4 attempts (~0.5 s, 1 s, 2 s apart) — then dropped.

Ack in under 15 seconds; expect duplicates; do not assume order

Respond immediately and process asynchronously. Retries mean the same event can arrive twice — de-duplicate on(clientId, chatRoomId, messageId). Events are delivered concurrently; order by timestamp if you need it. There is no durable queue: if your receiver is down beyond the retry window, those events are lost (use thehosted inbox as a safety net if that matters).

The event envelope

Every event shares these fields. At most one of the per-type objects at the bottom is present, matching type:

Event
{
  "event": "message.received",
  "type": "text",
  "clientId": "acmemain",
  "orgId": "acme",
  "timestamp": "2026-08-28T05:22:55Z",

  "chatRoomId": "acmemain-60111111111111-123456789012345",
  "myLid": "60111111111111",
  "clientLid": "123456789012345",
  "remoteJid": "123456789012345@lid",
  "remoteName": "Jane Lim",
  "remotePhone": "60123456789",
  "remoteUsername": "janelim",
  "avatarUrl": "https://files.meowsapp.com/…/avatars/123456789012345/1965117713.jpg",
  "pushName": "Jane",
  "fromMe": false,
  "isGroup": false,

  "messageId": "3EB0A1B2C3D4E5F6",
  "body": "Hello there",

  "quoted":      { "messageId": "3EB0…", "participant": "6011…", "body": "Earlier text", "type": "text" },
  "mentions":    ["60111111111111"],
  "isForwarded": false,
  "meta":        { "fromAI": true, "userId": "u_42", "sender": "Ops bot", "raw": { "campaign": "aug" } },

  "media":       { "…": "type = image | video | audio | document | sticker" },
  "poll":        { "…": "type = poll_creation" },
  "pollVote":    { "…": "type = vote_update" },
  "interactive": { "…": "type = interactive_reply" },
  "contact":     { "…": "type = contact" },
  "location":    { "…": "type = location" },
  "reaction":    { "…": "type = reaction" },
  "edit":        { "…": "type = edit" },
  "revoke":      { "…": "type = revoke" },
  "group":       { "…": "only when isGroup = true" }
}
FieldMeaning
typeEvent type — see the catalogue below. Also sent as the X-SN-Event header.
clientId / orgIdWhich of your sessions received it, and its workspace id ("" if none).
chatRoomId, myLid, clientLid, remoteJidConversation key and the two parties. Reply with to: clientLid.
remoteName, remotePhone, remoteUsername, pushName, avatarUrlContact display name, phone (when WhatsApp reveals it), @username handle, the sender's self-set name, and a permanent profile-picture link. Any may be empty.
fromMetrue for messages sent by this session (from the phone or via the API) — see below.
isGroup, groupGroup messages only (opt-in per session).
messageId, body, timestampWhatsApp id, text / caption / rendered text, send time (RFC 3339, UTC).
quoted, mentions, isForwardedReply target {messageId, participant?, body, type}, mentioned user-parts, forwarded flag.
metaThe metaData you attached when sending, echoed back on the fromMe event as {fromAI, userId, userName, sender, name, raw}.

Event types

The full catalogue with payload samples is on the Webhook events reference page. In short:

typeExtra objectWhat happened
textText message; body is the text.
image video audio document stickermediaA file; descriptors to download it, or a stored link with the hosted inbox.
poll_creationpollSomeone sent a poll.
vote_updatepollVoteSomeone voted.
interactive_replyinteractiveA button you sent was tapped.
contactcontactA contact card was shared.
locationlocationA pinned or live location.
reactionreactionEmoji added to / removed from an earlier message.
editeditAn earlier message was edited.
revokerevokeAn earlier message was deleted for everyone.

Media events

The bytes are not in the webhook. Every media event carries media.download — the WhatsApp CDN path, media key and hashes needed to fetch and decrypt the file (the standard WhatsApp Web media descriptors). Treat mediaKey as a secret. If you would rather not implement decryption, use thehosted inbox: it downloads every file, stores it on a permanent public link and exposes downloadUrl. See the Media guide.

Poll votes & the lookup endpoint

WhatsApp sends votes as hashes of the chosen option names, not the names. When a vote_update arrives we decrypt it and try to resolve the names: from hosted history if the poll was stored there, otherwise by calling your pollLookupUrl:

We call you
POST <pollLookupUrl>
Authorization: Bearer <webhookToken>
Content-Type: application/json

{ "clientId": "acmemain", "chatRoomId": "acmemain-6011…-1234…", "pollMessageId": "3EB0…" }
You answer (200, within 8 s)
{ "options": ["Pizza", "Sushi", "Salad"] }

Return the exact original option strings; order does not matter. If the lookup fails or you did not configure one, the vote is still delivered with resolved: false and selectedHashes (hex SHA-256 of each chosen option) so you can match it yourself.

Reactions, edits, deletes

reaction, edit and revoke are updates to an earlier message, identified by targetId. Do not insert them as new messages: update the reaction set, replace the body, or mark the row deleted. The messageId on these events is the id of the update itself.

Group messages

Off by default. Enable with POST /set-enable-group-ai {"clientId", "enabled": true}. Group events have isGroup: true,remoteJid is the group (…@g.us), chatRoomId is <clientId>-<myLid>-<groupId>, andgroup = {jid, subject, participant, participantPhone?, participantUsername?} identifies the sender. Broadcast lists, status updates and channels/newsletters are never delivered.

Your own sends (fromMe)

Messages sent from the linked phone and messages sent through the API are echoed to your webhook with fromMe: true, so a single feed reconstructs the whole conversation. API sends carry your metaData back as meta — attach{"fromAI": true, "userId": "…"} when sending and you can attribute every outbound message without a second lookup.

Security checklist

  • Verify Authorization: Bearer <webhookToken> on every call to both endpoints; reject anything else with 401.
  • HTTPS only. Respond fast (webhook ≤ 15 s, poll lookup ≤ 8 s); do the real work on a queue.
  • De-duplicate on (clientId, chatRoomId, messageId).
  • Never log media.download.mediaKey.
  • Keep the receiver highly available — there is no replay of dropped events.