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.

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.
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.
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.
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.
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.
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.
{
"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"
}
}
}
}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.
- 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.
- 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.
- 03
Verify, acknowledge, act
Check the webhook-signature header, return 2xx, then deduplicate on eventId: delivery is at least once.
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.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);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.
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.
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.
Group monitoring
In a group, participant names who posted and channel.conversation.participants lists the members, refreshed when the group changes.
Team alerts
Forward an urgent message to Slack or by email, or wake someone up when account.status_changed reports a disconnection.
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.
| Meta Cloud APIOfficial, direct | Twilio WhatsAppOfficial, via a provider | Unlimited MessagingLinked device, paid plans | |
|---|---|---|---|
| What you set up first | Meta app, business portfolio, WhatsApp Business Account | Twilio account, Meta Business Portfolio, a WABA created through Twilio | A QR code scan from your phone |
| Your current number | Registered to the platform | Must not already be registered with WhatsApp | Kept, still usable on your phone |
| Approval and limits | 250 business-initiated recipients a day until verified; App Review for tech providers | Meta business verification before production | None from Meta; WhatsApp's own anti-spam rules apply |
| Endpoint handshake | GET verification with hub.challenge, valid TLS, no self-signed | Webhook URL set on the sender | None: register the URL, receive |
| Signature | X-Hub-Signature-256, HMAC-SHA256 with the app secret | X-Twilio-Signature, HMAC-SHA1 with the auth token | Standard Webhooks, HMAC-SHA256 with a per-endpoint secret |
| Body format | JSON, nested entry/changes | Form-encoded (application/x-www-form-urlencoded) | JSON, same envelope for email and SMS |
| Retries | Decreasing frequency for up to 7 days | Not stated on the webhook docs | 8 attempts over a little more than 3 days |
| Cost of receiving and replying | Service replies free up to 1,000 per number a month, then charged; templates charged | $0.005 per message, inbound or outbound, plus Meta's fees | No 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.
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.
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.
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.
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.
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
Sources
Sources & references
To check the information in this article for yourself:
- Getting started with webhooks (verification, X-Hub-Signature-256, TLS)Meta for Developers
- Set up webhooks for WhatsApp (retries for up to 7 days)Meta for Developers
- WhatsApp Cloud API - get started (Meta app, business portfolio)Meta for Developers
- Messaging limits (250 for new business portfolios)Meta for Developers
- Get started for tech providers (business verification and App Review)Meta for Developers
- WhatsApp Business Platform pricingMeta for Developers
- WhatsApp self sign-up (number must not be registered with WhatsApp)Twilio Docs
- WhatsApp pricing ($0.005 per message, inbound or outbound)Twilio
- Webhooks security (X-Twilio-Signature, HMAC-SHA1)Twilio Docs
- Standard Webhooks specificationstandardwebhooks.com
- About linked devices (logout after 14 days without the main phone)WhatsApp Help Center
- Unlimited Messaging API reference - WebhooksUnlimited Messaging

Written by
Ilian · 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.
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.