{"id":670,"date":"2026-10-29T13:12:29","date_gmt":"2026-10-29T13:12:29","guid":{"rendered":"https:\/\/vectalk.com.br\/blog\/whatsapp-api-webhooks\/"},"modified":"2026-10-29T13:12:33","modified_gmt":"2026-10-29T13:12:33","slug":"whatsapp-api-webhooks","status":"publish","type":"post","link":"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-api-webhooks\/","title":{"rendered":"WhatsApp API webhooks: a practical guide with examples"},"content":{"rendered":"\n<p><em>Updated on October 29, 2026<\/em><\/p>\n\n\n\n<p>A WhatsApp API webhook is an HTTPS URL on your system that receives a <code>POST<\/code> for every event on your account: incoming messages, status changes (<code>sent<\/code>, <code>delivered<\/code>, <code>read<\/code>, <code>failed<\/code>) and account alerts. A well-built endpoint checks where the request came from, returns 200 quickly, puts the event on a queue, processes it in the background and drops duplicates by ID. When you integrate directly with Meta there is also a <code>GET<\/code> verification step; with Vectalk, you register the URL through the API and receive the same payload Meta sends.<\/p>\n\n\n\n<p>This guide covers the event format, the differences between integrating directly and through Vectalk, and a complete server in Node.js and in Python.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How the event flow works<\/h2>\n\n\n\n<p>With a direct integration, Meta calls the endpoint configured in your app. With Vectalk, Meta calls Vectalk, which forwards the event to every active URL registered for that instance:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-text\">Direct:   Meta  \u2500\u2500POST\u2500\u2500\u25b6  your endpoint\nVectalk:  Meta  \u2500\u2500POST\u2500\u2500\u25b6  Vectalk  \u2500\u2500POST\u2500\u2500\u25b6  your URLs (one or several)<\/code><\/pre>\n\n\n\n<p>The body is the same in both cases. What changes is how the URL is registered and how the request is authenticated.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Aspect<\/th><th>Direct Cloud API<\/th><th>Vectalk<\/th><\/tr><\/thead><tbody><tr><td>URL setup<\/td><td>App Dashboard &gt; WhatsApp &gt; Configuration<\/td><td><code>POST \/api\/webhooks<\/code> with <code>instance_id<\/code>, <code>url<\/code> and <code>name<\/code><\/td><\/tr><tr><td>Endpoint verification<\/td><td><code>GET<\/code> with <code>hub.mode<\/code>, <code>hub.verify_token<\/code> and <code>hub.challenge<\/code><\/td><td>Not described in the Vectalk docs<\/td><\/tr><tr><td>Signature<\/td><td><code>X-Hub-Signature-256<\/code> header (HMAC-SHA256 with the app secret)<\/td><td>Not described in the Vectalk docs<\/td><\/tr><tr><td>URLs per account<\/td><td>One endpoint per app (with overrides per messaging account or phone number)<\/td><td>Several URLs per instance, all receiving the same event<\/td><\/tr><tr><td>Response time<\/td><td>Meta retries if it does not get a 200<\/td><td>Return 200 in under 20 seconds; otherwise Vectalk resends<\/td><\/tr><tr><td>Retries<\/td><td>Decreasing frequency for up to 7 days<\/td><td>Resending documented, no time frame given<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>Sources: <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/webhooks\/overview\" target=\"_blank\" rel=\"noreferrer noopener\">WhatsApp webhooks overview<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/webhooks\/create-webhook-endpoint\" target=\"_blank\" rel=\"noreferrer noopener\">Create a webhook endpoint<\/a> and <a href=\"https:\/\/docs.vectalk.com.br\/concepts\/webhooks\">webhooks in the Vectalk docs<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Registering the webhook with Vectalk<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">curl -X POST https:\/\/apis.vectalk.com.br\/api\/webhooks \\\n  -H \"Authorization: Bearer YOUR_TOKEN\" \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\n    \"instance_id\": \"YOUR_INSTANCE_ID\",\n    \"url\": \"https:\/\/myapp.com\/whatsapp\/events\",\n    \"name\": \"Production\"\n  }'<\/code><\/pre>\n\n\n\n<p>The response includes the webhook <code>id<\/code> and <code>active: true<\/code>. The update endpoint takes that <code>id<\/code> and lets you change the URL, the name or turn the webhook off (<code>active<\/code>); see <a href=\"https:\/\/docs.vectalk.com.br\/api-reference\/webhooks\/register\">Register webhook<\/a> and <a href=\"https:\/\/docs.vectalk.com.br\/api-reference\/webhooks\/update\">Update webhook<\/a>. The URL must be HTTPS.<\/p>\n\n\n\n<p>Having several URLs per instance helps in two cases: keeping production and staging apart, and sending the same event to two systems (your CRM and your data warehouse, for example) without building your own fan-out.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Endpoint verification (direct integration)<\/h2>\n\n\n\n<p>When you set the webhook in Meta&#8217;s App Dashboard (under <strong>WhatsApp &gt; Configuration<\/strong>, or <strong>Use cases &gt; Customize &gt; Configuration<\/strong> for apps created with the WhatsApp use case), Meta sends a <code>GET<\/code> to your URL with three parameters:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-text\">GET https:\/\/myapp.com\/whatsapp\/events?hub.mode=subscribe&amp;hub.challenge=1158201444&amp;hub.verify_token=my-secret-token<\/code><\/pre>\n\n\n\n<p>Your endpoint must check that <code>hub.verify_token<\/code> matches the value you entered in the dashboard and return <code>hub.challenge<\/code> in the body with status 200. Any other response fails verification. Meta also requires a valid TLS certificate on the endpoint; self-signed certificates are not supported.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The payloads you will receive<\/h2>\n\n\n\n<p>Every event arrives with <code>object: \"whatsapp_business_account\"<\/code> and an <code>entry<\/code> list. Each <code>entry<\/code> has a <code>changes<\/code> list, and each <code>change<\/code> has a <code>field<\/code> and a <code>value<\/code>. For messages and statuses, <code>field<\/code> is <code>messages<\/code>. Other subscribable fields include <code>account_alerts<\/code>, <code>account_update<\/code>, <code>message_template_status_update<\/code>, <code>phone_number_quality_update<\/code> and <code>smb_message_echoes<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Incoming message<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-json\">{\n  \"object\": \"whatsapp_business_account\",\n  \"entry\": [{\n    \"id\": \"WABA_ID\",\n    \"changes\": [{\n      \"field\": \"messages\",\n      \"value\": {\n        \"messaging_product\": \"whatsapp\",\n        \"metadata\": {\n          \"display_phone_number\": \"558430000000\",\n          \"phone_number_id\": \"PHONE_NUMBER_ID\"\n        },\n        \"contacts\": [{\n          \"profile\": { \"name\": \"Jo\u00e3o Silva\" },\n          \"wa_id\": \"5584988888888\",\n          \"user_id\": \"BR.13491208655302741918\"\n        }],\n        \"messages\": [{\n          \"from\": \"5584988888888\",\n          \"from_user_id\": \"BR.13491208655302741918\",\n          \"id\": \"wamid.HBgLNTUx...\",\n          \"timestamp\": \"1760000000\",\n          \"type\": \"text\",\n          \"text\": { \"body\": \"Hi, I'd like more information\" }\n        }]\n      }\n    }]\n  }]\n}<\/code><\/pre>\n\n\n\n<p>Since April 2026, Meta also sends the <strong>BSUID<\/strong> (business-scoped user ID) in <code>contacts[].user_id<\/code> and <code>messages[].from_user_id<\/code>. The phone number is no longer guaranteed: if the customer has turned on a WhatsApp username and none of Meta&#8217;s conditions apply (a message or call exchanged with that business number in the last 30 days, or the customer being in your contact book), <code>from<\/code> and <code>wa_id<\/code> are omitted. Don&#8217;t use the phone number as your only contact key; the details are in <a href=\"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-usernames-bsuid\/\">WhatsApp usernames and BSUIDs<\/a>.<\/p>\n\n\n\n<p>The <code>type<\/code> field tells you where the content is. The most common ones:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th><code>type<\/code><\/th><th>Where to read<\/th><\/tr><\/thead><tbody><tr><td><code>text<\/code><\/td><td><code>text.body<\/code><\/td><\/tr><tr><td><code>image<\/code>, <code>video<\/code>, <code>audio<\/code>, <code>document<\/code>, <code>sticker<\/code><\/td><td><code>&lt;type&gt;.id<\/code> (download through the media endpoint), <code>caption<\/code>, <code>filename<\/code><\/td><\/tr><tr><td><code>interactive<\/code><\/td><td><code>interactive.button_reply<\/code> or <code>interactive.list_reply<\/code><\/td><\/tr><tr><td><code>location<\/code><\/td><td><code>location.latitude<\/code>, <code>location.longitude<\/code><\/td><\/tr><tr><td><code>reaction<\/code><\/td><td><code>reaction.message_id<\/code>, <code>reaction.emoji<\/code><\/td><\/tr><tr><td><code>order<\/code><\/td><td><code>order.catalog_id<\/code>, <code>order.product_items[]<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Status of a sent message<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-json\">{\n  \"object\": \"whatsapp_business_account\",\n  \"entry\": [{\n    \"id\": \"WABA_ID\",\n    \"changes\": [{\n      \"field\": \"messages\",\n      \"value\": {\n        \"messaging_product\": \"whatsapp\",\n        \"metadata\": { \"display_phone_number\": \"558430000000\", \"phone_number_id\": \"PHONE_NUMBER_ID\" },\n        \"statuses\": [{\n          \"id\": \"wamid.HBgLNTUx...\",\n          \"status\": \"delivered\",\n          \"timestamp\": \"1760000010\",\n          \"recipient_id\": \"5584988888888\",\n          \"recipient_user_id\": \"BR.13491208655302741918\",\n          \"pricing\": { \"billable\": true, \"pricing_model\": \"PMP\", \"type\": \"regular\", \"category\": \"utility\" }\n        }]\n      }\n    }]\n  }]\n}<\/code><\/pre>\n\n\n\n<p>The status <code>id<\/code> is the <code>wamid<\/code> you got back when you sent the message. The <code>recipient_user_id<\/code> field carries the customer&#8217;s BSUID whether you sent the message to the phone number or to the BSUID; <code>recipient_id<\/code> (the phone number) is omitted when you sent to the BSUID and Meta cannot include the number. Besides <code>sent<\/code>, <code>delivered<\/code>, <code>read<\/code> and <code>failed<\/code>, there is <code>played<\/code> for voice messages. When <code>status<\/code> is <code>failed<\/code>, the object includes <code>errors[]<\/code> with <code>code<\/code>, <code>title<\/code>, <code>message<\/code> and <code>error_data.details<\/code>.<\/p>\n\n\n\n<p>The <code>pricing<\/code> object carries <code>category<\/code> (<code>marketing<\/code>, <code>utility<\/code>, <code>authentication<\/code>, <code>service<\/code> and others) and <code>type<\/code>: <code>regular<\/code> for billable messages, or a free type such as <code>free_customer_service<\/code> or <code>free_entry_point<\/code>. Since October 1, 2026, utility templates sent inside the customer service window are charged, so they arrive as <code>regular<\/code> instead of <code>free_customer_service<\/code>. Meta has announced that the <code>billable<\/code> field will be deprecated in favor of <code>type<\/code> and <code>category<\/code>. <code>pricing<\/code> comes with the <code>sent<\/code> status and with one of <code>delivered<\/code> or <code>read<\/code>, never with <code>failed<\/code>. Full reference: <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/webhooks\/reference\/messages\/status\" target=\"_blank\" rel=\"noreferrer noopener\">status webhook documentation<\/a>. For what each category costs, see <a href=\"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-api-pricing-brazil\/\">WhatsApp API pricing in Brazil<\/a> and, for other markets, <a href=\"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-api-pricing-by-country\/\">WhatsApp API pricing by country<\/a>. Status handling is covered in depth in <a href=\"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-api-message-status-webhook\/\">WhatsApp API message status webhooks<\/a>.<\/p>\n\n\n\n<p>A single <code>POST<\/code> can carry several messages or several statuses: Meta&#8217;s webhooks documentation says notifications are batched with up to 1,000 updates per request, though batching is not guaranteed. Iterate over the lists instead of reading only the first item.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Best practices<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Return 200 fast and process later<\/h3>\n\n\n\n<p>The endpoint should not call your database, your AI or another API before responding. Write the raw body to a queue (Redis, SQS, RabbitMQ, an events table) and return 200. A separate worker does the heavy lifting. Vectalk documents a 20-second limit for the response; stay well below it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Be idempotent<\/h3>\n\n\n\n<p>Meta warns that retries can produce duplicate notifications, and a proxy can resend too. Use the message <code>id<\/code> (or the <code>id<\/code> + <code>status<\/code> pair, for statuses) as a unique key. If the key has already been processed, drop the event.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Validate the signature<\/h3>\n\n\n\n<p>With a direct integration, Meta signs every <code>POST<\/code> with an HMAC-SHA256 of the body, keyed with your app secret, and sends the result in the <code>X-Hub-Signature-256<\/code> header as <code>sha256=&lt;hash&gt;<\/code>. Compute the hash over the <strong>raw body<\/strong>, before any JSON parsing, and compare in constant time; the step-by-step guide is in <a href=\"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-webhook-signature-validation\/\">how to validate the WhatsApp webhook signature<\/a>. Webhooks also support mutual TLS (mTLS). Avoid IP allowlists as your only defense: Meta changes its IP addresses periodically and recommends mTLS instead.<\/p>\n\n\n\n<p>With Vectalk, use the authentication mechanism Vectalk specifies for forwarded requests (check the docs or the <a href=\"https:\/\/vectalk.com.br\/contato.html\">contact page<\/a>). Until that is set up in your environment, use a hard-to-guess URL path, keep the endpoint HTTPS-only, and do not trust the content for sensitive actions without checking the expected <code>phone_number_id<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Don&#8217;t depend on order<\/h3>\n\n\n\n<p>Retries mean an older status can arrive after a newer one, and when a message is read right away Meta sends <code>read<\/code> without a <code>delivered<\/code> webhook. Store the most advanced status and the <code>timestamp<\/code> of each step, without ever moving backward.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Plan for large payloads<\/h3>\n\n\n\n<p>Meta says webhook payloads can be up to 3 MB. Raise your server&#8217;s body size limit accordingly (Express defaults to 100 kb).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Monitor<\/h3>\n\n\n\n<p>Track how many events arrive per minute and how many fail in the worker. A sudden drop in message events is often the first sign of a disabled webhook, an expired certificate or a disconnected number.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Complete example in Node.js (Express)<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-js\">import express from 'express'\nimport crypto from 'node:crypto'\nimport { createClient } from 'redis'\n\nconst app = express()\nconst redis = createClient({ url: process.env.REDIS_URL })\nawait redis.connect()\n\nconst VERIFY_TOKEN = process.env.WHATSAPP_VERIFY_TOKEN \/\/ direct integration\nconst APP_SECRET = process.env.META_APP_SECRET        \/\/ direct integration\n\n\/\/ Keep the raw body so the signature can be validated\napp.use('\/whatsapp\/events', express.raw({ type: 'application\/json', limit: '3mb' }))\n\n\/\/ Endpoint verification (direct integration with Meta only)\napp.get('\/whatsapp\/events', (req, res) =&gt; {\n  const { 'hub.mode': mode, 'hub.verify_token': token, 'hub.challenge': challenge } = req.query\n  if (mode === 'subscribe' &amp;&amp; token === VERIFY_TOKEN) return res.status(200).send(challenge)\n  return res.sendStatus(403)\n})\n\nfunction validSignature(req) {\n  const header = req.get('X-Hub-Signature-256') || ''\n  const expected = 'sha256=' + crypto.createHmac('sha256', APP_SECRET).update(req.body).digest('hex')\n  const a = Buffer.from(header)\n  const b = Buffer.from(expected)\n  return a.length === b.length &amp;&amp; crypto.timingSafeEqual(a, b)\n}\n\napp.post('\/whatsapp\/events', async (req, res) =&gt; {\n  if (APP_SECRET &amp;&amp; !validSignature(req)) return res.sendStatus(401)\n  await redis.lPush('whatsapp:events', req.body.toString('utf8'))\n  res.sendStatus(200)\n})\n\napp.listen(3000)<\/code><\/pre>\n\n\n\n<p>And the worker, which consumes the queue and drops duplicates:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-js\">\/\/ uses a Redis client connected the same way as above\nasync function processEvent(event) {\n  for (const entry of event.entry ?? []) {\n    for (const change of entry.changes ?? []) {\n      const v = change.value ?? {}\n      for (const msg of v.messages ?? []) {\n        const isNew = await redis.set(`wa:msg:${msg.id}`, '1', { NX: true, EX: 60 * 60 * 24 * 8 })\n        if (!isNew) continue \/\/ duplicate\n        await handleMessage(v.metadata.phone_number_id, msg, v.contacts) \/\/ your business logic\n      }\n      for (const st of v.statuses ?? []) {\n        const isNew = await redis.set(`wa:st:${st.id}:${st.status}`, '1', { NX: true, EX: 60 * 60 * 24 * 8 })\n        if (!isNew) continue\n        await updateStatus(st) \/\/ store status, errors and pricing\n      }\n    }\n  }\n}\n\nwhile (true) {\n  const item = await redis.brPop('whatsapp:events', 0)\n  try {\n    await processEvent(JSON.parse(item.element))\n  } catch (err) {\n    console.error('failed to process event', err)\n    await redis.lPush('whatsapp:events:error', item.element)\n  }\n}<\/code><\/pre>\n\n\n\n<p>The eight-day expiry on the keys covers Meta&#8217;s retry window, which lasts up to seven days.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Complete example in Python (FastAPI)<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-python\">import hashlib, hmac, json, os\nimport redis.asyncio as redis\nfrom fastapi import FastAPI, Request, Response\n\napp = FastAPI()\nr = redis.from_url(os.environ[\"REDIS_URL\"])\nVERIFY_TOKEN = os.environ.get(\"WHATSAPP_VERIFY_TOKEN\")  # direct integration\nAPP_SECRET = os.environ.get(\"META_APP_SECRET\", \"\")      # direct integration\n\n\n@app.get(\"\/whatsapp\/events\")\nasync def verify(request: Request):\n    q = request.query_params\n    if q.get(\"hub.mode\") == \"subscribe\" and q.get(\"hub.verify_token\") == VERIFY_TOKEN:\n        return Response(content=q.get(\"hub.challenge\", \"\"), media_type=\"text\/plain\")\n    return Response(status_code=403)\n\n\ndef valid_signature(body: bytes, header: str) -&gt; bool:\n    expected = \"sha256=\" + hmac.new(APP_SECRET.encode(), body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, header or \"\")\n\n\n@app.post(\"\/whatsapp\/events\")\nasync def receive(request: Request):\n    body = await request.body()\n    if APP_SECRET and not valid_signature(body, request.headers.get(\"X-Hub-Signature-256\")):\n        return Response(status_code=401)\n    await r.lpush(\"whatsapp:events\", body)\n    return Response(status_code=200)<\/code><\/pre>\n\n\n\n<p>Python worker:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-python\">import asyncio, json, os\nimport redis.asyncio as redis\n\nr = redis.from_url(os.environ[\"REDIS_URL\"])\nEIGHT_DAYS = 60 * 60 * 24 * 8\n\n\nasync def process_event(event: dict):\n    for entry in event.get(\"entry\", []):\n        for change in entry.get(\"changes\", []):\n            v = change.get(\"value\", {})\n            for msg in v.get(\"messages\", []):\n                if not await r.set(f\"wa:msg:{msg['id']}\", 1, nx=True, ex=EIGHT_DAYS):\n                    continue\n                await handle_message(v[\"metadata\"][\"phone_number_id\"], msg, v.get(\"contacts\", []))\n            for st in v.get(\"statuses\", []):\n                if not await r.set(f\"wa:st:{st['id']}:{st['status']}\", 1, nx=True, ex=EIGHT_DAYS):\n                    continue\n                await update_status(st)\n\n\nasync def main():\n    while True:\n        _, item = await r.brpop(\"whatsapp:events\")\n        try:\n            await process_event(json.loads(item))\n        except Exception as exc:\n            print(\"failed to process event\", exc)\n            await r.lpush(\"whatsapp:events:error\", item)\n\nasyncio.run(main())<\/code><\/pre>\n\n\n\n<p>The <code>handle_message<\/code> and <code>update_status<\/code> functions (and their Node equivalents) are your business logic: reply, write to the CRM, trigger an agent. Inside them, identify the contact by <code>msg.from_user_id<\/code> (or <code>contacts[].user_id<\/code>) and use <code>msg.from<\/code> only when it is present.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What to do with each event type in the worker<\/h2>\n\n\n\n<p>Splitting handling by event type makes the worker easier to maintain. A split that works well:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Incoming messages<\/strong>: store the message, update the contact&#8217;s last-message time (that is what tells you whether the 24-hour window is open) and only then trigger the chatbot, the agent or the human support queue.<\/li>\n\n\n<li><strong><code>sent<\/code>, <code>delivered<\/code> and <code>read<\/code> statuses<\/strong>: update the record of the sent message by <code>wamid<\/code>, storing the time of each step. This data feeds delivery and read reports.<\/li>\n\n\n<li><strong><code>failed<\/code> status<\/strong>: store the error code and decide what to do. Some errors call for a retry later; others mean the send will not work without a change, such as a template sent outside the window or an invalid number. Don&#8217;t resend in a loop.<\/li>\n\n\n<li><strong>Other fields<\/strong> (<code>account_update<\/code>, <code>account_alerts<\/code>, template status changes): route them to a technical alert channel, because they usually need human action.<\/li>\n\n<\/ul>\n\n\n\n<p>Also keep an error queue, as in the examples above. An event that failed processing should not disappear: it needs to be available for reprocessing once the bug is fixed.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Testing locally<\/h2>\n\n\n\n<p>To receive real events on your machine, expose the local port through an HTTPS tunnel (ngrok, Cloudflare Tunnel or similar) and register the tunnel URL as an additional webhook on the instance, with a name like &#8220;dev&#8221;. Since Vectalk delivers each event to every active URL, your development environment gets the same traffic as production without interfering with it. Turn the dev webhook off when you are done. The full walkthrough is in <a href=\"https:\/\/vectalk.com.br\/blog\/en\/test-whatsapp-webhook-locally\/\">how to test the WhatsApp webhook locally with ngrok<\/a>.<\/p>\n\n\n\n<p>You can also test the parser without real traffic by saving sample payloads and sending them with <code>curl<\/code> (leave <code>META_APP_SECRET<\/code> unset in this test, or sign the body as shown in the signature guide, or the server will answer 401):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">curl -X POST http:\/\/localhost:3000\/whatsapp\/events \\\n  -H \"Content-Type: application\/json\" \\\n  --data @samples\/text-message.json<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Common errors<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Symptom<\/th><th>Likely cause<\/th><\/tr><\/thead><tbody><tr><td>Messages processed twice<\/td><td>No deduplication by <code>id<\/code><\/td><\/tr><tr><td>Events arriving late in bursts<\/td><td>Slow endpoint; the sender piled up retries<\/td><\/tr><tr><td>Signature never matches<\/td><td>Hash computed over parsed JSON instead of the raw body<\/td><\/tr><tr><td><code>413 Payload Too Large<\/code><\/td><td>Server body limit below 3 MB<\/td><\/tr><tr><td>Only the first message in a batch is handled<\/td><td>Code reads <code>messages[0]<\/code> instead of iterating<\/td><\/tr><tr><td>Status going back from <code>read<\/code> to <code>delivered<\/code><\/td><td>Out-of-order events overwriting state<\/td><\/tr><tr><td>Contacts without a phone number in the CRM<\/td><td>Code keyed only on <code>from<\/code>\/<code>wa_id<\/code>, ignoring the BSUID<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Frequently asked questions<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Do I need to return anything in the webhook response body?<\/h3>\n\n\n\n<p>No. HTTP status 200 is enough. The response body is ignored, except in the direct integration&#8217;s <code>GET<\/code> verification, where the body must be <code>hub.challenge<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can I use the same URL for several numbers?<\/h3>\n\n\n\n<p>Yes. Use <code>metadata.phone_number_id<\/code> to tell which number an event belongs to. With Vectalk, each webhook is registered per instance, and you can point several instances to the same URL.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How long does Meta keep retrying an event?<\/h3>\n\n\n\n<p>Up to seven days, with decreasing frequency, according to Meta&#8217;s webhooks documentation. With Vectalk, the docs state that the event is resent when the endpoint does not return 200 within 20 seconds.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How do I receive the messages the owner sends from the app, under coexistence?<\/h3>\n\n\n\n<p>Through the <code>smb_message_echoes<\/code> webhook field. The <a href=\"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-api-coexistence\/\">WhatsApp API coexistence<\/a> guide explains the extra fields.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">I&#8217;m migrating from Baileys. Is the format very different?<\/h3>\n\n\n\n<p>Yes. Events follow Meta&#8217;s <code>entry[].changes[].value<\/code> structure, contacts are identified by <code>wa_id<\/code> and, since April 2026, also by the BSUID (<code>user_id<\/code>), and media arrives as an <code>id<\/code>. The guide <a href=\"https:\/\/vectalk.com.br\/blog\/en\/migrate-to-official-whatsapp-api\/\">how to migrate to the official WhatsApp API<\/a> covers that transition. To get started with Vectalk, create your account at <a href=\"https:\/\/apps.vectalk.com.br\/register\">apps.vectalk.com.br\/register<\/a>; account questions go to the <a href=\"https:\/\/vectalk.com.br\/ajuda.html\">Help Center<\/a> (in Portuguese).<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p><strong>Sources checked on October 29, 2026:<\/strong> <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/webhooks\/overview\" target=\"_blank\" rel=\"noreferrer noopener\">WhatsApp webhooks overview<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/webhooks\/create-webhook-endpoint\" target=\"_blank\" rel=\"noreferrer noopener\">Create a webhook endpoint<\/a>, <a href=\"https:\/\/developers.facebook.com\/docs\/graph-api\/webhooks\/getting-started\" target=\"_blank\" rel=\"noreferrer noopener\">Graph API webhooks: getting started<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/webhooks\/reference\/messages\/status\" target=\"_blank\" rel=\"noreferrer noopener\">Status webhook reference<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/webhooks\/reference\/messages\/text\" target=\"_blank\" rel=\"noreferrer noopener\">Text message webhook reference<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/business-scoped-user-ids\" target=\"_blank\" rel=\"noreferrer noopener\">Business-scoped user IDs<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/pricing\" target=\"_blank\" rel=\"noreferrer noopener\">Pricing<\/a>, <a href=\"https:\/\/docs.vectalk.com.br\/concepts\/webhooks\">Webhooks in the Vectalk docs<\/a>, <a href=\"https:\/\/docs.vectalk.com.br\/api-reference\/webhooks\/register\">Register webhook (Vectalk)<\/a>, <a href=\"https:\/\/docs.vectalk.com.br\/api-reference\/webhooks\/update\">Update webhook (Vectalk)<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>How WhatsApp Cloud API webhooks work: verification, message and status payloads, BSUID fields, signature checks, queues, retries, and full Node.js and Python servers.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[1],"tags":[],"class_list":["post-670","post","type-post","status-publish","format-standard","hentry","category-blog"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.7 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>WhatsApp API Webhooks: Node.js and Python Guide | Vectalk<\/title>\n<meta name=\"description\" content=\"WhatsApp API webhooks in practice: endpoint verification, message and status payloads, signature, queueing, and Node.js and Python examples.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-api-webhooks\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"WhatsApp API Webhooks: Node.js and Python Guide | Vectalk\" \/>\n<meta property=\"og:description\" content=\"WhatsApp API webhooks in practice: endpoint verification, message and status payloads, signature, queueing, and Node.js and Python examples.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-api-webhooks\/\" \/>\n<meta property=\"og:site_name\" content=\"Blog Vectalk\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-29T13:12:29+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-10-29T13:12:33+00:00\" \/>\n<meta name=\"author\" content=\"wanderleyfcunha\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"wanderleyfcunha\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"14 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/en\\\/whatsapp-api-webhooks\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/en\\\/whatsapp-api-webhooks\\\/\"},\"author\":{\"name\":\"wanderleyfcunha\",\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/#\\\/schema\\\/person\\\/ff36ecd9d8257856f227f8e5d639db2b\"},\"headline\":\"WhatsApp API webhooks: a practical guide with examples\",\"datePublished\":\"2026-10-29T13:12:29+00:00\",\"dateModified\":\"2026-10-29T13:12:33+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/en\\\/whatsapp-api-webhooks\\\/\"},\"wordCount\":1792,\"articleSection\":[\"Blog\"],\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/en\\\/whatsapp-api-webhooks\\\/\",\"url\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/en\\\/whatsapp-api-webhooks\\\/\",\"name\":\"WhatsApp API Webhooks: Node.js and Python Guide | Vectalk\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/#website\"},\"datePublished\":\"2026-10-29T13:12:29+00:00\",\"dateModified\":\"2026-10-29T13:12:33+00:00\",\"author\":{\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/#\\\/schema\\\/person\\\/ff36ecd9d8257856f227f8e5d639db2b\"},\"description\":\"WhatsApp API webhooks in practice: endpoint verification, message and status payloads, signature, queueing, and Node.js and Python examples.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/en\\\/whatsapp-api-webhooks\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/en\\\/whatsapp-api-webhooks\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/en\\\/whatsapp-api-webhooks\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"WhatsApp API webhooks: a practical guide with examples\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/\",\"name\":\"Blog Vectalk\",\"description\":\"\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/#\\\/schema\\\/person\\\/ff36ecd9d8257856f227f8e5d639db2b\",\"name\":\"wanderleyfcunha\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/21c37077f1731eb558973f27a7b647d55f72fde5200a843ac5f5345b247ea725?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/21c37077f1731eb558973f27a7b647d55f72fde5200a843ac5f5345b247ea725?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/21c37077f1731eb558973f27a7b647d55f72fde5200a843ac5f5345b247ea725?s=96&d=mm&r=g\",\"caption\":\"wanderleyfcunha\"},\"sameAs\":[\"https:\\\/\\\/vectalk.com.br\\\/blog\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"WhatsApp API Webhooks: Node.js and Python Guide | Vectalk","description":"WhatsApp API webhooks in practice: endpoint verification, message and status payloads, signature, queueing, and Node.js and Python examples.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-api-webhooks\/","og_locale":"en_US","og_type":"article","og_title":"WhatsApp API Webhooks: Node.js and Python Guide | Vectalk","og_description":"WhatsApp API webhooks in practice: endpoint verification, message and status payloads, signature, queueing, and Node.js and Python examples.","og_url":"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-api-webhooks\/","og_site_name":"Blog Vectalk","article_published_time":"2026-10-29T13:12:29+00:00","article_modified_time":"2026-10-29T13:12:33+00:00","author":"wanderleyfcunha","twitter_card":"summary_large_image","twitter_misc":{"Written by":"wanderleyfcunha","Est. reading time":"14 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-api-webhooks\/#article","isPartOf":{"@id":"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-api-webhooks\/"},"author":{"name":"wanderleyfcunha","@id":"https:\/\/vectalk.com.br\/blog\/#\/schema\/person\/ff36ecd9d8257856f227f8e5d639db2b"},"headline":"WhatsApp API webhooks: a practical guide with examples","datePublished":"2026-10-29T13:12:29+00:00","dateModified":"2026-10-29T13:12:33+00:00","mainEntityOfPage":{"@id":"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-api-webhooks\/"},"wordCount":1792,"articleSection":["Blog"],"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-api-webhooks\/","url":"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-api-webhooks\/","name":"WhatsApp API Webhooks: Node.js and Python Guide | Vectalk","isPartOf":{"@id":"https:\/\/vectalk.com.br\/blog\/#website"},"datePublished":"2026-10-29T13:12:29+00:00","dateModified":"2026-10-29T13:12:33+00:00","author":{"@id":"https:\/\/vectalk.com.br\/blog\/#\/schema\/person\/ff36ecd9d8257856f227f8e5d639db2b"},"description":"WhatsApp API webhooks in practice: endpoint verification, message and status payloads, signature, queueing, and Node.js and Python examples.","breadcrumb":{"@id":"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-api-webhooks\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/vectalk.com.br\/blog\/en\/whatsapp-api-webhooks\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-api-webhooks\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/vectalk.com.br\/blog\/"},{"@type":"ListItem","position":2,"name":"WhatsApp API webhooks: a practical guide with examples"}]},{"@type":"WebSite","@id":"https:\/\/vectalk.com.br\/blog\/#website","url":"https:\/\/vectalk.com.br\/blog\/","name":"Blog Vectalk","description":"","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/vectalk.com.br\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/vectalk.com.br\/blog\/#\/schema\/person\/ff36ecd9d8257856f227f8e5d639db2b","name":"wanderleyfcunha","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/21c37077f1731eb558973f27a7b647d55f72fde5200a843ac5f5345b247ea725?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/21c37077f1731eb558973f27a7b647d55f72fde5200a843ac5f5345b247ea725?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/21c37077f1731eb558973f27a7b647d55f72fde5200a843ac5f5345b247ea725?s=96&d=mm&r=g","caption":"wanderleyfcunha"},"sameAs":["https:\/\/vectalk.com.br\/blog"]}]}},"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/posts\/670","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/comments?post=670"}],"version-history":[{"count":1,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/posts\/670\/revisions"}],"predecessor-version":[{"id":672,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/posts\/670\/revisions\/672"}],"wp:attachment":[{"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/media?parent=670"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/categories?post=670"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/tags?post=670"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}