{"id":679,"date":"2026-06-09T09:21:42","date_gmt":"2026-06-09T09:21:42","guid":{"rendered":"https:\/\/messagebot.in\/blog\/?p=679"},"modified":"2026-06-09T09:21:42","modified_gmt":"2026-06-09T09:21:42","slug":"whatsapp-webhooks-explained","status":"publish","type":"post","link":"https:\/\/messagebot.in\/blog\/whatsapp-webhooks-explained\/","title":{"rendered":"WhatsApp Webhooks Explained (2026): Setup, Events, Payloads &#038; Best Practices"},"content":{"rendered":"<p><span style=\"font-weight: 400;\">A customer just messaged your business on WhatsApp. Does your system know about it yet? Not in 30 seconds. Not in 5 seconds. Right now &#8211; the moment it happened. <\/span><span style=\"font-weight: 400;\">If the answer is no, this guide is for you. And even if you think the answer is yes, keep reading because most teams don&#8217;t realise how much is silently failing until they look closely enough.<\/span><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone wp-image-692 size-full\" src=\"https:\/\/messagebot.in\/blog\/wp-content\/uploads\/2026\/06\/whatsapp-webhooks.png\" alt=\"whatsapp-webhooks\" width=\"1920\" height=\"1080\" \/><span style=\"font-weight: 400;\">WhatsApp webhooks are the real-time event layer underneath every reliable WhatsApp integration. Chatbots, OTP confirmations, order tracking, support escalations, and CRM updates none of it works the way users expect without webhooks done right. <\/span><span style=\"font-weight: 400;\">This guide covers everything: what webhooks are, how to set them up, what every payload means, and how to build around them properly. Written for developers and product teams both.<\/span><\/p>\n<h2><b>What WhatsApp Webhooks Actually Are<\/b><\/h2>\n<p><span style=\"font-weight: 400;\">Think of your business as a busy store. Every time a customer walks in, a staff member comes to you directly. &#8220;Rahul&#8217;s here, he wants to track his order.&#8221; You didn&#8217;t go outside to check. The information came to you, that&#8217;s webhooks in digital form.\u00a0<\/span><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-697\" src=\"https:\/\/messagebot.in\/blog\/wp-content\/uploads\/2026\/06\/whatsapp-webhooks-1.jpg\" alt=\"how a whatsApp webhook works in real time\" width=\"1920\" height=\"1080\" \/><span style=\"font-weight: 400;\">When something happens on WhatsApp a customer sends a message, your OTP gets delivered, someone reads your notification, a template gets paused. Meta&#8217;s servers immediately fire an HTTP POST request to a URL you&#8217;ve registered. Full event details, JSON payload, delivered to your door. Your server receives it, processes it, responds <\/span><span style=\"font-weight: 400;\">200 OK<\/span><span style=\"font-weight: 400;\">. That&#8217;s the whole flow.<\/span><\/p>\n<p><b>What this unlocks for your business:<\/b><\/p>\n<ul>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">The moment a customer messages you, your chatbot or support system activates\u00a0 no polling delay<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">OTP delivery confirmation arrives in real time, feeding directly into your retry logic<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">You find out a template got paused before a campaign fails, not during it<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">Order updates reach customers the second something changes in your system<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">Read receipts tell you which messages were actually opened, not just sent<\/span><\/li>\n<\/ul>\n<p><span style=\"font-weight: 400;\">Without webhooks, all of this is either delayed or invisible.<\/span><\/p>\n<h2><b>Webhooks vs Polling: The Business Impact<\/b><\/h2>\n<p><span style=\"font-weight: 400;\">Polling is when your server keeps asking Meta &#8220;any new messages? any updates? anything?&#8221;\u00a0 over and over, whether something happened or not. <\/span><span style=\"font-weight: 400;\">Webhooks flip that. Meta tells you when something happens. Your server does nothing until it does.<\/span><\/p>\n<table>\n<tbody>\n<tr>\n<td><\/td>\n<td><b>Polling<\/b><\/td>\n<td><b>Webhooks<\/b><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">Response time<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Seconds to minutes<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Milliseconds to low seconds<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">Server load<\/span><\/td>\n<td><span style=\"font-weight: 400;\">High &#8211; constant outbound requests<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Low &#8211; fires only on events<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">OTP confirmation<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Delayed<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Instant<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">Missed events<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Common if interval is too long<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Rare &#8211; Meta retries for 7 days<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">Campaign monitoring<\/span><\/td>\n<td><span style=\"font-weight: 400;\">You check manually<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Automatic event alerts<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">At scale<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Gets worse as volume grows<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Handles spikes naturally<\/span><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><b>Here&#8217;s what polling costs in the real world:<\/b><span style=\"font-weight: 400;\"> A fintech user is going through KYC, waiting for an OTP. Polling system, 15 to 30 seconds to confirm whether the OTP even delivered. Webhook system, that confirmation is there in 1 to 2 seconds. In the first case, users think the system has crashed. A lot of them abandon the flow entirely.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">In India, where OTP flows power banking, fintech onboarding, and e-commerce checkout that delay is a conversion problem, not just a technical one. It&#8217;s one of the reasons businesses running WhatsApp OTP alongside SMS fallback need both channels working in real time. More on that in the<\/span><a href=\"https:\/\/messagebot.in\/blog\/whatsapp-otp-service-india\/\"> <span style=\"font-weight: 400;\">WhatsApp OTP Service guide for India<\/span><\/a><span style=\"font-weight: 400;\">.<\/span><\/p>\n<h2><b>How the Webhook Lifecycle Works<\/b><\/h2>\n<p><span style=\"font-weight: 400;\">Before WhatsApp sends a single event to your server, it needs to trust your endpoint. That&#8217;s Phase 1. After that, events start flowing. That&#8217;s Phase 2.<\/span><\/p>\n<h3><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-699\" src=\"https:\/\/messagebot.in\/blog\/wp-content\/uploads\/2026\/06\/webhook-life-cycle-works.jpg\" alt=\"webhook-life-cycle\" width=\"1920\" height=\"1080\" \/>Phase 1: Verification (One Time)<\/h3>\n<p><span style=\"font-weight: 400;\">When you register a webhook URL in the <a href=\"https:\/\/developers.facebook.com\/docs\/development\/create-an-app\/app-dashboard\/\" target=\"_blank\" rel=\"noopener\">Meta App Dashboard<\/a>, Meta sends a <\/span><span style=\"font-weight: 400;\">GET<\/span><span style=\"font-weight: 400;\"> request to your endpoint with three parameters:<\/span><\/p>\n<ul>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">hub.mode<\/span><span style=\"font-weight: 400;\"> &#8211; always <\/span><span style=\"font-weight: 400;\">subscribe<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">hub.verify_token<\/span><span style=\"font-weight: 400;\"> &#8211; the string you set in the dashboard<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">hub.challenge<\/span><span style=\"font-weight: 400;\"> &#8211; a random string Meta generates<\/span><\/li>\n<\/ul>\n<p><span style=\"font-weight: 400;\">Your job: confirm the mode and token match, then return the challenge value with HTTP 200. That&#8217;s it. Meta sees the correct response and marks your endpoint as verified.<\/span><\/p>\n<blockquote><p><em><span style=\"color: #ff0000;\"><span style=\"font-weight: 400;\">javascript<br \/>\n<\/span><span style=\"font-weight: 400;\">app<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">get<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">&#8216;\/webhook&#8217;<\/span><span style=\"font-weight: 400;\">,<\/span> <span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">req<\/span><span style=\"font-weight: 400;\">,<\/span><span style=\"font-weight: 400;\"> res<\/span><span style=\"font-weight: 400;\">)<\/span><span style=\"font-weight: 400;\"> =&gt; <\/span><span style=\"font-weight: 400;\">{<br \/>\n<\/span><span style=\"font-weight: 400;\">const<\/span><span style=\"font-weight: 400;\"> mode = req<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">query<\/span><span style=\"font-weight: 400;\">[<\/span><span style=\"font-weight: 400;\">&#8216;hub.mode&#8217;<\/span><span style=\"font-weight: 400;\">];<br \/>\n<\/span><span style=\"font-weight: 400;\">const<\/span><span style=\"font-weight: 400;\"> token = req<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">query<\/span><span style=\"font-weight: 400;\">[<\/span><span style=\"font-weight: 400;\">&#8216;hub.verify_token&#8217;<\/span><span style=\"font-weight: 400;\">];<br \/>\n<\/span><span style=\"font-weight: 400;\">const<\/span><span style=\"font-weight: 400;\"> challenge = req<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">query<\/span><span style=\"font-weight: 400;\">[<\/span><span style=\"font-weight: 400;\">&#8216;hub.challenge&#8217;<\/span><span style=\"font-weight: 400;\">];<br \/>\n<\/span><span style=\"font-weight: 400;\">if<\/span> <span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">mode === <\/span><span style=\"font-weight: 400;\">&#8216;subscribe&#8217;<\/span><span style=\"font-weight: 400;\"> &amp;&amp; token === process<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">env<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">VERIFY_TOKEN<\/span><span style=\"font-weight: 400;\">)<\/span> <span style=\"font-weight: 400;\">{<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0res<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">status<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">200<\/span><span style=\"font-weight: 400;\">).<\/span><span style=\"font-weight: 400;\">send<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">challenge<\/span><span style=\"font-weight: 400;\">);<br \/>\n<\/span><span style=\"font-weight: 400;\">}<\/span> <span style=\"font-weight: 400;\">else<\/span><span style=\"font-weight: 400;\">{<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0 \u00a0res<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">sendStatus<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">403<\/span><span style=\"font-weight: 400;\">);<br \/>\n<\/span><span style=\"font-weight: 400;\">}<br \/>\n<\/span><span style=\"font-weight: 400;\">});<\/span><\/span><\/em><\/p><\/blockquote>\n<p><span style=\"font-weight: 400;\">Most common reason this fails: the URL isn&#8217;t publicly accessible. <\/span><span style=\"font-weight: 400;\">localhost<\/span><span style=\"font-weight: 400;\"> doesn&#8217;t work. Use ngrok during development; it gives you a temporary public HTTPS URL that tunnels to your local machine.<\/span><\/p>\n<h3><b>Phase 2: Event Notifications (Ongoing)<\/b><\/h3>\n<p><span style=\"font-weight: 400;\">Once verified, every subscribed event triggers a <\/span><span style=\"font-weight: 400;\">POST<\/span><span style=\"font-weight: 400;\"> request to your endpoint. Every single payload regardless of event type &#8211; follows this wrapper structure:<\/span><\/p>\n<blockquote><p><em><span style=\"color: #ff0000;\"><span style=\"font-weight: 400;\">json<br \/>\n<\/span><span style=\"font-weight: 400;\">{<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;object&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;whatsapp_business_account&#8221;<\/span><span style=\"font-weight: 400;\">,<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;entry&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">[{<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;id&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;WABA_ID&#8221;<\/span><span style=\"font-weight: 400;\">,<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;changes&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">[{<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;value&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">{<\/span><span style=\"font-weight: 400;\"> &#8230; <\/span><span style=\"font-weight: 400;\">},<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;field&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;messages&#8221;<br \/>\n<\/span><span style=\"font-weight: 400;\">}]<br \/>\n<\/span><span style=\"font-weight: 400;\">}]<br \/>\n<\/span><span style=\"font-weight: 400;\">}<\/span><\/span><\/em><\/p><\/blockquote>\n<p><span style=\"font-weight: 400;\">field<\/span><span style=\"font-weight: 400;\"> tells you what category of event this is. <\/span><span style=\"font-weight: 400;\">value<\/span><span style=\"font-weight: 400;\"> holds everything specific to that event the message content, the delivery status, the template change, whatever happened.<\/span><\/p>\n<h2><b>Which Events You Can Subscribe To<\/b><\/h2>\n<p><span style=\"font-weight: 400;\">You don&#8217;t get everything by default. In the Meta App Dashboard, you choose which event fields to subscribe to. Here&#8217;s what&#8217;s available and who actually needs it:<\/span><\/p>\n<table>\n<tbody>\n<tr>\n<td><b>Field<\/b><\/td>\n<td><b>What You Receive<\/b><\/td>\n<td><b>Who Needs It<\/b><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">messages<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Inbound user messages + status updates on your outbound messages<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Everyone \u2014 this is the core field<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">message_template_status_update<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Template approved, rejected, paused, disabled, reinstated<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Every business using templates<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">account_update<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Policy violations, account restrictions<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Enterprise teams<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">phone_number_quality_update<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Quality rating changes for your registered number<\/span><\/td>\n<td><span style=\"font-weight: 400;\">High-volume senders<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">phone_number_name_update<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Display name approval or rejection<\/span><\/td>\n<td><span style=\"font-weight: 400;\">During onboarding<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">business_capability_update<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Messaging capability changes to your account<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Platform builders<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">security<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Unusual login activity, token events<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Security-conscious integrations<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">flows<\/span><\/td>\n<td><span style=\"font-weight: 400;\">WhatsApp Flows status updates<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Businesses using Flows<\/span><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3><b>The two you should subscribe to from day one:<\/b><\/h3>\n<p><span style=\"font-weight: 400;\">messages<\/span><span style=\"font-weight: 400;\"> &#8211; handles both inbound customer messages and outbound delivery tracking in one field. Without this, you&#8217;re flying blind.<br \/>\n<\/span><span style=\"font-weight: 400;\">message_template_status_update<\/span><span style=\"font-weight: 400;\"> &#8211; this is the one most teams skip, and it&#8217;s the one that causes the most painful surprises. More on that in the payloads section below.<\/span><\/p>\n<h2><b>Real Payloads: What Arrives and What It Means<\/b><\/h2>\n<h3><b>1. Customer Sent a Text Message<\/b><\/h3>\n<blockquote><p><em><span style=\"color: #ff0000;\"><span style=\"font-weight: 400;\">json<br \/>\n<\/span><span style=\"font-weight: 400;\">{<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;messages&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">[{<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;from&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;919876543210&#8221;<\/span><span style=\"font-weight: 400;\">,<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;id&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;wamid.XXXXX&#8221;<\/span><span style=\"font-weight: 400;\">,<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;timestamp&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;1717500000&#8221;<\/span><span style=\"font-weight: 400;\">,<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;type&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;text&#8221;<\/span><span style=\"font-weight: 400;\">,<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;text&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">{<\/span> <span style=\"font-weight: 400;\">&#8220;body&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;Where is my order?&#8221;<\/span> <span style=\"font-weight: 400;\">}<br \/>\n<\/span><span style=\"font-weight: 400;\">}],<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;contacts&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">[{<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;profile&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">{<\/span> <span style=\"font-weight: 400;\">&#8220;name&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;Priya Mehta&#8221;<\/span> <span style=\"font-weight: 400;\">},<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;wa_id&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;919876543210&#8221;<br \/>\n<\/span><span style=\"font-weight: 400;\">}]<br \/>\n<\/span><span style=\"font-weight: 400;\">}<\/span><\/span><\/em><\/p><\/blockquote>\n<p><span style=\"font-weight: 400;\">A customer just reached out. <\/span><span style=\"font-weight: 400;\">from<\/span><span style=\"font-weight: 400;\"> identifies them in your system, <\/span><span style=\"font-weight: 400;\">text.body<\/span><span style=\"font-weight: 400;\"> tells you what they want, <\/span><span style=\"font-weight: 400;\">id<\/span><span style=\"font-weight: 400;\"> is what you reference when you reply. This is the trigger point for everything &#8211; chatbot response, support ticket creation, CRM lookup, agent assignment.<\/span><\/p>\n<h3><b>2. Your Message Was Delivered or Read<\/b><\/h3>\n<blockquote><p><span style=\"color: #ff0000;\"><span style=\"font-weight: 400;\">json<br \/>\n<\/span><span style=\"font-weight: 400;\">{<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;statuses&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">[{<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;id&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;wamid.XXXXX&#8221;<\/span><span style=\"font-weight: 400;\">,<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;status&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;delivered&#8221;<\/span><span style=\"font-weight: 400;\">,<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;timestamp&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;1717500060&#8221;<\/span><span style=\"font-weight: 400;\">,<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;recipient_id&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;919876543210&#8221;<\/span><span style=\"font-weight: 400;\">,<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;conversation&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">{<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;origin&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">{<\/span> <span style=\"font-weight: 400;\">&#8220;type&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;utility&#8221;<\/span> <span style=\"font-weight: 400;\"><br \/>\n<\/span><span style=\"font-weight: 400;\">},<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;pricing&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">{<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;billable&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">true<\/span><span style=\"font-weight: 400;\">,<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;category&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;utility&#8221;<br \/>\n<\/span><span style=\"font-weight: 400;\">}<br \/>\n<\/span><span style=\"font-weight: 400;\">}]<br \/>\n<\/span><span style=\"font-weight: 400;\">}<\/span><\/span><\/p><\/blockquote>\n<p><span style=\"font-weight: 400;\">Status values move in sequence: <\/span><span style=\"font-weight: 400;\">sent<\/span><span style=\"font-weight: 400;\"> \u2192 <\/span><span style=\"font-weight: 400;\">delivered<\/span><span style=\"font-weight: 400;\"> \u2192 <\/span><span style=\"font-weight: 400;\">read<\/span><span style=\"font-weight: 400;\">. Each means something different:<\/span><\/p>\n<ul>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">sent<\/span><span style=\"font-weight: 400;\"> &#8211; Meta accepted it and is attempting delivery<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">delivered<\/span><span style=\"font-weight: 400;\"> &#8211; it reached the user&#8217;s device<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">read<\/span><span style=\"font-weight: 400;\"> &#8211; the user actually opened it<\/span><\/li>\n<\/ul>\n<p><span style=\"font-weight: 400;\">pricing.category<\/span><span style=\"font-weight: 400;\"> &#8211;\u00a0<\/span><span style=\"font-weight: 400;\">utility<\/span><span style=\"font-weight: 400;\">, <\/span><span style=\"font-weight: 400;\">authentication<\/span><span style=\"font-weight: 400;\">, <\/span><span style=\"font-weight: 400;\">marketing<\/span><span style=\"font-weight: 400;\">, or <\/span><span style=\"font-weight: 400;\">service<\/span><span style=\"font-weight: 400;\"> &#8211; maps directly to your conversation billing. Log this for cost tracking. It also tells you whether a conversation opened as a utility (transactional) or marketing session, which matters when you&#8217;re analysing campaign ROI. The full pricing breakdown for Indian businesses is covered in the<\/span><a href=\"https:\/\/messagebot.in\/blog\/whatsapp-business-api-pricing-in-india\/\"> <span style=\"font-weight: 400;\">WhatsApp Business API Pricing guide<\/span><\/a><span style=\"font-weight: 400;\">.<\/span><\/p>\n<h3><b>3. Message Failed to Deliver<\/b><\/h3>\n<blockquote><p><span style=\"color: #ff0000;\"><span style=\"font-weight: 400;\">json<br \/>\n<\/span><span style=\"font-weight: 400;\">{<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;statuses&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">[{<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;id&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;wamid.XXXXX&#8221;<\/span><span style=\"font-weight: 400;\">,<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;status&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;failed&#8221;<\/span><span style=\"font-weight: 400;\">,<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;recipient_id&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;919876543210&#8221;<\/span><span style=\"font-weight: 400;\">,<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;errors&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">[{<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;code&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">131026<\/span><span style=\"font-weight: 400;\">,<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;error_data&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">{<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;details&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;This user&#8217;s phone number is not registered on WhatsApp.&#8221;<br \/>\n<\/span><span style=\"font-weight: 400;\">}<br \/>\n<\/span><span style=\"font-weight: 400;\">}]<br \/>\n<\/span><span style=\"font-weight: 400;\">}]<br \/>\n<\/span><span style=\"font-weight: 400;\">}<\/span><\/span><\/p><\/blockquote>\n<p><span style=\"font-weight: 400;\">This is the payload most teams don&#8217;t process and it&#8217;s the most consequential one. Your API call returned 200. Meta accepted the request. But the message never actually reached the user. The failure arrives here, asynchronously, via webhook.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">If you&#8217;re not handling <\/span><span style=\"font-weight: 400;\">failed<\/span><span style=\"font-weight: 400;\"> status events, you have no visibility into how many of your messages are silently not reaching people. Error code <\/span><span style=\"font-weight: 400;\">131026<\/span><span style=\"font-weight: 400;\"> means the number isn&#8217;t on WhatsApp,\u00a0 trigger your SMS fallback immediately. Error code <\/span><span style=\"font-weight: 400;\">131050<\/span><span style=\"font-weight: 400;\"> means the user opted out \u2014 remove them from future campaigns and don&#8217;t retry.<\/span><\/p>\n<h3><b>4. Template Got Paused<\/b><\/h3>\n<blockquote><p><span style=\"color: #ff0000;\"><span style=\"font-weight: 400;\">json<br \/>\n<\/span><span style=\"font-weight: 400;\">{<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;changes&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">[{<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;value&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">{<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;event&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;PAUSED&#8221;<\/span><span style=\"font-weight: 400;\">,<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;message_template_name&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;order_confirmation&#8221;<\/span><span style=\"font-weight: 400;\">,<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;message_template_language&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;en&#8221;<\/span><span style=\"font-weight: 400;\">,<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;reason&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;LOW_QUALITY&#8221;<br \/>\n<\/span><span style=\"font-weight: 400;\">},<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;field&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;message_template_status_update&#8221;<br \/>\n<\/span><span style=\"font-weight: 400;\">}]<br \/>\n<\/span><span style=\"font-weight: 400;\">}<\/span><\/span><\/p><\/blockquote>\n<p><span style=\"font-weight: 400;\">Meta paused your template. Usually because too many recipients blocked or reported it during pacing. Meta&#8217;s quality check before a full campaign send. Possible <\/span><span style=\"font-weight: 400;\">event<\/span><span style=\"font-weight: 400;\"> values: <\/span><span style=\"font-weight: 400;\">APPROVED<\/span><span style=\"font-weight: 400;\">, <\/span><span style=\"font-weight: 400;\">REJECTED<\/span><span style=\"font-weight: 400;\">, <\/span><span style=\"font-weight: 400;\">PAUSED<\/span><span style=\"font-weight: 400;\">, <\/span><span style=\"font-weight: 400;\">DISABLED<\/span><span style=\"font-weight: 400;\">, <\/span><span style=\"font-weight: 400;\">REINSTATED<\/span><span style=\"font-weight: 400;\">.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Here&#8217;s what happens when teams don&#8217;t subscribe to this field: a bulk campaign starts sending, the template is already paused, thousands of messages fail silently, and nobody knows until customer complaints start coming in. Subscribe to <\/span><span style=\"font-weight: 400;\">message_template_status_update<\/span><span style=\"font-weight: 400;\">, alert your team the moment a <\/span><span style=\"font-weight: 400;\">PAUSED<\/span><span style=\"font-weight: 400;\"> event arrives, and stop affected sends immediately. Template quality management and the warm-up approach that prevents this are covered in the<\/span><a href=\"https:\/\/messagebot.in\/blog\/whatsapp-broadcast-messaging-india\/\"> <span style=\"font-weight: 400;\">WhatsApp Broadcast Messaging guide<\/span><\/a><span style=\"font-weight: 400;\">.<\/span><\/p>\n<h3><b>4. Customer Sent Media<\/b><\/h3>\n<blockquote><p><span style=\"color: #ff0000;\"><span style=\"font-weight: 400;\">json<br \/>\n<\/span><span style=\"font-weight: 400;\">{<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;messages&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">[{<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;from&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;919876543210&#8221;<\/span><span style=\"font-weight: 400;\">,<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;type&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;image&#8221;<\/span><span style=\"font-weight: 400;\">,<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;image&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">{<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;mime_type&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;image\/jpeg&#8221;<\/span><span style=\"font-weight: 400;\">,<br \/>\n<\/span><span style=\"font-weight: 400;\">&#8220;id&#8221;<\/span><span style=\"font-weight: 400;\">: <\/span><span style=\"font-weight: 400;\">&#8220;MEDIA_ID&#8221;<br \/>\n<\/span><span style=\"font-weight: 400;\">}<br \/>\n<\/span><span style=\"font-weight: 400;\">}]<br \/>\n<\/span><span style=\"font-weight: 400;\">}<\/span><\/span><\/p><\/blockquote>\n<p><span style=\"font-weight: 400;\">The <\/span><span style=\"font-weight: 400;\">id<\/span><span style=\"font-weight: 400;\"> here is a media reference, not a URL. To get the actual file, you make a separate API call using that ID. One important thing: media IDs expire after 30 days. If you need to store the file for a support ticket, a KYC document, a complaint image \u2014 download it to your own server promptly.<\/span><\/p>\n<h2><b>Real Business Workflows Powered by Webhooks<\/b><\/h2>\n<p><span style=\"font-weight: 400;\">This is where webhooks stop being a technical concept and start being a business tool. Every real-time WhatsApp experience your customers have is built on the event stream webhooks deliver.<\/span><\/p>\n<h3><b>OTP Verification Flow<\/b><\/h3>\n<p><span style=\"font-weight: 400;\">A user enters their phone number on your app. Your backend sends a WhatsApp OTP via the API. The moment the <\/span><span style=\"font-weight: 400;\">delivered<\/span><span style=\"font-weight: 400;\"> status webhook arrives, your timer starts. If the <\/span><span style=\"font-weight: 400;\">read<\/span><span style=\"font-weight: 400;\"> status comes and the user still hasn&#8217;t entered the OTP after a reasonable window, that&#8217;s your signal to offer a resend option not an arbitrary timer.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">If a <\/span><span style=\"font-weight: 400;\">failed<\/span><span style=\"font-weight: 400;\"> status arrives with error <\/span><span style=\"font-weight: 400;\">131026<\/span><span style=\"font-weight: 400;\"> (number not on WhatsApp), your system immediately falls back to SMS OTP without the user having to do anything. That seamless fallback is invisible to the user and only possible because you&#8217;re processing webhook events in real time. <\/span><span style=\"font-weight: 400;\">This is exactly how MessageBot&#8217;s<\/span> <a href=\"https:\/\/messagebot.in\/services\/whatsapp\/otp\"><span style=\"font-weight: 400;\">WhatsApp OTP service<\/span><\/a><span style=\"font-weight: 400;\"> handles delivery confirmation webhook-driven, with automatic SMS fallback when WhatsApp delivery fails.<\/span><\/p>\n<h3><b>Order Tracking &amp; Notifications<\/b><\/h3>\n<p><span style=\"font-weight: 400;\">A customer places an order. Your order management system triggers a webhook to MessageBot&#8217;s WhatsApp API,\u00a0 order confirmation sent. When the status webhook returns <\/span><span style=\"font-weight: 400;\">delivered<\/span><span style=\"font-weight: 400;\">, the record updates. When it returns <\/span><span style=\"font-weight: 400;\">read<\/span><span style=\"font-weight: 400;\">, you know the customer saw it.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">When the order ships, another trigger tracking link is sent. When delivered, final confirmation. Every step is confirmed, every failure is caught, and your support team isn&#8217;t fielding &#8220;where&#8217;s my order?&#8221; calls because customers already know<\/span><\/p>\n<h3><b>CRM Auto-Updates<\/b><\/h3>\n<p><span style=\"font-weight: 400;\">A customer replies to a promotional message. The inbound message webhook fires. Your system checks the sender&#8217;s number against your CRM, if they&#8217;re an existing contact, the conversation gets logged. If they&#8217;re new, a lead record gets created automatically. <\/span><span style=\"font-weight: 400;\">The sales team sees the conversation appear in their CRM within seconds of the customer replying. No manual entry, no delayed sync, no missed leads.<\/span><\/p>\n<h3><b>Support Chatbot with Agent Escalation<\/b><\/h3>\n<p><span style=\"font-weight: 400;\">A customer messages your business. The inbound webhook triggers your chatbot. The bot handles the query &#8211; tracks the order, answers the FAQ, processes the return. <\/span><span style=\"font-weight: 400;\">But then the customer types &#8220;I want to speak to someone.&#8221; Sentiment analysis or a keyword trigger fires. The webhook event that just arrived gets routed to your support queue instead of the bot handler. A human agent picks it up &#8211; with the full conversation context already there.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">The 24-hour conversation window is open, pricing is at the service rate, and the transition from bot to human is seamless. None of that handoff works without the inbound message webhook as the trigger point.<\/span><\/p>\n<h3><b>Lead Routing from WhatsApp Campaigns<\/b><\/h3>\n<p><span style=\"font-weight: 400;\">You run a<\/span> <span style=\"font-weight: 400;\">WhatsApp broadcast campaign,<\/span><span style=\"font-weight: 400;\">\u00a0a promotional message with a quick reply button. Every time a user taps a reply, an inbound message webhook fires. Your system reads the reply content, identifies the intent, and routes the lead:<\/span><\/p>\n<ul>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">&#8220;Show pricing&#8221; \u2192 routes to sales pipeline, triggers a pricing template<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">&#8220;Book a demo&#8221; \u2192 creates a calendar entry, sends a confirmation<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">&#8220;Not interested&#8221; \u2192 marks the contact, removes from future campaign lists<\/span><\/li>\n<\/ul>\n<p><span style=\"font-weight: 400;\">All automated, all real time, all driven by webhook events.<\/span><\/p>\n<h3><b>Bulk SMS Fallback Trigger<\/b><\/h3>\n<p><span style=\"font-weight: 400;\">Not every customer is on WhatsApp. And even WhatsApp users sometimes have delivery failures &#8211; network issues, outdated app versions, opted-out numbers. <\/span><span style=\"font-weight: 400;\">When a <\/span><span style=\"font-weight: 400;\">failed<\/span><span style=\"font-weight: 400;\"> status webhook arrives, your system doesn&#8217;t just log it. It immediately triggers an SMS via MessageBot&#8217;s<\/span> <a href=\"https:\/\/messagebot.in\/services\/sms\"><span style=\"font-weight: 400;\">Bulk SMS API<\/span><\/a><span style=\"font-weight: 400;\">\u00a0 &#8211;\u00a0 same message, different channel, delivered within seconds. The customer never notices the WhatsApp failure. You maintain delivery rates across your entire user base. <\/span><span style=\"font-weight: 400;\">This kind of omnichannel fallback is one of the core reasons businesses use a single platform for both WhatsApp API and Bulk SMS one event triggers the other automatically.<\/span><\/p>\n<h2><b>Step-by-Step Setup on Meta App Dashboard<\/b><\/h2>\n<p><b>Before you start, have these ready:<\/b><\/p>\n<ul>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">Meta Developer account with an app created<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">Verified WhatsApp Business Account (WABA)<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">A publicly accessible HTTPS URL &#8211; self-signed certificates will be rejected<\/span><\/li>\n<\/ul>\n<p><b>Step 1: <\/b><span style=\"font-weight: 400;\">Go to Meta App Dashboard \u2192 <\/span><span style=\"font-weight: 400;\">WhatsApp \u2192 Configuration<\/span><\/p>\n<p><b>Step 2:<\/b><span style=\"font-weight: 400;\"> Click &#8220;Edit&#8221; under Webhook. Enter your callback URL and set a Verify Token &#8211; any secure string, stored as an environment variable, never hardcoded<\/span><\/p>\n<p><b>Step 3:<\/b><span style=\"font-weight: 400;\">\u00a0Click &#8220;Verify and Save.&#8221; Meta immediately sends the GET verification request. Your endpoint responds correctly, verification completes.<\/span><\/p>\n<p><b>Step 4:<\/b><span style=\"font-weight: 400;\">\u00a0Click &#8220;Manage&#8221; next to Webhook Fields. Subscribe to <\/span><span style=\"font-weight: 400;\">messages<\/span><span style=\"font-weight: 400;\"> and <\/span><span style=\"font-weight: 400;\">message_template_status_update<\/span><span style=\"font-weight: 400;\"> at minimum.<\/span><\/p>\n<p><b>Step 5:<\/b><span style=\"font-weight: 400;\">\u00a0Send a real WhatsApp message to your registered number. Your endpoint should receive a POST payload within seconds.<\/span><\/p>\n<p><b>For local testing:<\/b><\/p>\n<blockquote><p><span style=\"color: #ff0000;\"><span style=\"font-weight: 400;\">bash<br \/>\n<\/span><span style=\"font-weight: 400;\">ngrok http <\/span><span style=\"font-weight: 400;\">3000<br \/>\n<\/span><span style=\"font-weight: 400;\"># Use the generated HTTPS URL as your webhook callback URL<\/span><\/span><\/p><\/blockquote>\n<h2><b>Signature Verification &#8211; The Security Foundation<\/b><\/h2>\n<p><span style=\"font-weight: 400;\">Every POST request Meta sends includes an <\/span><span style=\"font-weight: 400;\">X-Hub-Signature-256<\/span><span style=\"font-weight: 400;\"> header &#8211; an HMAC-SHA256 hash of the raw request body, signed with your App Secret. <\/span><span style=\"font-weight: 400;\">Skip this verification and your webhook endpoint becomes a public API anyone can abuse. A fake <\/span><span style=\"font-weight: 400;\">PAUSED<\/span><span style=\"font-weight: 400;\"> event could halt a live campaign. A fake delivery confirmation could mark a message as delivered when it never was.<\/span><\/p>\n<blockquote><p><span style=\"color: #ff0000;\"><span style=\"font-weight: 400;\">javascript<br \/>\n<\/span><span style=\"font-weight: 400;\">const<\/span><span style=\"font-weight: 400;\"> crypto = <\/span><span style=\"font-weight: 400;\">require<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">&#8216;crypto&#8217;<\/span><span style=\"font-weight: 400;\">);<br \/>\n<\/span><span style=\"font-weight: 400;\">function<\/span> <span style=\"font-weight: 400;\">verifyWebhookSignature<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">rawBody<\/span><span style=\"font-weight: 400;\">,<\/span><span style=\"font-weight: 400;\"> signatureHeader<\/span><span style=\"font-weight: 400;\">)<\/span> <span style=\"font-weight: 400;\">{<br \/>\n<\/span><span style=\"font-weight: 400;\">const<\/span><span style=\"font-weight: 400;\"> expected = <\/span><span style=\"font-weight: 400;\">&#8216;sha256=&#8217;<\/span><span style=\"font-weight: 400;\"> + crypto<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0 <\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">createHmac<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">&#8216;sha256&#8217;<\/span><span style=\"font-weight: 400;\">,<\/span><span style=\"font-weight: 400;\"> process<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">env<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">APP_SECRET<\/span><span style=\"font-weight: 400;\">)<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0 <\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">update<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">rawBody<\/span><span style=\"font-weight: 400;\">)<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">digest<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">&#8216;hex&#8217;<\/span><span style=\"font-weight: 400;\">);<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0<\/span><span style=\"font-weight: 400;\">return<\/span><span style=\"font-weight: 400;\"> crypto<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">timingSafeEqual<\/span><span style=\"font-weight: 400;\">(<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0<\/span><span style=\"font-weight: 400;\">Buffer<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">from<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">signatureHeader<\/span><span style=\"font-weight: 400;\">),<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0 <\/span><span style=\"font-weight: 400;\">Buffer<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">from<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">expected<\/span><span style=\"font-weight: 400;\">)<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0\u00a0<\/span><span style=\"font-weight: 400;\">);<br \/>\n<\/span><span style=\"font-weight: 400;\">}<\/span><\/span><\/p><\/blockquote>\n<p><span style=\"color: #ff0000;\"><span style=\"color: #000000;\"><span style=\"font-weight: 400;\">Two things that matter here &#8211; use <\/span><span style=\"font-weight: 400;\">crypto.timingSafeEqual<\/span><span style=\"font-weight: 400;\"> instead of <\/span><span style=\"font-weight: 400;\">===<\/span><span style=\"font-weight: 400;\"> (standard string comparison is vulnerable to timing attacks), and verify against the <\/span><b>raw request body<\/b><span style=\"font-weight: 400;\"> before parsing. Once a framework parses JSON, the byte representation may not match exactly what Meta signed.<\/span><\/span><\/span><\/p>\n<h2><b>Async Errors &#8211; The Ones That Only Come Via Webhook<\/b><\/h2>\n<p><span style=\"font-weight: 400;\">Here&#8217;s the thing most developers discover the hard way: a 200 response from the WhatsApp API does not mean your message delivered. <\/span><span style=\"font-weight: 400;\">It means Meta accepted the request. What happens after that whether the message actually reached the user arrives later, asynchronously, as a webhook status event. <\/span><span style=\"font-weight: 400;\">If the delivery fails, you get a <\/span><span style=\"font-weight: 400;\">failed<\/span><span style=\"font-weight: 400;\"> status with an error code in the webhook payload. Nothing in the original API response tells you this happened.<\/span><\/p>\n<p><b>The real cost of not processing this:<\/b><\/p>\n<p><span style=\"font-weight: 400;\">A company sends 50,000 order confirmations. Every API call returns 200. But 3,000 of those numbers aren&#8217;t registered on WhatsApp. Without webhook processing, the team assumes all 50,000 delivered. Customer care starts receiving calls. The fallback SMS that should have gone out automatically never triggered.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Process <\/span><span style=\"font-weight: 400;\">failed<\/span><span style=\"font-weight: 400;\"> webhooks, identify the error code, trigger the right response. For <\/span><span style=\"font-weight: 400;\">131026<\/span><span style=\"font-weight: 400;\"> &#8211; fallback to SMS. For <\/span><span style=\"font-weight: 400;\">131050<\/span><span style=\"font-weight: 400;\"> &#8211; remove from campaigns, don&#8217;t retry. The full breakdown of every error code, what triggers it, and what to do about it is in the<\/span><a href=\"https:\/\/messagebot.in\/blog\/whatsapp-api-error-codes\/\"> <span style=\"font-weight: 400;\">WhatsApp API Error Codes guide<\/span><\/a><span style=\"font-weight: 400;\">.<\/span><\/p>\n<h2><b>Retry Behaviour &#8211; What Happens When Your Server Doesn&#8217;t Respond<\/b><\/h2>\n<table>\n<tbody>\n<tr>\n<td><b>Your Response<\/b><\/td>\n<td><b>What Meta Does<\/b><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">200 OK<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Event acknowledged \u2014 done<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">4xx<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Event permanently dropped \u2014 Meta treats it as intentional rejection<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">5xx<\/span><\/td>\n<td>Retries for some time using exponential backoff<\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">Timeout (over 10 seconds)<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Retry with exponential backoff<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">No response<\/span><\/td>\n<td>Retries for some time using exponential backoff<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Retries run for up to <b>7 days<\/b>. After that, the event is gone permanently &#8211; no dead-letter queue, no replay option. <b>The trap nobody warns you about:<\/b><span style=\"font-weight: 400;\"> If your database goes down temporarily and your handler returns <\/span><span style=\"font-weight: 400;\">400<\/span><span style=\"font-weight: 400;\">, that event is lost forever. Meta only retries <\/span><span style=\"font-weight: 400;\">5xx<\/span><span style=\"font-weight: 400;\"> and timeouts. Always return <\/span><span style=\"font-weight: 400;\">500<\/span><span style=\"font-weight: 400;\"> or <\/span><span style=\"font-weight: 400;\">503<\/span><span style=\"font-weight: 400;\"> for recoverable infrastructure errors &#8211; never <\/span><span style=\"font-weight: 400;\">4xx<\/span><span style=\"font-weight: 400;\">.<\/span><\/p>\n<h2><b>Production Architecture \u2014 The Right Way<\/b><\/h2>\n<p><b>Wrong &#8211; synchronous processing:<br \/>\n<\/b><span style=\"font-weight: 400;\">Meta \u2192 Webhook handler \u2192 DB write \u2192 API call \u2192 Response (10+ seconds)<br \/>\n<\/span><span style=\"font-weight: 400;\">Timeout. Retry. Duplicate events. More load. System falls over.<\/span><\/p>\n<p><b>Right &#8211; async queue:<br \/>\n<\/b><span style=\"font-weight: 400;\">Meta \u2192 Webhook handler (under 1 second) \u2192 Queue \u2192 Worker processes<\/span><\/p>\n<p><b>Your handler does three things only:<\/b><\/p>\n<blockquote><p><span style=\"color: #ff0000;\"><span style=\"font-weight: 400;\">javascript<br \/>\n<\/span><span style=\"font-weight: 400;\">app<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">post<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">&#8216;\/webhook&#8217;<\/span><span style=\"font-weight: 400;\">,<\/span> <span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">req<\/span><span style=\"font-weight: 400;\">,<\/span><span style=\"font-weight: 400;\"> res<\/span><span style=\"font-weight: 400;\">)<\/span><span style=\"font-weight: 400;\"> =&gt; <\/span><span style=\"font-weight: 400;\">{<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0<\/span><span style=\"font-weight: 400;\">if<\/span> <span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">!<\/span><span style=\"font-weight: 400;\">verifyWebhookSignature<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">req<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">rawBody<\/span><span style=\"font-weight: 400;\">,<\/span><span style=\"font-weight: 400;\"> req<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">headers<\/span><span style=\"font-weight: 400;\">[<\/span><span style=\"font-weight: 400;\">&#8216;x-hub-signature-256&#8217;<\/span><span style=\"font-weight: 400;\">]))<\/span> <span style=\"font-weight: 400;\">{<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0<\/span><span style=\"font-weight: 400;\">return<\/span><span style=\"font-weight: 400;\"> res<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">sendStatus<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">401<\/span><span style=\"font-weight: 400;\">);<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0\u00a0<\/span><span style=\"font-weight: 400;\">}<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0\u00a0messageQueue<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">add<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">req<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">body<\/span><span style=\"font-weight: 400;\">);<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0\u00a0res<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">sendStatus<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">200<\/span><span style=\"font-weight: 400;\">);<br \/>\n<\/span><span style=\"font-weight: 400;\">});<\/span><\/span><\/p><\/blockquote>\n<p><b>Your worker handles everything else without time pressure:<\/b><\/p>\n<blockquote><p><span style=\"color: #ff0000;\"><span style=\"font-weight: 400;\">javascript<br \/>\n<\/span><span style=\"font-weight: 400;\">messageQueue<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">process<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">async<\/span> <span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">job<\/span><span style=\"font-weight: 400;\">)<\/span><span style=\"font-weight: 400;\"> =&gt; <\/span><span style=\"font-weight: 400;\">{<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0\u00a0<\/span><span style=\"font-weight: 400;\">const<\/span> <span style=\"font-weight: 400;\">{<\/span><span style=\"font-weight: 400;\"> field<\/span><span style=\"font-weight: 400;\">,<\/span><span style=\"font-weight: 400;\"> value <\/span><span style=\"font-weight: 400;\">}<\/span><span style=\"font-weight: 400;\"> = job<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">data<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">entry<\/span><span style=\"font-weight: 400;\">[<\/span><span style=\"font-weight: 400;\">0<\/span><span style=\"font-weight: 400;\">].<\/span><span style=\"font-weight: 400;\">changes<\/span><span style=\"font-weight: 400;\">[<\/span><span style=\"font-weight: 400;\">0<\/span><span style=\"font-weight: 400;\">];<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0\u00a0<\/span><span style=\"font-weight: 400;\">if<\/span> <span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">field === <\/span><span style=\"font-weight: 400;\">&#8216;messages&#8217;<\/span><span style=\"font-weight: 400;\">)<\/span> <span style=\"font-weight: 400;\">{<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0\u00a0\u00a0\u00a0<\/span><span style=\"font-weight: 400;\">if<\/span> <span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">value<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">messages<\/span><span style=\"font-weight: 400;\">)<\/span> <span style=\"font-weight: 400;\">await<\/span> <span style=\"font-weight: 400;\">handleInboundMessage<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">value<\/span><span style=\"font-weight: 400;\">);<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0\u00a0\u00a0\u00a0<\/span><span style=\"font-weight: 400;\">if<\/span> <span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">value<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">statuses<\/span><span style=\"font-weight: 400;\">)<\/span> <span style=\"font-weight: 400;\">await<\/span> <span style=\"font-weight: 400;\">handleStatusUpdate<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">value<\/span><span style=\"font-weight: 400;\">);<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0\u00a0<\/span><span style=\"font-weight: 400;\">}<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0\u00a0<\/span><span style=\"font-weight: 400;\">if<\/span> <span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">field === <\/span><span style=\"font-weight: 400;\">&#8216;message_template_status_update&#8217;<\/span><span style=\"font-weight: 400;\">)<\/span> <span style=\"font-weight: 400;\">{<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0 <\/span><span style=\"font-weight: 400;\">await<\/span> <span style=\"font-weight: 400;\">handleTemplateStatusChange<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">value<\/span><span style=\"font-weight: 400;\">);<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0\u00a0<\/span><span style=\"font-weight: 400;\">}<br \/>\n<\/span><span style=\"font-weight: 400;\">});<\/span><\/span><\/p><\/blockquote>\n<p><b>Why this matters at Indian scale:<\/b><span style=\"font-weight: 400;\"> Diwali campaign launches, IPO application windows, bank KYC rushes &#8211; webhook volume can hit 50x normal within minutes. A synchronous handler with database writes will time out under that load, triggering Meta retries, making the load worse. A queue absorbs the spike and processes at whatever rate your infrastructure can handle.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">For OTP flows, this architecture is especially critical &#8211; the delivery confirmation webhook feeds into your retry decision. Process it in the handler and you risk timeout-triggered duplicates. Process it in the worker and your OTP retry logic stays clean and controlled.<\/span><\/p>\n<p><b>Idempotency &#8211; Handle Duplicate Events<\/b><\/p>\n<p><span style=\"font-weight: 400;\">Meta guarantees <\/span><b>at-least-once<\/b><span style=\"font-weight: 400;\"> delivery &#8211; not exactly-once. The same webhook can arrive multiple times, particularly during retries or outages.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Every message has a unique <\/span><span style=\"font-weight: 400;\">wamid<\/span><span style=\"font-weight: 400;\">. Check before processing:<\/span><\/p>\n<blockquote><p><span style=\"color: #ff0000;\"><span style=\"font-weight: 400;\">javascript<br \/>\n<\/span><span style=\"font-weight: 400;\">async<\/span> <span style=\"font-weight: 400;\">function<\/span> <span style=\"font-weight: 400;\">handleStatusUpdate<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">status<\/span><span style=\"font-weight: 400;\">)<\/span> <span style=\"font-weight: 400;\">{<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0\u00a0<\/span><span style=\"font-weight: 400;\">if<\/span> <span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">await<\/span><span style=\"font-weight: 400;\"> redis<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">get<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">`processed:<\/span><span style=\"font-weight: 400;\">${<\/span><span style=\"font-weight: 400;\">status<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">id<\/span><span style=\"font-weight: 400;\">}<\/span><span style=\"font-weight: 400;\">`<\/span><span style=\"font-weight: 400;\">))<\/span> <span style=\"font-weight: 400;\">return<\/span><span style=\"font-weight: 400;\">;<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0\u00a0<\/span><span style=\"font-weight: 400;\">await<\/span><span style=\"font-weight: 400;\"> redis<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">set<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">`processed:<\/span><span style=\"font-weight: 400;\">${<\/span><span style=\"font-weight: 400;\">status<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">id<\/span><span style=\"font-weight: 400;\">}<\/span><span style=\"font-weight: 400;\">`<\/span><span style=\"font-weight: 400;\">,<\/span> <span style=\"font-weight: 400;\">&#8216;1&#8217;<\/span><span style=\"font-weight: 400;\">,<\/span> <span style=\"font-weight: 400;\">&#8216;EX&#8217;<\/span><span style=\"font-weight: 400;\">,<\/span> <span style=\"font-weight: 400;\">86400<\/span><span style=\"font-weight: 400;\">);<br \/>\n<\/span><span style=\"font-weight: 400;\">\u00a0<\/span><span style=\"font-weight: 400;\">await<\/span> <span style=\"font-weight: 400;\">updateMessageStatus<\/span><span style=\"font-weight: 400;\">(<\/span><span style=\"font-weight: 400;\">status<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">id<\/span><span style=\"font-weight: 400;\">,<\/span><span style=\"font-weight: 400;\"> status<\/span><span style=\"font-weight: 400;\">.<\/span><span style=\"font-weight: 400;\">status<\/span><span style=\"font-weight: 400;\">);<br \/>\n<\/span><span style=\"font-weight: 400;\">}<\/span><\/span><\/p><\/blockquote>\n<p><span style=\"font-weight: 400;\">Without this, a retry storm can mark the same order as delivered multiple times, trigger duplicate SMS fallbacks, or create multiple support tickets from a single customer message.<\/span><\/p>\n<h2><b>Common Mistakes That Are Expensive in Production<\/b><\/h2>\n<p><b>Doing database writes inside the webhook handler:<\/b><span style=\"font-weight: 400;\">\u00a0anything that takes more than a second belongs in the worker. Timeout triggers retries, retries create duplicates, duplicates create chaos.<\/span><\/p>\n<p><b>Skipping signature verification:<\/b><span style=\"font-weight: 400;\">\u00a0a fake <\/span><span style=\"font-weight: 400;\">PAUSED<\/span><span style=\"font-weight: 400;\"> event from an attacker could halt a live campaign. A fake delivery confirmation could corrupt your analytics.<\/span><\/p>\n<p><b>Only watching API responses:<\/b><span style=\"font-weight: 400;\">\u00a0delivery failures, opted-out users, undeliverable numbers all arrive via webhook only. Both channels need to be monitored.<\/span><\/p>\n<p><b>Not subscribing to <\/b><b>message_template_status_update:<\/b><span style=\"font-weight: 400;\">\u00a0finding out a template is paused mid-campaign is very different from getting alerted the second it happens.<\/span><\/p>\n<p><b>Returning <\/b><b>4xx<\/b><b> for transient errors:<\/b><span style=\"font-weight: 400;\">\u00a0database down, cache miss, external API slow. Return <\/span><span style=\"font-weight: 400;\">5xx<\/span><span style=\"font-weight: 400;\">. Meta only retries <\/span><span style=\"font-weight: 400;\">5xx<\/span><span style=\"font-weight: 400;\"> and timeouts.<\/span><\/p>\n<p><b>Not logging <\/b><b>wamid<\/b><b> and <\/b><b>fbtrace_id:<\/b><span style=\"font-weight: 400;\">\u00a0when something breaks in production and you need to escalate to your BSP or Meta, these IDs are how specific events get traced. Without them, debugging is guesswork.<\/span><\/p>\n<p><b>WhatsApp Webhook &#8211; Quick Reference<\/b><\/p>\n<table>\n<tbody>\n<tr>\n<td><b>Feature<\/b><\/td>\n<td><b>Detail<\/b><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">Protocol<\/span><\/td>\n<td><span style=\"font-weight: 400;\">HTTPS only &#8211; valid SSL certificate required<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">HTTP method<\/span><\/td>\n<td><span style=\"font-weight: 400;\">POST (events), GET (verification)<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">Data format<\/span><\/td>\n<td><span style=\"font-weight: 400;\">JSON<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">Authentication<\/span><\/td>\n<td><span style=\"font-weight: 400;\">HMAC-SHA256 via <\/span><span style=\"font-weight: 400;\">X-Hub-Signature-256<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">Timeout window<\/span><\/td>\n<td><span style=\"font-weight: 400;\">5\u201310 seconds<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">Retry duration<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Up to 7 days &#8211; exponential backoff<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">Delivery guarantee<\/span><\/td>\n<td><span style=\"font-weight: 400;\">At-least-once<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">Ordering guarantee<\/span><\/td>\n<td><span style=\"font-weight: 400;\">None &#8211; out-of-order events possible<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">Max payload size<\/span><\/td>\n<td><span style=\"font-weight: 400;\">3MB<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">Dead-letter queue<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Not supported natively<\/span><\/td>\n<\/tr>\n<tr>\n<td><span style=\"font-weight: 400;\">Manual replay<\/span><\/td>\n<td><span style=\"font-weight: 400;\">Not available<\/span><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2><b>Best Practices Checklist<\/b><\/h2>\n<h3><b>Security<\/b><\/h3>\n<ul>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">Verify <\/span><span style=\"font-weight: 400;\">X-Hub-Signature-256<\/span><span style=\"font-weight: 400;\"> on every request using <\/span><span style=\"font-weight: 400;\">timingSafeEqual<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">Verify token and App Secret in environment variables \u2014 never hardcoded<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">HTTPS with valid certificate only<\/span><\/li>\n<\/ul>\n<h3><b>Reliability<\/b><\/h3>\n<ul>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">Return <\/span><span style=\"font-weight: 400;\">200 OK<\/span><span style=\"font-weight: 400;\"> immediately &#8211; async queue for all processing<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">Idempotency on <\/span><span style=\"font-weight: 400;\">wamid<\/span><span style=\"font-weight: 400;\"> &#8211; deduplicate before acting<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">Handle all four status types: <\/span><span style=\"font-weight: 400;\">sent<\/span><span style=\"font-weight: 400;\">, <\/span><span style=\"font-weight: 400;\">delivered<\/span><span style=\"font-weight: 400;\">, <\/span><span style=\"font-weight: 400;\">read<\/span><span style=\"font-weight: 400;\">, <\/span><span style=\"font-weight: 400;\">failed<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">Return <\/span><span style=\"font-weight: 400;\">5xx<\/span><span style=\"font-weight: 400;\"> for transient infrastructure errors &#8211; never <\/span><span style=\"font-weight: 400;\">4xx<\/span><\/li>\n<\/ul>\n<h3><b>Observability<\/b><\/h3>\n<ul>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">Log every incoming payload with timestamp<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">Log <\/span><span style=\"font-weight: 400;\">wamid<\/span><span style=\"font-weight: 400;\"> and <\/span><span style=\"font-weight: 400;\">fbtrace_id<\/span><span style=\"font-weight: 400;\"> for every event<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">Immediate team alert on template <\/span><span style=\"font-weight: 400;\">PAUSED<\/span><span style=\"font-weight: 400;\"> events<\/span><\/li>\n<\/ul>\n<h3><b>Event Coverage<\/b><\/h3>\n<ul>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">messages<\/span><span style=\"font-weight: 400;\"> and <\/span><span style=\"font-weight: 400;\">message_template_status_update<\/span><span style=\"font-weight: 400;\"> subscribed from day one<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">failed<\/span><span style=\"font-weight: 400;\"> status events processed &#8211; not just <\/span><span style=\"font-weight: 400;\">delivered<\/span><span style=\"font-weight: 400;\"> and <\/span><span style=\"font-weight: 400;\">read<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">Account restriction events monitored for high-volume operations<\/span><\/li>\n<\/ul>\n<h2><b>Final Word<\/b><\/h2>\n<p><span style=\"font-weight: 400;\">Webhooks are not a setup task you tick off and forget. They&#8217;re the live pulse of your WhatsApp integration: every message, every delivery confirmation, every template change, every customer reply flows through them. <\/span><span style=\"font-weight: 400;\">Get the architecture right queue-based ingestion, signature verification, idempotency, async processing and you have a system that handles whatever volume comes at it without missing events or creating duplicates.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Get it wrong, and the failures are silent. Customers don&#8217;t get their OTPs. Campaigns run on paused templates. Delivery failures never trigger fallbacks. Nobody knows until the complaints start. <\/span><span style=\"font-weight: 400;\">If you&#8217;re building on WhatsApp Business API and want delivery monitoring, webhook reliability, and SMS fallback built into the same platform :<\/span> <a href=\"https:\/\/messagebot.in\/\"><span style=\"font-weight: 400;\">MessageBot<\/span><\/a><span style=\"font-weight: 400;\"> handles WhatsApp API,<\/span> <span style=\"font-weight: 400;\">Bulk SMS<\/span><span style=\"font-weight: 400;\">, and<\/span> <span style=\"font-weight: 400;\">Voice Call API<\/span><span style=\"font-weight: 400;\"> together, so your fallback logic works automatically without stitching multiple vendors.<br \/>\n<\/span><\/p>\n<h2><b>Frequently Asked Questions<\/b><\/h2>\n<p><b>What is a WhatsApp webhook and how is it different from the API?<br \/>\n<\/b><span style=\"font-weight: 400;\">The WhatsApp API is what you use to send messages when you make a request, Meta delivers the message. A webhook is the reverse: Meta makes a request to you whenever something happens a message arrives, a delivery status changes, a template gets paused. The API is outbound. Webhooks are inbound. Both are needed for a complete integration.<\/span><\/p>\n<p><b>Do I need webhooks if I&#8217;m only sending notifications and not receiving replies?<br \/>\n<\/b><span style=\"font-weight: 400;\">Yes, and here&#8217;s why most people get this wrong. Even if customers never reply, your delivery confirmations (<\/span><span style=\"font-weight: 400;\">delivered<\/span><span style=\"font-weight: 400;\">, <\/span><span style=\"font-weight: 400;\">read<\/span><span style=\"font-weight: 400;\">, <\/span><span style=\"font-weight: 400;\">failed<\/span><span style=\"font-weight: 400;\">) only arrive via webhook. Without processing them, you have no idea which of your messages actually reached people and which silently failed. For any serious notification system OTPs, order updates, payment alerts that visibility is non-negotiable.<\/span><\/p>\n<p><b>Can I use one webhook URL for multiple WhatsApp numbers?<br \/>\n<\/b><span style=\"font-weight: 400;\">Yes. A single webhook endpoint can receive events for multiple phone numbers registered under the same WhatsApp Business Account (WABA). The <\/span><span style=\"font-weight: 400;\">metadata.phone_number_id<\/span><span style=\"font-weight: 400;\"> field in every payload tells you which number the event came from use that to route processing correctly in your system.<\/span><\/p>\n<p><b>Are WhatsApp Webhooks available on WhatsApp Cloud API?<\/b><b><\/b><span style=\"font-weight: 400;\">Yes. WhatsApp Cloud API uses the same webhook mechanism to deliver inbound messages, delivery updates, read receipts, template status changes, and account-related events. In fact, webhooks are required for most production Cloud API implementations because many important events are only delivered through webhook notifications.<\/span><\/p>\n<p><b>Can I use webhooks to trigger SMS if WhatsApp delivery fails?<br \/>\n<\/b><b><span style=\"font-weight: 400;\">Yes, and this is one of the most valuable things you can build. When a <\/span><span style=\"font-weight: 400;\">failed<\/span><span style=\"font-weight: 400;\"> status webhook arrives with error code <\/span><span style=\"font-weight: 400;\">131026<\/span><span style=\"font-weight: 400;\"> (number not on WhatsApp) or a similar delivery failure, your system can immediately trigger an SMS fallback through MessageBot&#8217;s Bulk SMS API. The customer never notices the WhatsApp failure &#8211; they just get the message through a different channel. This kind of automatic omnichannel fallback is exactly why having WhatsApp API and<\/span> <span style=\"font-weight: 400;\">Bulk SMS<\/span><span style=\"font-weight: 400;\"> on the same platform makes operational sense.<\/span><br \/>\n<\/b><\/p>\n<p><b>Why did my API call return 200 but the message never delivered?<br \/>\n<\/b><span style=\"font-weight: 400;\">Because <\/span><span style=\"font-weight: 400;\">200 OK<\/span><span style=\"font-weight: 400;\"> from the API only means Meta accepted your request &#8211; not that the message reached the user. Actual delivery confirmation comes asynchronously via webhook as a <\/span><span style=\"font-weight: 400;\">status<\/span><span style=\"font-weight: 400;\"> update. If the delivery failed, you&#8217;ll see a <\/span><span style=\"font-weight: 400;\">failed<\/span><span style=\"font-weight: 400;\"> status with an error code in the webhook payload. This is the most common gap in WhatsApp integrations &#8211; teams only watch API responses and miss the entire delivery failure layer.\u00a0<\/span><\/p>\n","protected":false},"excerpt":{"rendered":"<p>A customer just messaged your business on WhatsApp. Does your system know about it yet? Not in 30 seconds. Not in 5 seconds. Right now &#8211; the moment it happened. If the answer is no, this guide is for you. And even if you think the answer is yes, keep reading because most teams don&#8217;t [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":694,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[],"class_list":["post-679","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-whatsapp"],"_links":{"self":[{"href":"https:\/\/messagebot.in\/blog\/wp-json\/wp\/v2\/posts\/679","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/messagebot.in\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/messagebot.in\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/messagebot.in\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/messagebot.in\/blog\/wp-json\/wp\/v2\/comments?post=679"}],"version-history":[{"count":17,"href":"https:\/\/messagebot.in\/blog\/wp-json\/wp\/v2\/posts\/679\/revisions"}],"predecessor-version":[{"id":700,"href":"https:\/\/messagebot.in\/blog\/wp-json\/wp\/v2\/posts\/679\/revisions\/700"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/messagebot.in\/blog\/wp-json\/wp\/v2\/media\/694"}],"wp:attachment":[{"href":"https:\/\/messagebot.in\/blog\/wp-json\/wp\/v2\/media?parent=679"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/messagebot.in\/blog\/wp-json\/wp\/v2\/categories?post=679"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/messagebot.in\/blog\/wp-json\/wp\/v2\/tags?post=679"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}