Guides
WhatsApp Cloud API Developer Guide: Webhooks, Limits, Errors and Code
The parts of Meta's WhatsApp Cloud API docs you need on day one — webhook handshake and payload shape, the limits that actually bite, the errors you will see, and working Node.js and PHP calls.
By the wbm.link teamUpdated 14 September 2026 9 min read
Meta's documentation for the WhatsApp Business Platform is thorough, but it is spread across dozens of pages, and the numbers that matter in production — how fast you can send, what a webhook looks like, what error 131047 means — sit in different places. This guide pulls them together, checked against Meta's docs in September 2026. If you have not created your app and test number yet, start with the WhatsApp Cloud API setup guide.
WhatsApp Business API REST API documentation: where it lives
The Cloud API is not a separate service with its own SDK — it is a set of endpoints on Meta's Graph API, called over HTTPS at graph.facebook.com with a bearer token. Everything official is on Meta for Developers:
- WhatsApp Business Platform docs — guides for getting started, messages, templates, webhooks, phone numbers and limits. Older links under developers.facebook.com/docs/whatsapp redirect or still work.
- API reference — each endpoint (messages, phone numbers, WhatsApp Business Account, templates, media) with fields and sample requests.
- Graph API changelog — the current version. At the time of writing that is v26.0, released 29 July 2026. Put the version in every URL and upgrade deliberately.
- Error codes page — the full list of Cloud API error codes, summarised below.
- Webhooks reference — one page per webhook field, with sample payloads for each message and status type.
The two IDs you use most: the Phone Number ID for sending (POST /PHONE_NUMBER_ID/messages) and the WhatsApp Business Account ID for templates, phone number lists and webhook subscriptions.
Webhooks: verification first
Incoming messages and delivery updates reach you only through webhooks. When you save a callback URL in the app's WhatsApp configuration, Meta first sends a GET request to it with three query parameters:
- hub.mode — always subscribe.
- hub.verify_token — the string you typed into the Verify Token field.
- hub.challenge — a value you must send back.
Your endpoint checks that hub.verify_token matches your own secret and responds with status 200 and the hub.challenge value as the body. Anything else and Meta will not save the URL. Then subscribe the app to the messages webhook field.
After that, every event arrives as a POST with a JSON body and an X-Hub-Signature-256 header containing sha256= followed by an HMAC-SHA256 of the raw request body, keyed with your app secret. Compute the same HMAC over the raw bytes and compare before trusting the payload — parse the JSON only after the check.
WhatsApp Business API webhook payload structure
Every WhatsApp webhook has the same envelope: object is whatsapp_business_account; entry is an array whose items carry an id (the WABA ID) and a changes array; each change has a field (for example messages) and a value holding the event. Inside value for the messages field you will find:
| Field in value | What it tells you |
|---|---|
| messaging_product | Always whatsapp |
| metadata.display_phone_number | Your business number, as digits |
| metadata.phone_number_id | Which of your numbers the event belongs to — route on this |
| contacts[].profile.name, contacts[].wa_id | The customer's WhatsApp profile name and WhatsApp ID |
| messages[] | Incoming messages: from, id (a wamid), timestamp (Unix seconds), type, and a type-specific object such as text.body |
| statuses[] | Updates on messages you sent: id (the wamid you got when sending), status, timestamp, recipient_id, and conversation and pricing objects with billing details |
| statuses[].errors | Present when status is failed — carries the error code and title |
A trimmed incoming text message, as in Meta's reference:
{"object":"whatsapp_business_account","entry":[{"id":"WABA_ID","changes":[{"field":"messages","value":{"messaging_product":"whatsapp","metadata":{"display_phone_number":"15550783881","phone_number_id":"PHONE_NUMBER_ID"},"contacts":[{"profile":{"name":"Sheena Nelson"},"wa_id":"16505551234"}],"messages":[{"from":"16505551234","id":"wamid.HBgL...","timestamp":"1749416383","type":"text","text":{"body":"Does it come in another color?"}}]}}]}]}
A status update uses the same envelope, with statuses in place of contacts and messages:
"statuses":[{"id":"wamid.HBgL...","status":"delivered","timestamp":"1750263773","recipient_id":"16505551234","pricing":{"billable":true,"category":"service"}}]
Status values move through sent, delivered and read, or end at failed. They can arrive out of order, and one POST can batch several entries or changes — loop over every level rather than reading index 0.
WhatsApp Business API webhook integration example
A minimal Node.js (Express) handler, with the verify token and app secret in environment variables:
- app.get("/webhook", (req, res) => { const ok = req.query["hub.mode"] === "subscribe" && req.query["hub.verify_token"] === process.env.VERIFY_TOKEN; ok ? res.status(200).send(req.query["hub.challenge"]) : res.sendStatus(403); });
- app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => { const sig = "sha256=" + crypto.createHmac("sha256", process.env.APP_SECRET).update(req.body).digest("hex"); if (sig !== req.get("x-hub-signature-256")) return res.sendStatus(401); res.sendStatus(200); handle(JSON.parse(req.body)); });
- Answer 200 fast, work later. Acknowledge the POST, then push the payload to a queue or background job. If Meta does not get a 200, it retries with decreasing frequency for up to 7 days.
- Deduplicate on the wamid. Retries mean the same message can arrive twice.
- Expect big bodies. WhatsApp webhook payloads can be up to 3 MB. Media arrives as an ID you download separately, not as bytes in the webhook.
- Use a timing-safe comparison for the signature in production (crypto.timingSafeEqual in Node).
WhatsApp Business API rate limits explained
“Rate limit” means four different things on the Cloud API. Mixing them up is why teams raise their throughput and still get errors.
| Limit | What Meta documents | Error code or remedy |
|---|---|---|
| Throughput (per business number) | 80 messages per second by default; automatically upgraded up to 1,000 mps for eligible numbers. Numbers also on the WhatsApp Business app (coexistence) are fixed at 20 mps. | 130429 |
| Pair rate (to one customer) | 1 message every 6 seconds to the same WhatsApp user, with bursts of up to 45 messages in 6 seconds that must be paid back by waiting | 131056 |
| Messaging limit (unique customers) | Business-initiated conversations with unique users in a rolling 24 hours: 250 for new portfolios, then 2,000, 10,000, 100,000 and unlimited. Set per business portfolio and shared by all its numbers. | Raise it through verification and good quality, not retries |
| Graph API request rate | Requests per app per WhatsApp Business Account in a rolling hour: 200 by default, 5,000 for an active WABA with a registered number | 4 (app) or 80007 (WABA) |
The automatic throughput upgrade to 1,000 mps requires an unlimited messaging limit, messaging 100,000 or more unique users outside the customer-service window in a moving 24 hours, and a quality rating of yellow or better. The upgrade can take up to a minute, during which sends return 131057.
WhatsApp Business API error codes list
Errors come back in the API response as an error object with a code, and for asynchronous failures in the errors array of a failed status webhook. The codes you will meet most often, from Meta's error codes page:
| Code | Meaning | What to do |
|---|---|---|
| 0 | Authentication failed | Get a new access token — the current one expired or was invalidated |
| 190 | Access token has expired | Generate a new token; move to a system user token for production |
| 3 / 10 | Capability or permission not granted, or removed | Check the token's permissions in the access token debugger |
| 100 | Invalid parameter — unsupported or misspelled | Compare the request body with the endpoint reference |
| 4 | App has hit its API call rate limit | Back off and retry later |
| 80007 | WhatsApp Business Account has reached its rate limit | Reduce request frequency; retry later |
| 130429 | Cloud API message throughput reached | Slow down sending or queue messages |
| 131026 | Message undeliverable — not on WhatsApp, old app version or terms not accepted | Confirm the number with the customer; do not retry blindly |
| 131042 | Payment method problem | Fix the payment method or credit line on the WABA |
| 131047 | More than 24 hours since the customer last replied | Send an approved template instead of free-form text |
| 131049 | Not delivered to maintain healthy ecosystem engagement | Do not resend immediately; wait at least 24 hours |
| 131050 | Customer has stopped marketing messages from you | Do not retry; respect the opt-out |
| 131051 | Unsupported message type | Use a type listed in the messages reference |
| 131056 | Too many messages to the same recipient in a short time | Wait before messaging that user again |
| 132000 | Template variable count does not match | Send exactly as many parameters as the template defines |
| 132001 | Template does not exist in that language or is not approved | Check the template name, language code and approval status |
| 132015 / 132016 | Template paused / permanently disabled for low quality | Edit the template, or create a new one if disabled |
| 133010 | Phone number not registered on the platform | Call the register endpoint for the number |
| 368 | Account restricted for a policy violation | Review the enforcement notice in WhatsApp Manager |
Log the full error object, not just the code — the message and any details alongside it usually say which parameter or rule failed. Meta's error codes page lists every code, including the registration and two-step verification errors (the 133xxx range) you meet when adding numbers.
Sending messages from your code
Every send is the same request: POST to https://graph.facebook.com/v26.0/PHONE_NUMBER_ID/messages with a bearer token and a JSON body containing messaging_product whatsapp, the recipient in to (international format, no plus sign), a type, and an object for that type.
WhatsApp Business API Node.js integration example
Node.js 18 and later ship fetch, so no library is needed. Sending an approved template:
- const url = "https://graph.facebook.com/v26.0/" + process.env.PHONE_NUMBER_ID + "/messages";
- const res = await fetch(url, { method: "POST", headers: { Authorization: "Bearer " + process.env.WA_TOKEN, "Content-Type": "application/json" },
- body: JSON.stringify({ messaging_product: "whatsapp", to: "91XXXXXXXXXX", type: "template", template: { name: "order_update", language: { code: "en" }, components: [{ type: "body", parameters: [{ type: "text", text: "A1042" }] }] } }) });
- const data = await res.json(); if (!res.ok) throw new Error(data.error.code + ": " + data.error.message);
- console.log(data.messages[0].id); // wamid — match it to status webhooks
For a free-form reply inside the 24-hour window, change the body to type text with text: { body: "Your order ships today" }.
WhatsApp Business API PHP SDK example (cURL)
Meta does not publish a PHP SDK for the WhatsApp Cloud API. PHP's built-in cURL extension is all you need to call the REST API — here, a text reply:
- $payload = json_encode(["messaging_product" => "whatsapp", "to" => "91XXXXXXXXXX", "type" => "text", "text" => ["body" => "Your order ships today"]]);
- $ch = curl_init("https://graph.facebook.com/v26.0/" . getenv("PHONE_NUMBER_ID") . "/messages");
- curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_POSTFIELDS => $payload,
- CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("WA_TOKEN"), "Content-Type: application/json"]]);
- $result = json_decode(curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch);
- if ($status !== 200) { error_log($result["error"]["code"] . ": " . $result["error"]["message"]); }
Third-party PHP packages for the Cloud API are available on Packagist and GitHub. They are not maintained by Meta, so check that a package tracks recent Graph API versions before you depend on it.
A production checklist
- Use a system user token stored as a server secret, never a temporary token.
- Pin the Graph API version in one config value and plan upgrades from the changelog.
- Verify signatures on every webhook and acknowledge within your handler before heavy work.
- Queue outbound sends and respect throughput (130429) and pair rate (131056) with backoff.
- Store wamid → your record so status webhooks update the right message.
- Handle 131047 by switching to a template, and 131050 by marking the contact opted out.
- Watch template quality — a paused template (132015) will fail every send until fixed.
Building this versus using a panel
Everything above is what it takes to send and receive reliably. A team inbox, agent assignment, template management for non-developers, broadcasts and a chatbot are further projects on top. If WhatsApp is a feature inside your own product, build on the Cloud API directly.
If your goal is simply for your team to work on WhatsApp, wbm.link gives you that as a no-code panel on the WhatsApp Business Platform — shared inbox, template builder that submits to Meta, broadcasts to labels or CSV contacts, keyword auto-replies, a flow chatbot and an AI assistant — for ₹999 per connected number per month with unlimited team members. It has no public API or customer webhooks, so it is not a replacement for a custom integration; it is the choice when you do not want to write one.
No webhooks to host
Connect your WhatsApp number to wbm.link and your team can reply, broadcast and automate from day one — without writing or maintaining integration code.
Try wbm.link