Новое Safe Mode прогревает новые номера и снижает риск блокировки в WhatsApp. Подробнее →

Maytapi/Blog/Guides

WhatsApp Webhooks: Every Event Your Server Can Receive

A practical tour of WhatsApp webhook events: messages, acks, reactions, edits, polls, calls, labels, orders and group events, with payload examples.

Guides Updated Oct 27, 2026·8 min read

WhatsApp Webhooks: Every Event Your Server Can Receive

Polling for new WhatsApp messages is slow and wasteful. A WhatsApp webhook flips it around: every time something happens on your number, Maytapi sends an HTTPS POST with a JSON body to a URL you control. This guide covers how to set the webhook (globally and per number), how to secure it, including the optional static IP, and then walks through every event type you can receive, with payload examples taken from the API reference.

How the WhatsApp webhook works

You register one HTTPS URL. From then on, every phone connected to your product posts its events to that URL as JSON. Each event has a top-level type field, so a single endpoint can route everything:

  • message: anything that arrives in a chat, plus reactions, poll votes, edits, pins, calls, labels, orders and group notifications, which all ride on this type with their own message.type or subtype.
  • ack: delivery and read receipts for messages you sent.
  • status: session state changes for a connected phone.
  • error: a message you queued could not be sent.
  • live_event: agent events, only when you use the API together with LiveChat.

Every event carries product_id (and phone_id where it applies), so one endpoint can serve all the numbers on your account.

Set the webhook URL

setWebhook is a product-level endpoint, so there is no phone id in the path:

Terminal
curl -X POST "https://api.maytapi.com/api/$PRODUCT_ID/setWebhook" \
  -H "x-maytapi-key: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"webhook": "https://example.com/maytapi/webhook"}'
Workflow for configuring the WhatsApp webhook: set the product URL, optionally a per-phone URL, turn receipts on or off and test from the console.

You can also set and change it from the console on the Developers > Webhook page.

A different webhook per phone

If different numbers belong to different clients or systems, give a phone its own URL with POST /config. When a phone's config is empty, the product webhook is used.

Terminal
curl -X POST "https://api.maytapi.com/api/$PRODUCT_ID/$PHONE_ID/config" \
  -H "x-maytapi-key: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"webhook": "https://client-a.example.com/webhook", "ack_delivery": true}'

GET /config returns the current values.

Turn receipts on or off

Ack events can be a large share of your traffic. If you do not need them, switch them off for the product with setAckPreference, and your webhook will only receive incoming messages and status changes:

Terminal
curl -X POST "https://api.maytapi.com/api/$PRODUCT_ID/setAckPreference" \
  -H "x-maytapi-key: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ack_delivery": false}'

The per-phone ack_delivery flag in /config does the same for a single number.

Maytapi console webhook page with the default webhook URL, the static IP toggle and phone-specific webhooks The Developers > Webhook page: a default URL, a static IP switch and a separate webhook per phone, each with a Test button.

The Test button posts a sample message event to your URL and shows the response time, which is the quickest way to check that your endpoint is reachable.

Secure your webhook endpoint

Your webhook URL is a public endpoint that accepts JSON from the internet, so treat it like one.

  • Use HTTPS. The webhook must be reachable from the internet. During development, a tunnel such as ngrok can expose your local server.
  • Use an unguessable path. Something like /maytapi/webhook/7f3c9a... keeps random scanners from posting to it.
  • Check the ids. Reject events whose product_id is not yours, and route by phone_id only to numbers you know.
  • Know what is not there. Requests are not signed and carry no secret header, so the URL path, the id checks and the static IP below are your protection. Keep the URL private and change it if it leaks.
  • Allowlist the static IP. Since 14.08.2026 you can enable a static IP on the console webhook page, so deliveries reach you from a single IP. Turn on the switch, copy the IP shown there and allow only that address in your firewall. This is the strongest of these options if your network supports allowlists.

Message events: text, media and more

The message event is the one you will handle most. A plain text message looks like this:

JSON
{
  "type": "message",
  "product_id": "1a2b3c4d-...",
  "phone_id": 12345,
  "user": { "id": "7916xxxxx67@c.us", "name": "Customer", "phone": "7916xxxxx67" },
  "message": { "id": "3EB081AF26FADE6416D8", "type": "text", "text": "Is my order shipped?", "fromMe": false },
  "conversation": "7916xxxxx67@c.us",
  "receiver": "7916xxxxx21",
  "timestamp": 1574081887,
  "reply": "https://api.maytapi.com/api/1a2b3c4d-.../12345/sendMessage"
}

conversation is the chat id (a group id ending in @g.us for group messages), user is the sender, and reply is a ready-made sendMessage URL for answering. message.fromMe is true for messages sent from your own number, so ignore those in bots or you will reply to yourself.

message.type tells you what arrived. Media types (image, video, document, ptt, audio, sticker, gif, ptv) include a url, mime, filename and an optional caption. A location message carries payload as "latitude,longitude". Contact cards arrive as vcard or multi_vcard. When a message mentions people, mentions lists their ids and contains "all" when the whole group was mentioned.

Reactions, poll votes, edits and pins

These events arrive as message events with their own message.type, and they point back to the original message through parentMsgId. Read reactions and poll votes from these message events. Some connections still send a legacy copy of them under ack, which is deprecated as of 30.10.2026.

A reaction:

JSON
{ "type": "reaction", "id": "3EB0...", "parentMsgId": "7EB065B353B9493D00G2", "reaction": "<emoji>", "fromMe": false }

A poll vote (pollVoteUpdate) includes the vote that was just cast:

JSON
{
  "type": "pollVoteUpdate",
  "id": "3EB0...",
  "parentMsgId": "7EB065B353B9493D00G2",
  "text": "Poll Message",
  "currentVoteOption": { "id": 1, "name": "a", "votes": 1 },
  "fromMe": false
}

An edit (message_edit) gives you both versions, prevText/newText for text and prevCaption/newCaption for media:

JSON
{ "type": "message_edit", "id": "3EB0...", "parentMsgId": "7EB065B353B9493D00G2", "prevText": "See you at 5", "newText": "See you at 6", "fromMe": false }

A pin (pinMessage, on the webhook since 21.04.2026) uses action 1 for pin and 2 for unpin, and duration in seconds (86400, 604800 or 2592000, null on unpin):

JSON
{ "type": "pinMessage", "id": "3EB0...", "parentMsgId": "7EB065B353B9493D00G2", "action": 1, "pinned": true, "duration": 86400, "fromMe": false }

Event responses and live location

When someone answers a WhatsApp event you created, you get event_response with eventResponse set to going, maybe or notGoing. Shared live locations arrive as locationLive updates:

JSON
{ "type": "locationLive", "id": "3EB0...", "payload": "38.3978531,28.968876", "isLive": true }

Group, call and label notifications

Notifications that are not chat messages arrive with message.type set to info and a subtype.

Group events include group/add, group/remove, group/promote, group/demote, group/leave, group/invite, group/name, group/description, group/restrict, group/announce, group/member_add_mode, group/membership_approval_mode and group/revoke_invite. The participant field names the member involved, and conversation is the group:

JSON
{ "type": "info", "id": "3EB0...", "subtype": "group/add", "participant": "7916xxxxx67@c.us" }

Group participants can appear as @lid privacy ids. WhatsApp @lid Explained shows how to resolve them.

Calls use call/ringing, call/miss, call/reject and call/accept. Since 15.09.2026 they include isVideo, so you can tell voice from video calls:

JSON
{ "type": "info", "id": "3EB0...", "subtype": "call/ringing", "isVideo": false }

Labels (WhatsApp Business) arrive as label/add and label/remove since 12.10.2026, with the label's id, name and color:

JSON
{
  "type": "info",
  "subtype": "label/add",
  "id": "true_7916xxxxx67@c.us_LABELADD845838T1790935070_7495xxxxx67@c.us",
  "fromMe": true,
  "label": { "id": "845838", "name": "Order complete", "color": "#F0B330", "colorIndex": 5 }
}

There is also chat/disappearing, sent when the disappearing-messages timer of a chat changes, with the new timer in ephemeral (seconds).

Orders, receipts and session status

Orders. Since 20.02.2026, an order placed from your WhatsApp catalog arrives as a message event with message.type set to order. It contains the products (title, quantity, price, retailerId), subtotal, total and currency. The catalog and orders guide covers it in detail.

Acks. Receipts for messages you sent come as type: "ack", with a data array:

JSON
{
  "type": "ack",
  "product_id": "1a2b3c4d-...",
  "data": [{ "ackType": "seen", "ackCode": 3, "chatId": "7916xxxxx67@c.us", "msgId": "50d8b950-a67f-11eb-91cc-d97af631df95", "time": 1586874663 }]
}

Match msgId to the id that sendMessage returned. Codes run 1 delivered (WhatsApp servers), 2 reached (the phone), 3 seen, 4 played (voice notes) and 5 deleted.

Session status. type: "status" reports the state of a connected phone: active, qr-screen, idle (left on the QR screen for three minutes), conn-retry (reconnecting after a dropped connection), phone-error, loading, disabled, banned, or logout when the session was logged out. Alert someone when a number sits in qr-screen, idle, logout or phone-error, because it needs attention before it can send again.

JSON
{ "type": "status", "product_id": "1a2b3c4d-...", "phone_id": 12345, "status": "qr-screen" }

Send errors. A message that sendMessage accepted but your phone could not send arrives as type: "error", with a code, a message and the original request in data. data.id is the msgId the send returned, so you can mark the right message as failed:

JSON
{
  "type": "error",
  "product_id": "1a2b3c4d-...",
  "phone_id": 12345,
  "code": "E01",
  "message": "The contact cannot be found!",
  "data": { "id": "3EB0A1B2C3D4E5F6", "to_number": "7916xxxxx67", "type": "text", "message": "Hello" }
}

Common codes are E01 (number not on WhatsApp, or a duplicate dropped by the filter), F12 (WhatsApp has restricted new conversations), F13 and F14 (Safe Mode limits), F16 (new-chat quota used up) and UN5 (key-protected username without the right key). The send message guide lists the full table and what to do for each code.

WhatsApp webhook event reference

Where Value What it means
type message Incoming or own message; inspect message.type
message.type text, image, video, document, ptt, audio, sticker, gif, ptv, location, vcard, multi_vcard Regular chat content
message.type reaction, pollVoteUpdate, message_edit, pinMessage Updates that point to parentMsgId
message.type event_response, locationLive Event RSVP, live location update
message.type order Order placed from your catalog
message.type group_invite, group_invite_v4 Group invitation sent in a chat
message.subtype group/... Group membership and settings changes
message.subtype call/ringing, call/miss, call/reject, call/accept Calls, with isVideo
message.subtype label/add, label/remove Label changes on a chat
type ack Delivery and read receipts
type status Session state of a phone
type error A queued message could not be sent; see code
type live_event LiveChat agent events (API + LiveChat only)

Retries, idempotency and a minimal handler

A few habits make a webhook handler reliable:

  • Return 200 fast. Acknowledge first, then do the work in a queue or background task. A slow database or AI call should never hold the response.
  • Know the retry policy. Any 2xx response counts as delivered. If your endpoint times out (about 10 seconds) or returns another status, Maytapi tries again right away, and if that fails too, makes one last attempt about 30 minutes later. After three failed attempts the event is dropped, so a long outage loses events.
  • Expect duplicates. Because of these retries, the same event may arrive more than once. Deduplicate on message.id for messages and on msgId plus ackCode for receipts.
  • Do not assume order. A seen ack can be processed before reached if your workers run in parallel. Keep the highest ackCode you have seen per message.
  • Ignore what you do not handle. New event types get added over time (the changelog lists them), so log unknown types instead of throwing.
Checklist for a reliable webhook handler: return 200 fast, expect duplicates, do not assume order and ignore event types you do not handle.

A minimal Express handler that follows these rules:

JavaScript
import express from "express";

const { TOKEN } = process.env;
const app = express();
const seen = new Set(); // use Redis or your database in production

app.post("/maytapi/webhook", express.json(), async (req, res) => {
  res.sendStatus(200); // acknowledge first

  const event = req.body;
  if (event.type !== "message" || event.message.fromMe) return;
  if (seen.has(event.message.id)) return;
  seen.add(event.message.id);

  if (event.message.type === "text" && event.reply) {
    await fetch(event.reply, {
      method: "POST",
      headers: { "x-maytapi-key": TOKEN, "Content-Type": "application/json" },
      body: JSON.stringify({ to_number: event.conversation, type: "text", message: "Thanks, we got your message." })
    });
  }
});

app.listen(3000);

Auto-replies are where automation most often trips WhatsApp's spam detection, so keep them relevant and rate-limited. The ban prevention guide explains why.

Next steps

Published May 6, 2025 · Updated Oct 27, 2026 ← All posts
Начните работать с WhatsApp API

Получите ключ и отправьте первое сообщение за десять минут

Без одобрения, без шаблонов, без карты. Все эндпоинты открыты на три дня.