Dev

How to receive WhatsApp messages via webhook (Node.js example)

Asking the API every few seconds whether a message arrived is slow, wasteful and scales badly. A webhook flips it: every incoming message becomes a POST to a URL you control, and your system reacts within seconds. It is the base of chatbots, CRMs and support automations. Here you get the full flow, a real Node.js example with Express and a production checklist so you neither lose messages nor hit timeouts. The code works with any provider that sends events by webhook, and at the end I show how it looks on ZapZap API.

Published on · Updated on · 8 min read

This article is also available in: Português · Español

What a webhook is and why it beats polling

A webhook is an HTTP endpoint on your server that waits to be called. When something happens on the WhatsApp side, such as an incoming message, a read receipt or a change in connection status, the provider sends a POST to your URL with the event data in the request body. You process that body and reply 200. That simple. The difference from polling is huge: with polling you have to ask every so often whether something new arrived, which wastes resources, adds delay and scales badly when you have many connected numbers.

With a webhook, delivery is practically instant and you are only triggered when there is actually something to handle. This lowers infrastructure cost and improves the experience: a customer sends a message and your bot can reply in a second or two. For support, lead qualification and sales automation, that low latency has a direct impact on conversion.

The price of this architecture is that you must keep a public, stable and fast endpoint. If it goes down or takes too long, the provider may consider the delivery failed and retry later, or in some cases stop trying. That is why handling the response and idempotency well is not a detail: it is what separates a toy integration from one that survives production.

What the payload of an incoming message looks like

The exact format varies between providers, but the conceptual structure is always similar. The POST body usually carries a field indicating the event type (for example messages or message), the identifier of the instance or number that received it, the sender data (number, WhatsApp name), the message content and a boolean field saying whether the message was sent by you (the famous fromMe).

That fromMe is one of the biggest causes of bugs in WhatsApp integrations. When your own system sends a message, many providers also fire a webhook event for it. If you do not filter by fromMe, your bot may reply to its own message and enter a loop. The practical rule: at the start of the handler, if the event is for a message sent by you, return 200 and do nothing.

It is also worth handling the content types. Plain text is the simplest case, but you will receive audio, images, documents, locations, button replies and reactions. Each arrives with a different field structure. Start with text, make sure the flow works, and only then add the other types as your product really needs them. Do not try to cover everything at once.

Before writing any heavy logic, log the raw body you receive in a test environment and send a real message to the number. Seeing the real JSON arrive is the fastest and most honest way to understand your provider’s format. Documentation helps, but the real payload rules.

Practical example: a webhook in Node.js with Express

Below is a minimal handler that already includes the safeguards that matter. It uses Express, parses JSON, ignores your own messages, extracts the text and replies 200 fast. Heavy processing (calling the database, an AI, sending a reply) stays out of the critical path of the HTTP response.

  • ```js
  • const express = require('express');
  • const app = express();
  • app.use(express.json({ limit: '2mb' }));
  • app.post('/webhook', (req, res) => {
  • // 1. Reply fast: acknowledge receipt first
  • res.sendStatus(200);
  • const evento = req.body;
  • // 2. Ignore messages sent by yourself (avoids a loop)
  • const msg = evento?.message || evento?.data || {};
  • if (msg.fromMe) return;
  • // 3. Extract the fields you need
  • const de = msg.sender || msg.from;
  • const texto = msg.text || msg.body || '';
  • if (!texto) return; // for now, only handle text
  • // 4. Process outside the HTTP response path
  • processarMensagem(de, texto).catch((e) =>
  • console.error('error processing', e)
  • );
  • });
  • async function processarMensagem(de, texto) {
  • console.log(`Message from ${de}: ${texto}`);
  • // here you call your database, your AI, or send the reply
  • }
  • app.listen(3000, () => console.log('webhook listening on port 3000'));
  • ```

Notice the order: res.sendStatus(200) comes before processing. That is intentional. The provider only needs to know you received it; it does not need to wait while you think up the answer. If you leave the provider waiting for an AI to generate text or the database to respond, you risk blowing its timeout and receiving the same message again in a retry. Acknowledge first, work later.

The field names in the example (message, data, sender, text) are illustrative and may change depending on the provider. Adjust them after seeing the real payload. If you use ZapZap API, the exact formats of each event type are described in a machine-readable way at api.zapzapapi.com/llms.txt, which serves as a single source for building your parser without guessing.

Best practices to avoid losing messages in production

Always reply with a 2xx and do it fast. A webhook that takes more than a few seconds is a problematic webhook. Move any slow work, such as an AI call, a slow database write or sending a reply, out of the request, using a queue or at least asynchronous processing that does not block the response. The endpoint has a single synchronous obligation: to say “received”.

Treat redeliveries as expected, not as an exception. Networks fail, timeouts happen, and a serious provider will resend the event if it did not get your 200. That means the same message can arrive twice. If every arrival triggers a reply or a write, you create duplicates. The defense is idempotency: store the unique message ID and, before processing, check whether you already handled that ID. If you did, reply 200 and stop.

Validate the webhook origin. Your URL is public, so anyone who discovers it can send fake POSTs. Depending on the provider you validate a token in the header, an HMAC signature or a secret in the URL itself. Never blindly trust the body you receive. And log failed deliveries: if a POST of yours returns an error, you want to know which message it was and why, so you can reprocess it without losing the lead.

An easy detail to forget: in a development environment your local machine has no public URL. Use a tunnel tool to expose the local port during tests, or point the webhook to a real staging server. Creating the instance with the webhook pointing to localhost simply receives nothing.

Receiving WhatsApp messages with ZapZap API

ZapZap API is an unofficial, instance-based WhatsApp API: you connect a real number by scanning the QR code and start sending and receiving messages through the API, without depending on the approval and queues of the official API. Each active number costs R$15 per month on the prepaid model, and there is a welcome credit (subject to the current campaign). For anyone who needs to get an automation running quickly without verification bureaucracy, it is the most direct path.

In practice, you register your webhook URL and each instance starts forwarding the events it receives to your endpoint in the same POST format shown above. You plug in the Node.js handler, filter fromMe, handle idempotency and you already have a working bot. The configuration details and payload formats are documented on the webhook integration page, and the general reference of available features is on the WhatsApp API page.

Beyond the basics of sending and receiving, the platform adds features that matter when you operate a real unofficial number: community warm-up of instances to give the number a history, anti-ban mechanisms that reduce the risk of restriction, AI configurable per instance and modules for specific flows. You receive the message on your webhook and decide whether to reply with your own logic, with the instance’s AI, or forward it to your CRM.

A note of honesty: no solution built on an unofficial number removes the risk of restriction. Warm-up and anti-ban reduce that risk and make the operation more predictable, and the webhook is the front door to build on top of that, spreading volume and behaving like a real user. With the handler ready, the next step is to connect an instance and watch the first message land on your endpoint.

Quick summary

  • A webhook delivers messages in real time via POST to your server, with no polling and low latency.
  • Always reply 200 fast and process the heavy work asynchronously, outside the HTTP response path.
  • Filter fromMe at the start of the handler so the bot does not reply to its own message and enter a loop.
  • Handle redeliveries with idempotency (store the message ID) and validate the webhook origin by token or signature.
  • On ZapZap API, each instance forwards events to your URL; formats are on the webhook integration page and at api.zapzapapi.com/llms.txt.

Also read

Frequently asked questions

Do I need a server with a fixed IP to receive webhooks?

You do not need a fixed IP, but you do need a public, stable URL with HTTPS. In production, use your own domain pointing to your server or cloud service. In development, use a tunnel tool to temporarily expose your local port, since localhost does not receive external calls.

Why does my bot keep replying to its own message?

Because you are not filtering the fromMe field. Many providers also fire a webhook event for the messages your system sends. Check fromMe at the start of the handler and, if it is true, return 200 without processing. This cuts the loop at the source.

What happens if my server goes down and I do not reply 200?

The provider considers the delivery failed and usually tries to resend the event later, within a retry window. That is why you should handle redeliveries with idempotency: store the message ID and ignore duplicates. If failures persist for a long time, you may lose events, so monitor and log failed deliveries.

Can I receive messages without the official WhatsApp API?

Yes. ZapZap API uses instances with a real number connected by QR code, which lets you send and receive by webhook without the approval and queues of the official API. The cost is R$15 per active number per month, prepaid, with a welcome credit when you create the account, subject to the current campaign. Keep in mind that an unofficial number always carries some risk of restriction, mitigated by warm-up and anti-ban.

Get your first message on your endpoint

Connect a number by QR code and register your webhook URL. Verify your phone and you get a welcome credit to test without depositing (one per phone, subject to the current campaign).