Skip to main content

WhatsApp webhook

Receive WhatsApp messages on your server,
from the number you already use.

Meta's webhooks come with a Meta app, a business portfolio and a number moved to the API. Link your existing WhatsApp instead, and each incoming message arrives at your URL as signed JSON.

In a hurry? Summarize this article with an AI

The short answer

Can you get a WhatsApp webhook without Meta's Business API?

Yes. Unlimited Messaging links your existing WhatsApp number as a linked device, like WhatsApp Web, and posts every incoming message to your URL as a Standard Webhooks signed JSON event called message.received. There is no Meta app to create, no business verification and no fee per message. Webhooks are on the paid Medium and Max plans.

The official route is Meta's WhatsApp Cloud API. To receive its webhooks you create a Meta app with the WhatsApp use case, attach a business portfolio and a WhatsApp Business Account, answer a GET verification request with hub.challenge, and serve a valid TLS certificate: Meta does not accept self-signed ones. A new business portfolio can then message only 250 people a day first (outside the 24-hour window) until it is verified, and a company building this for clients needs Meta's App Review on top.

Unlimited Messaging takes the other path: you scan a QR code from WhatsApp, Linked devices, and the number keeps working on your phone. Each message that reaches it is stored and pushed to your endpoint with the sender, the text, the conversation, and for a photo or a voice note a mediaUrl that serves the file. Your answer goes back through POST /message from the same number, so the customer sees one ordinary chat.

The trade-off is honest: this is the unofficial linked-device protocol, not a Meta product. It suits replies and notifications to people who already talk to you. Mass messaging strangers gets numbers banned, whatever the tool.

Before you build: the free WhatsApp API options compared, and how to keep a linked number safe from bans.

Illustration: a phone showing a WhatsApp-style chat next to a green chat badge, with the headline “WhatsApp webhook, no Meta app”.

The events

Which WhatsApp events can you receive by webhook?

Ten event types, each chosen per endpoint: an endpoint only receives what is in its eventTypes list. These are the ones a WhatsApp inbox needs.

  1. message.received

    A message arrived, in a 1:1 chat or a group: text, image, video, document, audio, voice note (ptt), sticker or location. Fired once per message; a media message is announced once its file is stored, with mediaUrl.

  2. message.self_sent

    You typed a message on the phone itself, outside the API. Same shape as message.received with direction OUT, so your CRM sees both sides of the chat.

  3. message.reaction, message.edited, message.deleted

    Someone reacted with an emoji (or took it back), edited a message, or deleted it for everyone. Each names its target by externalId, and can arrive before the message it changes.

  4. message.sent and message.status

    A message you sent by API reached WhatsApp, then moved to DELIVERED, READ, FAILED or UNDELIVERABLE. message.status carries previousStatus so you can ignore a stale receipt.

  5. account.status_changed

    The number connected, disconnected, was logged out or restricted by WhatsApp (BLOCKED, with blockedUntil). Subscribe to it so a silent inbox never goes unnoticed.

POST https://example.com/webhooks/umJSON
{
  "type": "message.received",
  "eventId": "whd_6f1c2a9e-1b7d-4c55-9a43-2f0e8b7d3c11",
  "occurredAt": "2026-11-09T10:00:01.000Z",
  "data": {
    "message": {
      "id": "msg_0e55ff33-854a-482c-8510-68226e09cdcb",
      "externalId": "3EB0A1C2D3E4F5061728",
      "direction": "IN",
      "fromMe": false,
      "content": "Hi! Is my order ready for pickup?",
      "sentAt": "2026-11-09T10:00:00.000Z",
      "receivedAt": "2026-11-09T10:00:01.000Z",
      "sender": "+447700900123",
      "senderId": "[email protected]",
      "senderName": "Lea",
      "participant": null,
      "replyTo": null,
      "transcript": null
    },
    "channel": {
      "type": "WHATSAPP",
      "account": { "id": "acc_373bc415-447a-4813-9b61-a3a09ea9aaf3", "name": "Support" },
      "conversation": {
        "id": "[email protected]",
        "isGroup": false,
        "address": "+447700900123"
      }
    }
  }
}
A real message.received body, built from the published schema. sender is +E.164, or null when WhatsApp hides the number, in which case senderId identifies the person. Events are not delivered in order: sort on sentAt.

Set it up

How do you receive incoming WhatsApp messages in Node.js?

Three steps: link the number, register an endpoint, verify and handle each POST. Answer within 10 seconds and do the work after.

  1. 01

    Link your WhatsApp

    In the web app, add a WhatsApp account and scan the QR code from WhatsApp, Settings, Linked devices. The number stays on your phone.

  2. 02

    Register the endpoint

    POST /webhooks with your URL and eventTypes, or add it on the Webhooks page of the dashboard. The secret (whsec_...) is shown once.

  3. 03

    Verify, acknowledge, act

    Check the webhook-signature header, return 2xx, then deduplicate on eventId: delivery is at least once.

POST /webhookscurl
curl https://api.unlimitedmessaging.app/webhooks \
  -H "Authorization: Bearer $UM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/um",
    "eventTypes": ["message.received", "account.status_changed"],
    "description": "Incoming WhatsApp"
  }'

# The response carries "secret": "whsec_..." once. Store it as UM_WEBHOOK_SECRET.
accountIds is optional: without it the endpoint covers every account you have, including numbers linked later. PATCH /webhooks/{id} replaces the eventTypes list.
webhook handlerNode.js / Express
import crypto from 'node:crypto';
import express from 'express';
import { UnlimitedMessagingApiClient } from '@unlimited-messaging/sdk';

const um = new UnlimitedMessagingApiClient({ token: process.env.UM_API_KEY! });
const key = Buffer.from(process.env.UM_WEBHOOK_SECRET!.replace(/^whsec_/, ''), 'base64');
const seen = new Set<string>(); // use your database in production

// Standard Webhooks: HMAC-SHA256 of "id.timestamp.rawBody", base64.
function verify(raw: string, id: string, ts: string, header: string) {
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = crypto.createHmac('sha256', key).update(`${id}.${ts}.${raw}`).digest('base64');
  return header.split(' ').some((entry) => {
    const sig = entry.startsWith('v1,') ? entry.slice(3) : '';
    return sig.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  });
}

const app = express();
app.post('/webhooks/um', express.raw({ type: 'application/json' }), async (req, res) => {
  const raw = req.body.toString('utf8');
  const ok = verify(raw, req.get('webhook-id') ?? '', req.get('webhook-timestamp') ?? '',
    req.get('webhook-signature') ?? '');
  if (!ok) return res.sendStatus(401);
  res.sendStatus(200); // answer within 10 s, work afterwards

  const event = JSON.parse(raw);
  if (seen.has(event.eventId)) return; // at-least-once delivery
  seen.add(event.eventId);

  if (event.type === 'message.received' && event.data.channel.type === 'WHATSAPP') {
    const { id, content } = event.data.message;
    if (content?.toLowerCase().includes('order')) {
      // Answers in the same chat, from the same linked number.
      await um.messages.messageSend({ replyToMessageId: id, text: 'Checking your order now.' });
    }
  }
});
app.listen(3000);
The signature check follows the documented scheme: HMAC-SHA256 over id.timestamp.rawBody with the base64-decoded secret, timestamps older than 5 minutes rejected. Any Standard Webhooks library does the same. replyToMessageId answers in the same chat.

Vibe coding

Building with Cursor, Claude Code, Lovable or Bolt? Paste this

The prompt gives your AI editor the endpoint, the signature scheme and the event shape, so it writes the webhook route instead of guessing.

Prompt to copy
Add an Unlimited Messaging webhook to this app so it receives incoming WhatsApp messages from my own number.

1. Create a POST route at /webhooks/um that reads the RAW request body (no JSON parsing before verification).
2. Verify the Standard Webhooks signature: key = base64-decode(UM_WEBHOOK_SECRET without its "whsec_" prefix); expected = base64(HMAC-SHA256(key, `${webhook-id}.${webhook-timestamp}.${rawBody}`)); accept if any space-separated entry of the webhook-signature header equals `v1,${expected}` (constant-time compare); reject timestamps more than 5 minutes off. Return 401 if invalid.
3. Return 200 at once, then process. Deduplicate on body.eventId.
4. On type "message.received" with data.channel.type "WHATSAPP", read data.message.content, data.message.sender (+E164 or null), data.message.senderName and data.message.mediaUrl, and save them.
5. To reply, install @unlimited-messaging/sdk, read UM_API_KEY from env, and call `new UnlimitedMessagingApiClient({ token }).messages.messageSend({ replyToMessageId: data.message.id, text })`.
6. Add UM_API_KEY and UM_WEBHOOK_SECRET to .env and .env.example, never hardcode them.

Docs: https://docs.unlimitedmessaging.app (Webhooks section). Register the URL with POST https://api.unlimitedmessaging.app/webhooks and eventTypes ["message.received"].

What people build

What can you do with incoming WhatsApp messages?

Anything that starts with "when a customer writes". A few that fit the linked-number model well.

An AI auto-responder on your real number

Pass content to Claude or GPT, send the answer back with replyToMessageId. Customers keep writing to the number they saved, and you can take over from the phone at any time.

WhatsApp into your CRM or helpdesk

Store every message.received and message.self_sent against the contact, keyed on sender or senderId, so the whole conversation lives next to the deal or the ticket.

  1. Order and booking lookups

    Match a keyword or an order number in the text, look it up, answer in seconds. Media arrive as a stored file you can fetch.

  2. Group monitoring

    In a group, participant names who posted and channel.conversation.participants lists the members, refreshed when the group changes.

  3. Team alerts

    Forward an urgent message to Slack or by email, or wake someone up when account.status_changed reports a disconnection.

  4. No-code flows

    Point the webhook at n8n, Make or Zapier and branch on type and channel.type without writing a server.

The table

Incoming WhatsApp webhooks: Meta Cloud API vs Twilio vs Unlimited Messaging

Meta and Twilio are the official Business Platform, with its approvals and per-message pricing. Unlimited Messaging links the number you already have.

Incoming WhatsApp webhooks: Meta Cloud API vs Twilio vs Unlimited Messaging
Meta Cloud APIOfficial, directTwilio WhatsAppOfficial, via a providerUnlimited MessagingLinked device, paid plans
What you set up firstMeta app, business portfolio, WhatsApp Business AccountTwilio account, Meta Business Portfolio, a WABA created through TwilioA QR code scan from your phone
Your current numberRegistered to the platformMust not already be registered with WhatsAppKept, still usable on your phone
Approval and limits250 business-initiated recipients a day until verified; App Review for tech providersMeta business verification before productionNone from Meta; WhatsApp's own anti-spam rules apply
Endpoint handshakeGET verification with hub.challenge, valid TLS, no self-signedWebhook URL set on the senderNone: register the URL, receive
SignatureX-Hub-Signature-256, HMAC-SHA256 with the app secretX-Twilio-Signature, HMAC-SHA1 with the auth tokenStandard Webhooks, HMAC-SHA256 with a per-endpoint secret
Body formatJSON, nested entry/changesForm-encoded (application/x-www-form-urlencoded)JSON, same envelope for email and SMS
RetriesDecreasing frequency for up to 7 daysNot stated on the webhook docs8 attempts over a little more than 3 days
Cost of receiving and replyingService replies free up to 1,000 per number a month, then charged; templates charged$0.005 per message, inbound or outbound, plus Meta's feesNo per-message fee; webhooks need Medium or Max

Checked on 9 November 2026 on Meta's webhook, messaging-limit and pricing docs, Twilio's WhatsApp self sign-up guide, pricing and webhook security pages (links in the sources).

The catch

What are the limits of a linked-number WhatsApp webhook?

Skipping Meta removes the approvals. It does not remove WhatsApp's rules or the realities of a linked device.

  1. Webhooks are a paid feature

    The Free plan sends and shows incoming messages in the web app, but pushing them to your server needs Medium (€19 a month, or €17 a month billed yearly) or Max. No per-message fee on either.

  2. It is unofficial

    The linked-device protocol is the one WhatsApp Web uses, not a Meta API. WhatsApp can restrict a number that spams or gets reported: account.status_changed tells you when it happens, with blockedUntil when known.

  3. A linked device follows WhatsApp's rules

    WhatsApp logs linked devices out if the main phone stays offline for 14 days. Open WhatsApp on the phone now and then and the link keeps running with the phone off.

  4. Order and duplicates are your job

    Delivery is at least once and not in order: a text sent right after a photo usually arrives first, since the photo waits for its file to be stored. Deduplicate on eventId, sort on sentAt.

  5. History is opt-in

    Only messages received after linking are announced, unless the account was linked with syncHistory, which sends past chats as message.history batches of up to 100 (history media are described, not stored).

FAQ

Questions about WhatsApp webhooks

Ilian, senior software engineer

Written by

· Senior Software Engineer

A senior software engineer, he founds and runs several tech companies. He builds products around messaging APIs, automation and AI integration, and shares here what he learns while building.

View LinkedIn profileUpdated

Your WhatsApp messages, on your server.

Create an account, link your number by QR code, and turn on webhooks with Medium or Max. Sending stays free on every plan.