{"id":535,"date":"2026-10-28T16:06:51","date_gmt":"2026-10-28T16:06:51","guid":{"rendered":"https:\/\/vectalk.com.br\/blog\/whatsapp-bot-python-fastapi\/"},"modified":"2026-10-28T16:06:55","modified_gmt":"2026-10-28T16:06:55","slug":"whatsapp-bot-python-fastapi","status":"publish","type":"post","link":"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-bot-python-fastapi\/","title":{"rendered":"How to Build a WhatsApp Bot in Python with FastAPI (Official Cloud API, from Scratch)"},"content":{"rendered":"\n<p><em>Updated on October 28, 2026<\/em><\/p>\n\n\n\n<p>To build a WhatsApp bot in Python on the official API, you need an HTTPS server with two routes: a <code>GET<\/code> that answers Meta&#8217;s verification challenge and a <code>POST<\/code> that receives messages, validates the <code>X-Hub-Signature-256<\/code> signature and replies by calling the Cloud API <code>\/messages<\/code> endpoint. With FastAPI and httpx, that fits in a single file of about 150 lines, and this tutorial builds it from scratch.<\/p>\n\n\n\n<p>The finished bot answers &#8220;hi&#8221; or &#8220;menu&#8221; with three buttons, handles each button tap, marks messages as read, ignores duplicate events and runs in production with Docker and automatic HTTPS. The code was tested with FastAPI 0.128 and works both directly with Meta and through Vectalk by changing two environment variables.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How the bot works<\/h2>\n\n\n\n<p>The flow has four steps:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>A customer sends a message to your WhatsApp Business number.<\/li>\n\n\n<li>Meta sends a <code>POST<\/code> with the event to your webhook URL.<\/li>\n\n\n<li>Your server validates the signature, returns <code>200<\/code> right away and processes the event in the background.<\/li>\n\n\n<li>The bot calls <code>POST \/&lt;PHONE_NUMBER_ID&gt;\/messages<\/code> to reply.<\/li>\n\n<\/ol>\n\n\n\n<p>Because the reply goes out right after the customer&#8217;s message, it falls inside the 24-hour customer service window, so it can be free-form text or an interactive message with no template. If you are still deciding how to access the API, the <a href=\"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-cloud-api-en\/\">WhatsApp Cloud API guide<\/a> covers the options.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What you need first<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Variable<\/th><th>What it is<\/th><th>Where to find it<\/th><\/tr><\/thead><tbody><tr><td><code>PHONE_NUMBER_ID<\/code><\/td><td>The number&#8217;s ID on the Cloud API<\/td><td>App Dashboard, WhatsApp section<\/td><\/tr><tr><td><code>ACCESS_TOKEN<\/code><\/td><td>Token with the <code>whatsapp_business_messaging<\/code> permission<\/td><td>System user token<\/td><\/tr><tr><td><code>APP_SECRET<\/code><\/td><td>App secret, used for the signature<\/td><td>App settings &gt; Basic<\/td><\/tr><tr><td><code>VERIFY_TOKEN<\/code><\/td><td>Any string you choose<\/td><td>You set it and repeat it in the dashboard<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>Use a <a href=\"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-api-permanent-access-token\/\">permanent system user access token<\/a>: the temporary token from the dashboard expires and takes the bot down with it. The number must also be registered on the Cloud API; if it isn&#8217;t yet, see <a href=\"https:\/\/vectalk.com.br\/blog\/en\/register-phone-number-whatsapp-cloud-api\/\">how to register the number<\/a>.<\/p>\n\n\n\n<p>Locally, you need Python 3.10 or newer.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1. Set up the project<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">mkdir whatsapp-bot &amp;&amp; cd whatsapp-bot\npython -m venv .venv\nsource .venv\/bin\/activate\npip install \"fastapi[standard]\" httpx<\/code><\/pre>\n\n\n\n<p><code>fastapi[standard]<\/code> installs Uvicorn and the <code>fastapi<\/code> command. httpx makes the async calls to the API.<\/p>\n\n\n\n<p>Create <code>main.py<\/code>. The code blocks in sections 1 to 5 make up the complete file: paste them in order.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-python\">import hashlib\nimport hmac\nimport json\nimport logging\nimport os\n\nimport httpx\nfrom fastapi import BackgroundTasks, FastAPI, HTTPException, Request\nfrom fastapi.responses import PlainTextResponse\n\nVERIFY_TOKEN = os.environ[\"VERIFY_TOKEN\"]\nACCESS_TOKEN = os.environ[\"ACCESS_TOKEN\"]\nAPP_SECRET = os.environ.get(\"APP_SECRET\", \"\")\nWEBHOOK_KEY = os.environ.get(\"WEBHOOK_KEY\", \"\")\nPHONE_NUMBER_ID = os.environ.get(\"PHONE_NUMBER_ID\", \"\")\nAPI_URL = os.environ.get(\n    \"API_URL\", f\"https:\/\/graph.facebook.com\/v26.0\/{PHONE_NUMBER_ID}\/messages\"\n)\n\nlog = logging.getLogger(\"bot\")\napp = FastAPI()\nclient = httpx.AsyncClient(timeout=10)\nprocessed = set()  # in production, use Redis or your database<\/code><\/pre>\n\n\n\n<p><code>API_URL<\/code> defaults to Meta&#8217;s Graph API. To use Vectalk, you only change this variable and the token, as shown in the Vectalk section below.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. Webhook verification (GET)<\/h2>\n\n\n\n<p>When you save the callback URL in the App Dashboard, Meta sends a <code>GET<\/code> with <code>hub.mode=subscribe<\/code>, <code>hub.verify_token<\/code> and <code>hub.challenge<\/code>. If the token matches yours, return <code>200<\/code> with the <code>hub.challenge<\/code> value as plain text. Any other response leaves the endpoint unverified, and Meta won&#8217;t send events.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-python\">@app.get(\"\/webhook\")\ndef 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 PlainTextResponse(q.get(\"hub.challenge\", \"\"))\n    raise HTTPException(status_code=403)<\/code><\/pre>\n\n\n\n<p>The parameter names contain a dot (<code>hub.mode<\/code>), which is why the code reads <code>request.query_params<\/code> instead of declaring function parameters.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3. Validate the X-Hub-Signature-256 header<\/h2>\n\n\n\n<p>Every <code>POST<\/code> from Meta carries the header <code>X-Hub-Signature-256: sha256=&lt;hash&gt;<\/code>. The hash is an HMAC-SHA256 of the request body, keyed with your app secret. Without this check, anyone who finds your URL can forge customer messages.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-python\">def signature_ok(body: bytes, header: str) -&gt; bool:\n    if not APP_SECRET:\n        return True\n    expected = \"sha256=\" + hmac.new(APP_SECRET.encode(), body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, header or \"\")<\/code><\/pre>\n\n\n\n<p>Two details that save hours of debugging:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Hash the original bytes.<\/strong> Compute the HMAC over <code>await request.body()<\/code>, never over re-serialized JSON. A single extra space changes the hash.<\/li>\n\n\n<li><strong>Compare in constant time.<\/strong> <code>hmac.compare_digest<\/code> keeps comparison timing from leaking the hash.<\/li>\n\n<\/ul>\n\n\n\n<p>The article on <a href=\"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-webhook-signature-validation\/\">validating the webhook signature<\/a> goes deeper and lists the usual mistakes.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4. Receive the event and acknowledge fast<\/h2>\n\n\n\n<p>Meta expects a quick <code>200<\/code>. If delivery fails, it retries with decreasing frequency for up to 7 days, so the same event can arrive more than once. That is why the code acknowledges before processing and skips message IDs it has already seen.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-python\">@app.post(\"\/webhook\")\nasync def receive(request: Request, background: BackgroundTasks):\n    if WEBHOOK_KEY and request.query_params.get(\"key\") != WEBHOOK_KEY:\n        raise HTTPException(status_code=403)\n    body = await request.body()\n    if not signature_ok(body, request.headers.get(\"x-hub-signature-256\", \"\")):\n        raise HTTPException(status_code=401, detail=\"invalid signature\")\n    background.add_task(process, json.loads(body))\n    return {\"ok\": True}\n\n\nasync def process(payload: dict) -&gt; None:\n    for entry in payload.get(\"entry\", []):\n        for change in entry.get(\"changes\", []):\n            for msg in change.get(\"value\", {}).get(\"messages\", []):\n                if msg[\"id\"] in processed:\n                    continue  # Meta may redeliver the same event\n                processed.add(msg[\"id\"])\n                try:\n                    await handle(msg)\n                except httpx.HTTPError:\n                    log.exception(\"failed to reply to %s\", msg[\"id\"])<\/code><\/pre>\n\n\n\n<p>FastAPI&#8217;s <code>BackgroundTasks<\/code> runs <code>process<\/code> after the response is sent. Status events (<code>sent<\/code>, <code>delivered<\/code>, <code>read<\/code>, <code>failed<\/code>) arrive on the same webhook under <code>value.statuses<\/code>, and this bot ignores them. To handle them, see <a href=\"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-api-message-status-webhook\/\">WhatsApp API message status webhooks<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">5. Reply with text and interactive buttons<\/h2>\n\n\n\n<p>The bot logic lives in <code>handle<\/code>. &#8220;hi&#8221; or &#8220;menu&#8221; opens the button menu; a button tap arrives as <code>type: \"interactive\"<\/code> with <code>interactive.type: \"button_reply\"<\/code> and the <code>id<\/code> you set when sending.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-python\">ANSWERS = {\n    \"hours\": \"We're open Monday to Friday, 9 a.m. to 6 p.m.\",\n    \"pricing\": \"See the full price list at https:\/\/example.com\/pricing\",\n    \"human\": \"Sure! An agent will pick up the conversation here.\",\n}\n\n\nasync def handle(msg: dict) -&gt; None:\n    dest = recipient_of(msg)\n    await mark_read(msg[\"id\"])\n    kind = msg.get(\"type\")\n    if kind == \"text\":\n        text = msg[\"text\"][\"body\"].strip().lower()\n        if text in {\"hi\", \"hello\", \"menu\"}:\n            await send_menu(dest)\n        else:\n            await send_text(dest, \"Sorry, I didn't get that. Type *menu* to see the options.\")\n    elif kind == \"interactive\" and msg[\"interactive\"][\"type\"] == \"button_reply\":\n        choice = msg[\"interactive\"][\"button_reply\"][\"id\"]\n        await send_text(dest, ANSWERS.get(choice, \"Invalid option.\"))\n    else:\n        await send_text(dest, \"For now I only understand text and buttons.\")\n\n\ndef recipient_of(msg: dict) -&gt; dict:\n    # Users with a username may arrive without \"from\", only with their BSUID\n    if \"from\" in msg:\n        return {\"to\": msg[\"from\"]}\n    return {\"recipient\": msg[\"from_user_id\"]}<\/code><\/pre>\n\n\n\n<p><code>recipient_of<\/code> gets the bot ready for WhatsApp usernames. According to Meta&#8217;s documentation, when a user adopts a username the <code>from<\/code> field may be missing from the webhook, but the business-scoped user ID in <code>from_user_id<\/code> (the BSUID) is always there, and since July 2026 you can send messages to it with the <code>recipient<\/code> field. The article on <a href=\"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-usernames-bsuid\/\">usernames and BSUIDs<\/a> explains the change.<\/p>\n\n\n\n<p>Finally, the functions that call the API:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-python\">async def call_api(payload: dict) -&gt; dict:\n    r = await client.post(\n        API_URL,\n        headers={\"Authorization\": f\"Bearer {ACCESS_TOKEN}\"},\n        json={\"messaging_product\": \"whatsapp\", **payload},\n    )\n    if r.is_error:\n        log.error(\"API %s: %s\", r.status_code, r.text)\n    r.raise_for_status()\n    return r.json()\n\n\nasync def mark_read(message_id: str) -&gt; None:\n    try:\n        await call_api({\n            \"status\": \"read\",\n            \"message_id\": message_id,\n            \"typing_indicator\": {\"type\": \"text\"},\n        })\n    except httpx.HTTPError:\n        pass  # never blocks the reply\n\n\nasync def send_text(dest: dict, text: str) -&gt; dict:\n    return await call_api({\n        \"recipient_type\": \"individual\",\n        **dest,\n        \"type\": \"text\",\n        \"text\": {\"body\": text},\n    })\n\n\nasync def send_menu(dest: dict) -&gt; dict:\n    buttons = [(\"hours\", \"Opening hours\"), (\"pricing\", \"Pricing\"), (\"human\", \"Talk to an agent\")]\n    return await call_api({\n        \"recipient_type\": \"individual\",\n        **dest,\n        \"type\": \"interactive\",\n        \"interactive\": {\n            \"type\": \"button\",\n            \"body\": {\"text\": \"Hi! How can I help?\"},\n            \"action\": {\n                \"buttons\": [\n                    {\"type\": \"reply\", \"reply\": {\"id\": i, \"title\": t}} for i, t in buttons\n                ]\n            },\n        },\n    })<\/code><\/pre>\n\n\n\n<p><code>mark_read<\/code> marks the message as read and shows a typing indicator to the customer. Meta removes the indicator when you reply or after 25 seconds.<\/p>\n\n\n\n<p>Reply button limits, according to Meta&#8217;s documentation:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Field<\/th><th>Limit<\/th><\/tr><\/thead><tbody><tr><td>Buttons per message<\/td><td>up to 3<\/td><\/tr><tr><td>Button <code>title<\/code><\/td><td>20 characters<\/td><\/tr><tr><td>Button <code>id<\/code><\/td><td>256 characters<\/td><\/tr><tr><td><code>body.text<\/code><\/td><td>1,024 characters<\/td><\/tr><tr><td><code>footer.text<\/code><\/td><td>60 characters<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">6. Test locally<\/h2>\n\n\n\n<p>Export the variables and start the dev server with auto-reload:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">export VERIFY_TOKEN=\"any-string-you-like\"\nexport ACCESS_TOKEN=\"EAAG...\"\nexport APP_SECRET=\"your-app-secret\"\nexport PHONE_NUMBER_ID=\"123456789012345\"\nfastapi dev main.py<\/code><\/pre>\n\n\n\n<p>Test verification and a signed event without involving Meta:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">curl \"http:\/\/127.0.0.1:8000\/webhook?hub.mode=subscribe&amp;hub.verify_token=any-string-you-like&amp;hub.challenge=42\"\n\nBODY='{\"entry\":[{\"changes\":[{\"value\":{\"messages\":[{\"from\":\"15551234567\",\"id\":\"wamid.test1\",\"type\":\"text\",\"text\":{\"body\":\"menu\"}}]}}]}]}'\nSIG=$(printf '%s' \"$BODY\" | openssl dgst -sha256 -hmac \"$APP_SECRET\" | awk '{print $NF}')\ncurl -X POST http:\/\/127.0.0.1:8000\/webhook \\\n  -H \"Content-Type: application\/json\" \\\n  -H \"X-Hub-Signature-256: sha256=$SIG\" \\\n  --data-binary \"$BODY\"<\/code><\/pre>\n\n\n\n<p>The first command should print <code>42<\/code>. The second returns <code>{\"ok\":true}<\/code> and makes the bot send the menu to the number in <code>from<\/code> (use your own test number).<\/p>\n\n\n\n<p>To receive real events, Meta requires a public URL with a valid TLS certificate. During development, a tunnel does the job: <a href=\"https:\/\/vectalk.com.br\/blog\/en\/test-whatsapp-webhook-locally\/\">testing the WhatsApp webhook locally with ngrok<\/a> walks through it. With the public URL ready, go to App Dashboard &gt; WhatsApp &gt; Configuration, fill in the callback URL (<code>https:\/\/...\/webhook<\/code>) and the verify token, save and subscribe to the <code>messages<\/code> field.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">7. Simple deployment with Docker and HTTPS<\/h2>\n\n\n\n<p>Create <code>requirements.txt<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">fastapi[standard]\nhttpx<\/code><\/pre>\n\n\n\n<p>And the <code>Dockerfile<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">FROM python:3.12-slim\nWORKDIR \/app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\nCOPY main.py .\nCMD [\"fastapi\", \"run\", \"main.py\", \"--port\", \"8000\"]<\/code><\/pre>\n\n\n\n<p><code>fastapi run<\/code> is production mode, without auto-reload. On a server with Docker:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">docker build -t whatsapp-bot .\ndocker run -d --restart unless-stopped -p 127.0.0.1:8000:8000 \\\n  -e VERIFY_TOKEN -e ACCESS_TOKEN -e APP_SECRET -e PHONE_NUMBER_ID \\\n  whatsapp-bot<\/code><\/pre>\n\n\n\n<p>HTTPS is the missing piece. A simple option is Caddy, which obtains and renews the certificate on its own when the domain&#8217;s DNS points to the server and ports 80 and 443 are open. The whole <code>Caddyfile<\/code> is two lines:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"language-bash\">bot.yourcompany.com\nreverse_proxy 127.0.0.1:8000<\/code><\/pre>\n\n\n\n<p>Update the callback URL in the dashboard to <code>https:\/\/bot.yourcompany.com\/webhook<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Production checklist<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Persistent deduplication.<\/strong> The in-memory <code>set<\/code> disappears on every restart and isn&#8217;t shared across processes. Store processed IDs in Redis or your database with an expiry.<\/li>\n\n\n<li><strong>A queue.<\/strong> If replies depend on an LLM, a database or slow systems, swap <code>BackgroundTasks<\/code> for a queue (Redis, RabbitMQ, SQS) with a separate worker.<\/li>\n\n\n<li><strong>One process for now.<\/strong> With in-memory state, run a single worker. Move state out of the process before scaling.<\/li>\n\n\n<li><strong>Log API errors.<\/strong> The code logs Meta&#8217;s error body. The <a href=\"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-api-error-codes\/\">WhatsApp API error codes<\/a> reference helps you read them.<\/li>\n\n\n<li><strong>Secrets out of the code.<\/strong> Keep the token and app secret in environment variables or a secrets manager.<\/li>\n\n\n<li><strong>Media.<\/strong> Images, audio and documents arrive on the webhook with an ID only; downloading them is a separate call to the media API.<\/li>\n\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Doing it with Vectalk<\/h2>\n\n\n\n<p>Vectalk is a proxy for Meta&#8217;s official WhatsApp Cloud API: send payloads and webhook events are the same as Meta&#8217;s, so <code>main.py<\/code> works without logic changes. Three things change:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>URL and token.<\/strong> Set <code>API_URL=https:\/\/apis.vectalk.com.br\/api\/messages<\/code> and use a Vectalk instance token as <code>ACCESS_TOKEN<\/code>. With it, the <code>phone_number_id<\/code> is resolved automatically.<\/li>\n\n\n<li><strong>Webhook registration via API.<\/strong> You register the URL with <code>POST https:\/\/apis.vectalk.com.br\/api\/webhooks<\/code>, passing <code>instance_id<\/code>, <code>url<\/code> and <code>name<\/code>, and you can have several URLs per instance.<\/li>\n\n\n<li><strong>Endpoint protection.<\/strong> <a href=\"https:\/\/docs.vectalk.com.br\/concepts\/webhooks\">Vectalk&#8217;s webhook documentation<\/a> doesn&#8217;t describe a signature header. Leave <code>APP_SECRET<\/code> empty, set <code>WEBHOOK_KEY<\/code> to a long random value and register the URL as <code>https:\/\/bot.yourcompany.com\/webhook?key=YOUR_VALUE<\/code>.<\/li>\n\n<\/ol>\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_VECTALK_TOKEN\" \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"instance_id\": \"YOUR_INSTANCE\", \"url\": \"https:\/\/bot.yourcompany.com\/webhook?key=YOUR_VALUE\", \"name\": \"Python bot\"}'<\/code><\/pre>\n\n\n\n<p>Vectalk expects a <code>200<\/code> within 20 seconds and resends the event if it doesn&#8217;t get one, which the deduplication already handles. Create your account at <a href=\"https:\/\/apps.vectalk.com.br\/register\">apps.vectalk.com.br\/register<\/a> and check the <a href=\"https:\/\/docs.vectalk.com.br\">documentation<\/a> for the other endpoints. If the next step is answering with AI, the tutorial on <a href=\"https:\/\/vectalk.com.br\/blog\/en\/chatgpt-whatsapp-business-api\/\">connecting ChatGPT to the official WhatsApp API<\/a> starts from a similar base.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">FAQ<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Do I need a WhatsApp-specific Python library?<\/h3>\n\n\n\n<p>No. The Cloud API is plain HTTP and JSON, so FastAPI to receive and httpx (or requests) to send is enough. Third-party SDKs can save some code, but calling the API directly lets you use new features as soon as Meta ships them.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can the bot start the conversation?<\/h3>\n\n\n\n<p>Free-form text and buttons only work inside the 24-hour window opened by the customer&#8217;s last message. To start a conversation, or to reply after that window closes, you must send an approved message template.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How much does it cost to run a bot?<\/h3>\n\n\n\n<p>Replies inside the customer service window are service messages. Since October 1, 2026, each number gets 1,000 free service messages per month; from the 1,001st on, the rate for recipients in Brazil is R$ 0.0350 (Brazilian reais) per message. Rates for other countries are on Meta&#8217;s pricing page.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why does webhook verification fail in the dashboard?<\/h3>\n\n\n\n<p>The usual causes: the URL isn&#8217;t HTTPS with a valid certificate, the server&#8217;s <code>VERIFY_TOKEN<\/code> differs from the one typed in the dashboard, or the route returns the challenge as JSON instead of plain text. That&#8217;s why the code uses <code>PlainTextResponse<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can I use Flask or Django instead of FastAPI?<\/h3>\n\n\n\n<p>Yes. The sequence is the same: answer the verification <code>GET<\/code>, validate the HMAC over the raw body, return <code>200<\/code> fast and call the messages endpoint. In Flask, the raw body comes from <code>request.get_data()<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p><strong>Official sources checked on October 28, 2026:<\/strong> <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/webhooks\/create-webhook-endpoint\" target=\"_blank\" rel=\"noreferrer noopener\">Create a webhook endpoint (Meta)<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/webhooks\/overview\" target=\"_blank\" rel=\"noreferrer noopener\">Webhooks overview (Meta)<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/messages\/interactive-reply-buttons-messages\" target=\"_blank\" rel=\"noreferrer noopener\">Interactive reply buttons messages (Meta)<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/webhooks\/reference\/messages\/interactive\" target=\"_blank\" rel=\"noreferrer noopener\">Interactive messages webhook (Meta)<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/typing-indicators\" target=\"_blank\" rel=\"noreferrer noopener\">Typing indicators (Meta)<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/business-scoped-user-ids\" target=\"_blank\" rel=\"noreferrer noopener\">Business-scoped user IDs (Meta)<\/a>, <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/pricing\" target=\"_blank\" rel=\"noreferrer noopener\">Pricing (Meta)<\/a>, <a href=\"https:\/\/fastapi.tiangolo.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">FastAPI<\/a>, <a href=\"https:\/\/caddyserver.com\/docs\/quick-starts\/reverse-proxy\" target=\"_blank\" rel=\"noreferrer noopener\">Caddy: reverse proxy quick-start<\/a>, <a href=\"https:\/\/docs.vectalk.com.br\/concepts\/webhooks\">Vectalk: webhooks<\/a>, <a href=\"https:\/\/docs.vectalk.com.br\/authentication\">Vectalk: authentication<\/a>, <a href=\"https:\/\/docs.vectalk.com.br\/api-reference\/messages\/send-buttons\">Vectalk: reply buttons<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A complete tutorial to build a WhatsApp bot in Python with FastAPI on the official Cloud API: webhook, signature check, text, buttons and HTTPS deploy.<\/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-535","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 Python Bot with FastAPI: Tutorial | Vectalk<\/title>\n<meta name=\"description\" content=\"WhatsApp API Python tutorial with FastAPI: webhook verification, X-Hub-Signature-256 validation, text replies, interactive buttons and deployment.\" \/>\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-bot-python-fastapi\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"WhatsApp API Python Bot with FastAPI: Tutorial | Vectalk\" \/>\n<meta property=\"og:description\" content=\"WhatsApp API Python tutorial with FastAPI: webhook verification, X-Hub-Signature-256 validation, text replies, interactive buttons and deployment.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-bot-python-fastapi\/\" \/>\n<meta property=\"og:site_name\" content=\"Blog Vectalk\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-28T16:06:51+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-10-28T16:06:55+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=\"12 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-bot-python-fastapi\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/en\\\/whatsapp-bot-python-fastapi\\\/\"},\"author\":{\"name\":\"wanderleyfcunha\",\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/#\\\/schema\\\/person\\\/ff36ecd9d8257856f227f8e5d639db2b\"},\"headline\":\"How to Build a WhatsApp Bot in Python with FastAPI (Official Cloud API, from Scratch)\",\"datePublished\":\"2026-10-28T16:06:51+00:00\",\"dateModified\":\"2026-10-28T16:06:55+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/en\\\/whatsapp-bot-python-fastapi\\\/\"},\"wordCount\":1483,\"articleSection\":[\"Blog\"],\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/en\\\/whatsapp-bot-python-fastapi\\\/\",\"url\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/en\\\/whatsapp-bot-python-fastapi\\\/\",\"name\":\"WhatsApp API Python Bot with FastAPI: Tutorial | Vectalk\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/#website\"},\"datePublished\":\"2026-10-28T16:06:51+00:00\",\"dateModified\":\"2026-10-28T16:06:55+00:00\",\"author\":{\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/#\\\/schema\\\/person\\\/ff36ecd9d8257856f227f8e5d639db2b\"},\"description\":\"WhatsApp API Python tutorial with FastAPI: webhook verification, X-Hub-Signature-256 validation, text replies, interactive buttons and deployment.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/en\\\/whatsapp-bot-python-fastapi\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/en\\\/whatsapp-bot-python-fastapi\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/en\\\/whatsapp-bot-python-fastapi\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/vectalk.com.br\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"How to Build a WhatsApp Bot in Python with FastAPI (Official Cloud API, from Scratch)\"}]},{\"@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 Python Bot with FastAPI: Tutorial | Vectalk","description":"WhatsApp API Python tutorial with FastAPI: webhook verification, X-Hub-Signature-256 validation, text replies, interactive buttons and deployment.","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-bot-python-fastapi\/","og_locale":"en_US","og_type":"article","og_title":"WhatsApp API Python Bot with FastAPI: Tutorial | Vectalk","og_description":"WhatsApp API Python tutorial with FastAPI: webhook verification, X-Hub-Signature-256 validation, text replies, interactive buttons and deployment.","og_url":"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-bot-python-fastapi\/","og_site_name":"Blog Vectalk","article_published_time":"2026-10-28T16:06:51+00:00","article_modified_time":"2026-10-28T16:06:55+00:00","author":"wanderleyfcunha","twitter_card":"summary_large_image","twitter_misc":{"Written by":"wanderleyfcunha","Est. reading time":"12 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-bot-python-fastapi\/#article","isPartOf":{"@id":"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-bot-python-fastapi\/"},"author":{"name":"wanderleyfcunha","@id":"https:\/\/vectalk.com.br\/blog\/#\/schema\/person\/ff36ecd9d8257856f227f8e5d639db2b"},"headline":"How to Build a WhatsApp Bot in Python with FastAPI (Official Cloud API, from Scratch)","datePublished":"2026-10-28T16:06:51+00:00","dateModified":"2026-10-28T16:06:55+00:00","mainEntityOfPage":{"@id":"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-bot-python-fastapi\/"},"wordCount":1483,"articleSection":["Blog"],"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-bot-python-fastapi\/","url":"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-bot-python-fastapi\/","name":"WhatsApp API Python Bot with FastAPI: Tutorial | Vectalk","isPartOf":{"@id":"https:\/\/vectalk.com.br\/blog\/#website"},"datePublished":"2026-10-28T16:06:51+00:00","dateModified":"2026-10-28T16:06:55+00:00","author":{"@id":"https:\/\/vectalk.com.br\/blog\/#\/schema\/person\/ff36ecd9d8257856f227f8e5d639db2b"},"description":"WhatsApp API Python tutorial with FastAPI: webhook verification, X-Hub-Signature-256 validation, text replies, interactive buttons and deployment.","breadcrumb":{"@id":"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-bot-python-fastapi\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/vectalk.com.br\/blog\/en\/whatsapp-bot-python-fastapi\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/vectalk.com.br\/blog\/en\/whatsapp-bot-python-fastapi\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/vectalk.com.br\/blog\/"},{"@type":"ListItem","position":2,"name":"How to Build a WhatsApp Bot in Python with FastAPI (Official Cloud API, from Scratch)"}]},{"@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\/535","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=535"}],"version-history":[{"count":1,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/posts\/535\/revisions"}],"predecessor-version":[{"id":538,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/posts\/535\/revisions\/538"}],"wp:attachment":[{"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/media?parent=535"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/categories?post=535"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vectalk.com.br\/blog\/wp-json\/wp\/v2\/tags?post=535"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}