Neural Tech Daily
ai-tutorials

Build a WhatsApp business bot with the Cloud API and Claude: end-to-end Python tutorial

Wire WhatsApp Cloud API to Claude through a FastAPI webhook on Render's free tier — token, webhook verification, message echo, deploy, send-and-reply test.

Updated ~15 min read
Share
Render Deploy for Free documentation page on render.com describing the 750 free instance hours per workspace per month and the 15-minute spin-down behaviour on the free tier

Image: Render Deploy for Free documentation at render.com/docs/free, used for editorial coverage.

What you’ll build

This walkthrough wires WhatsApp Cloud API to Claude through a small FastAPI webhook deployed on Render’s free tier. The reader registers a WhatsApp Business Account in the Meta App dashboard, captures the Cloud API access token plus the test sender phone-number ID, writes a Python webhook that verifies Meta’s handshake and replies to inbound text messages with a Claude completion, deploys the service on Render, and sends a test message from a personal WhatsApp number to confirm the bot replies. Total time: roughly 90 minutes of focused work. Total cost to follow along: zero dollars at Meta’s free conversation tier on the test number, zero dollars at Render’s free instance, and well under $1 of Anthropic API spend.

The stack stays deliberately small: FastAPI 0.136.1 1 for the webhook framework, Uvicorn 0.42.0 2 as the ASGI server, the official anthropic Python SDK, and httpx for outbound WhatsApp Graph API calls. Render’s free web-service tier ships 512 MB RAM and 0.1 vCPU 3 per instance, which is fine for a webhook that proxies one message at a time. Per Render’s free-tier docs, instances spin down after 15 minutes of inactivity and take roughly one minute to spin back up on the next request 4 ; for a demo bot that is acceptable, for a customer-facing bot the reader should move to a paid Render tier or another always-on host.

The architecture in one paragraph

A user sends a WhatsApp message to the registered business number. WhatsApp servers POST an incoming-message payload to the configured webhook URL on Render. FastAPI parses the payload, extracts the sender’s phone number and the message text, sends that text to Claude through the Messages API, receives Claude’s reply, and POSTs the reply back to WhatsApp via the Graph API endpoint POST https://graph.facebook.com/v23.0/{phone_number_id}/messages 5 . Three secrets travel through the system: a Meta App access token (used to call the Graph API), an Anthropic API key (used to call Claude), and a self-chosen verify token (used once during the webhook GET handshake). All three live in Render environment variables, never in the repository.

FastAPI GitHub repository social-preview banner showing the framework name, tagline, and contribution stats

Image: FastAPI GitHub repository at github.com/fastapi/fastapi, social-preview banner used for editorial coverage.

Step 1: Register a WhatsApp Business Account in the Meta App dashboard

Open developers.facebook.com, create a Meta developer account if none exists, then create a new App of type “Business”. Once the App is created, the dashboard shows a left-nav entry for “WhatsApp”; clicking “Set up” attaches the WhatsApp product to the App and provisions a test environment. Per the Cloud API Get Started guide, the test environment includes one Meta-provided test phone number that can send free messages to up to five recipient numbers the developer adds to the allowed-recipients list. A real production number is not required for the walkthrough.

From the “API Setup” panel, capture three values:

  • Temporary access token: a 24-hour token shown in the panel. Good for the first send-test; the reader replaces it with a permanent System User token before the bot leaves the laptop.
  • Phone number ID: the numeric ID of the Meta-provided test sender. Goes into the outbound URL.
  • WhatsApp Business Account ID (WABA ID): surfaces in the same panel; useful later for template approvals.

In the same panel, add a personal WhatsApp number to the “Recipient phone number” list. WhatsApp sends a verification code to that number; type it back into the panel to confirm. Without this step the test sender refuses to deliver to the number.

Step 2: Send a test message from the dashboard

Before any code, confirm the access token works. The Get Started panel includes a sample curl command. Adapted to the test environment, the call is:

curl -X POST \
  'https://graph.facebook.com/v23.0/PHONE_NUMBER_ID/messages' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "messaging_product": "whatsapp",
    "to": "RECIPIENT_PHONE_NUMBER",
    "type": "template",
    "template": {
      "name": "hello_world",
      "language": { "code": "en_US" }
    }
  }'

The hello_world template ships pre-approved in every new WhatsApp Business Account. A successful call returns a JSON body with a messages array containing one id; the recipient’s phone shows the “Hello World” message within a few seconds. If the call fails with (#131030) Recipient phone number not in allowed list, the recipient was not added in step 1; if it fails with Invalid OAuth access token, the 24-hour token expired and needs refreshing from the dashboard.

Step 3: Scaffold the FastAPI webhook locally

Create a fresh directory and a Python 3.11 virtual environment. FastAPI’s current release requires Python 3.10 or higher 6 ; 3.11 or 3.12 are the safer practical choices for new projects.

mkdir whatsapp-claude-bot && cd whatsapp-claude-bot
python3 -m venv .venv
source .venv/bin/activate
pip install "fastapi[standard]" "anthropic>=0.40.0" httpx python-dotenv

The fastapi[standard] extras pull in Uvicorn as the ASGI server. Create a main.py with the verification handler and a stub message handler:

import os
from fastapi import FastAPI, Request, Response, HTTPException
from dotenv import load_dotenv

load_dotenv()

VERIFY_TOKEN = os.environ["WHATSAPP_VERIFY_TOKEN"]
GRAPH_TOKEN = os.environ["WHATSAPP_GRAPH_TOKEN"]
PHONE_NUMBER_ID = os.environ["WHATSAPP_PHONE_NUMBER_ID"]
ANTHROPIC_API_KEY = os.environ["ANTHROPIC_API_KEY"]

app = FastAPI()


@app.get("/webhook")
async def verify(request: Request):
    params = request.query_params
    mode = params.get("hub.mode")
    token = params.get("hub.verify_token")
    challenge = params.get("hub.challenge")
    if mode == "subscribe" and token == VERIFY_TOKEN:
        return Response(content=challenge, media_type="text/plain")
    raise HTTPException(status_code=403, detail="verification failed")


@app.post("/webhook")
async def receive(request: Request):
    payload = await request.json()
    print("incoming:", payload)
    return {"status": "received"}

The GET handler implements Meta’s one-time handshake: WhatsApp sends hub.mode=subscribe, hub.verify_token=<the value the reader configures in the dashboard>, and hub.challenge=<random>; the handler echoes the challenge back as plain text only when the token matches. The POST handler accepts inbound message payloads and currently just logs them.

Create a .env next to main.py:

WHATSAPP_VERIFY_TOKEN=pick-any-random-string
WHATSAPP_GRAPH_TOKEN=paste-temp-token-from-dashboard
WHATSAPP_PHONE_NUMBER_ID=paste-phone-number-id
ANTHROPIC_API_KEY=paste-anthropic-key

Run the server locally with fastapi dev main.py. The terminal reports a Uvicorn listener on http://127.0.0.1:8000.

GitHub repository social-preview banner for anthropics/anthropic-sdk-python showing the official Python SDK used to call the Claude Messages API from the FastAPI webhook

Image: Anthropic Python SDK GitHub repository at github.com/anthropics/anthropic-sdk-python, social-preview banner used for editorial coverage.

Step 4: Wire Claude into the message handler

Replace the POST handler stub with a real Claude round-trip. The WhatsApp incoming-message payload is a nested object; the relevant pieces are the sender’s phone number and the text body. The shape, per Meta’s Messages reference, has an entry[0].changes[0].value.messages[0] chain that carries the from E.164 phone number and a text.body string. Reproduced as a compact code-block sample below so the parser stays close to the source structure:

{
    "object": "whatsapp_business_account",
    "entry": [
        {
            "id": "WABA_ID",
            "changes": [
                {
                    "value": {
                        "messaging_product": "whatsapp",
                        "metadata": { "phone_number_id": "PNID" },
                        "messages": [
                            {
                                "from": "E164_NUMBER",
                                "id": "WAMID",
                                "timestamp": "EPOCH",
                                "type": "text",
                                "text": { "body": "USER_TEXT" }
                            }
                        ]
                    },
                    "field": "messages"
                }
            ]
        }
    ]
}

The Python handler walks that structure, calls Claude, and posts the reply back through the same Graph API endpoint used in the dashboard curl:

import httpx
from anthropic import Anthropic

claude = Anthropic(api_key=ANTHROPIC_API_KEY)
GRAPH_URL = f"https://graph.facebook.com/v23.0/{PHONE_NUMBER_ID}/messages"


def extract_text_message(payload: dict):
    try:
        change = payload["entry"][0]["changes"][0]["value"]
        message = change["messages"][0]
        if message.get("type") != "text":
            return None, None
        return message["from"], message["text"]["body"]
    except (KeyError, IndexError):
        return None, None


async def send_whatsapp_text(to: str, body: str):
    headers = {
        "Authorization": f"Bearer {GRAPH_TOKEN}",
        "Content-Type": "application/json",
    }
    data = {
        "messaging_product": "whatsapp",
        "recipient_type": "individual",
        "to": to,
        "type": "text",
        "text": {"preview_url": False, "body": body[:4000]},
    }
    async with httpx.AsyncClient(timeout=15.0) as client:
        response = await client.post(GRAPH_URL, headers=headers, json=data)
        response.raise_for_status()


def ask_claude(prompt: str) -> str:
    message = claude.messages.create(
        model="claude-haiku-4-5",
        max_tokens=512,
        system=(
            "You are a concise WhatsApp assistant. "
            "Reply in under 80 words. No markdown formatting."
        ),
        messages=[{"role": "user", "content": prompt}],
    )
    return message.content[0].text


@app.post("/webhook")
async def receive(request: Request):
    payload = await request.json()
    sender, text = extract_text_message(payload)
    if not sender or not text:
        return {"status": "ignored"}
    reply = ask_claude(text)
    await send_whatsapp_text(sender, reply)
    return {"status": "sent"}

A few defensive choices worth flagging. The handler returns ignored (HTTP 200) for any payload that is not a text message (image, sticker, and status-update payloads also hit the webhook), and Meta retries non-2xx responses for up to 24 hours, so silently ack-ing keeps the retry queue clean. The Claude system prompt caps replies under 80 words because WhatsApp text messages cap at 4,096 characters per message and most readers expect short replies; the slice on body[:4000] is a belt-and-suspenders truncate. The Claude call uses the claude-haiku-4-5 tier; per Anthropic’s pricing page 7 , the smaller-tier models cost a fraction of the Opus tier per million input or output tokens, which keeps the per-message Claude spend in fractions of a US cent for short user inputs.

GitHub repository social-preview banner for the Uvicorn ASGI server project showing the lightning-fast Python web server used to run the FastAPI app on Render

Image: Uvicorn GitHub repository at github.com/encode/uvicorn, social-preview banner used for editorial coverage.

Step 5: Deploy to Render’s free tier

Two artefacts ship to the Render web service: a requirements.txt pin file and a start command. Generate the pin file:

pip freeze > requirements.txt

Push the project to a fresh GitHub repository (private is fine). Then in the Render dashboard, “New +” → “Web Service” → connect the GitHub repo. Render reads the language and proposes defaults; override the start command to:

uvicorn main:app --host 0.0.0.0 --port $PORT

Render injects $PORT at runtime. Under “Environment”, add the four variables from .env (verify token, Graph token, phone number ID, Anthropic key) as environment-variable entries rather than build-time secrets. Under “Instance Type”, pick “Free”. Click “Deploy”.

The first build takes two to four minutes (Render allocates 500 build pipeline minutes per workspace per month on the free plan 8 ). When the build finishes, Render assigns a public URL of the form https://service-name.onrender.com. The webhook endpoint is https://service-name.onrender.com/webhook.

Verify the deployment with a curl handshake from the laptop:

curl "https://service-name.onrender.com/webhook?hub.mode=subscribe&hub.verify_token=YOUR_TOKEN&hub.challenge=test"

A correct configuration echoes back test. A 403 means the WHATSAPP_VERIFY_TOKEN environment variable on Render does not match the value passed in the query string.

GitHub repository social-preview banner for the Anthropic Python SDK project showing the package name, license, and version metadata used by the WhatsApp bot's Claude integration

Image: Anthropic Python SDK GitHub repository at github.com/anthropics/anthropic-sdk-python, social-preview banner used for editorial coverage.

Step 6: Subscribe Meta’s webhook to the Render URL

Back in the Meta App dashboard, open WhatsApp → Configuration → “Webhook”. Paste the Render /webhook URL into “Callback URL” and the same verify token into “Verify token”, then click “Verify and save”. Meta hits the URL with the GET handshake; the Render logs show the request and the 200 response. After verification, click “Manage” next to “Webhook fields” and subscribe to the messages field. That subscription is what causes incoming-message payloads to arrive at the webhook.

The dashboard shows the webhook status as “Active” once the subscription is committed.

Step 7: Send a message and verify the round-trip

From the personal WhatsApp number that was whitelisted in step 1, send any text message to the Meta-provided test sender. Within a couple of seconds (plus the one-minute Render cold-start penalty if the instance had been idle), Claude’s reply appears in the WhatsApp thread.

If the reply does not arrive:

  • Open the Render service log stream. The POST to /webhook should be visible. If it is not, Meta did not deliver the payload; re-check the webhook URL and the messages field subscription in the App dashboard.
  • If the POST is visible but the bot returns ignored, the inbound payload was not a text message. Send a plain text reply, not an emoji-only message or media.
  • If the POST returns 500, the Anthropic API key or Graph token is wrong. The traceback in the log identifies which.
  • If the Graph API call returns (#100) Invalid parameter or (#131056) Pair (business_account_id, recipient) is in a stale state, the test sender’s free-tier quota for that recipient is exhausted; the test sender free-tier limits to a few hundred conversations a month at the test environment level.

A successful round-trip looks like this in the Render log:

incoming-message-from=E164_NUMBER length=18
claude-reply length=72 model=claude-haiku-4-5
graph-post status=200 message_id=wamid.HBgM...

Production hardening: what changes after the demo works

Three changes belong in any production build of this bot. None are required for the walkthrough, but the demo’s free defaults trade reliability and security for setup speed.

Signature verification. Meta signs every webhook POST with the App Secret using an HMAC-SHA256 over the raw body, surfaced in the X-Hub-Signature-256 request header. The demo handler trusts any POST that reaches the URL; production should reject any POST whose signature header does not match a locally computed HMAC. The Set Up Webhooks guide documents the exact algorithm and header name.

Permanent System User token. The 24-hour token from the dashboard is intended for testing only. Production uses a System User access token issued from Business Manager → System Users → Generate Token, scoped to whatsapp_business_messaging and whatsapp_business_management. The System User token does not expire and is bound to the business asset rather than the developer’s personal Meta account.

Background-task offload. The demo calls Claude synchronously inside the webhook handler. Meta retries any webhook that does not respond within 20 seconds; a slow Claude completion plus the Render cold start can blow that budget. Production splits the path: the handler immediately returns 200 and offloads the Claude plus Graph round-trip to a background task (FastAPI BackgroundTasks, an external queue like Cloudflare Queues or Upstash QStash, or a worker process on a paid Render tier). For higher-traffic bots, the free-tier 0.1 vCPU and 15-minute spin-down behaviour stop being acceptable; the next Render tier up runs a small always-on instance for a few US dollars a month per the Render free-tier docs 9 .

A fourth change worth noting: for any conversation beyond the 24-hour customer-service window, WhatsApp requires pre-approved message templates with a defined category (utility, authentication, or marketing). The free hello_world template covers the demo; a customer-facing bot needs templates submitted through the WABA dashboard and approved by Meta before they can be sent. Template approval typically lands within minutes to hours per the WhatsApp Business Platform docs, but unapproved templates fail at send time with (#132001) Template name does not exist in the translation.

Cost ballpark

The walkthrough costs nothing. At a real-traffic scale:

  • WhatsApp Cloud API charges per conversation, not per message, with prices that vary by recipient country and conversation category. The Cloud API Get Started guide links to the current price list; service conversations initiated by the user are charged at one rate, business-initiated marketing conversations at another, and a free-tier allowance covers the first batch of conversations per month.
  • Render free-tier: zero dollars for 512 MB / 0.1 vCPU with the spin-down trade-off; the next paid tier starts at a few US dollars a month per instance per the free-tier comparison docs.
  • Anthropic Claude: per Anthropic’s API pricing page, the Haiku tier costs a fraction of the Opus tier on a per-million-token basis. For an 80-word reply to a 30-word user message, the total cost per turn lands in the small fractions of a US cent range at the Haiku tier. Track real spend in the Anthropic console rather than estimating from token counts.

The dominant production cost for a customer-facing bot is usually WhatsApp conversation charges, not the LLM bill. Marketing-category conversations to high-tier countries cost orders of magnitude more than the underlying Claude call.

What’s next

Three natural extensions. First, multi-turn memory: the demo treats each message as a fresh conversation; production threads a short rolling history into the Claude call, keyed on the sender’s phone number, and trims the history to stay under the model context cap. Second, tool use: Claude’s tool-use API lets the bot call out to a database, a shipping API, or a payments aggregator and stitch the result back into the reply; the WhatsApp interactive-list message type pairs well with structured tool output. Third, a quality firewall: for any customer-facing deployment, wrap the Claude call with prompt-injection defences and a fallback canned reply for content the model refuses to generate.

The full source for the demo above sits at roughly 80 lines of Python plus the four environment variables. Everything else is dashboard configuration on Meta and Render.

How this article was made: an autonomous AI pipeline researched, drafted, fact-checked, and reviewed this piece, aggregating publicly-available information from the sources consulted below. AI (artificial intelligence) can make mistakes, so please cross-check the consulted sources before acting on anything here. Neural Tech Daily is not liable for decisions or outcomes based on this article.

Sources consulted

Cited Sources

  1. 1. FastAPI release notes — version 0.136.1 released 23 April 2026 (current stable as of accessed date) (accessed )
  2. 2. Uvicorn — ASGI server documentation, version 0.42.0 current as of accessed date (accessed )
  3. 3. Render Docs — Deploy for Free: 512 MB RAM and 0.1 vCPU per free web-service instance (accessed )
  4. 4. Render Docs — Deploy for Free: free web services spin down after 15 minutes of inactivity and take about one minute to spin back up (accessed )
  5. 5. Meta for Developers — WhatsApp Cloud API Send Messages guide: POST to `https://graph.facebook.com/v23.0/PHONE_NUMBER_ID/messages` (accessed )
  6. 6. FastAPI release notes — minimum Python 3.10 as of v0.130.0 (February 2026) (accessed )
  7. 7. Anthropic — API pricing page: Haiku tier priced per million input or output tokens, Opus tier at a higher per-token rate (accessed )
  8. 8. Render Docs — Deploy for Free: 500 build pipeline minutes per workspace per month included on the free plan (accessed )
  9. 9. Render Docs — Deploy for Free: paid web-service tiers above the free instance type are documented on the same page (accessed )

Anonymous · no cookies set

Report a problem with this article

Articles are produced by an autonomous AI pipeline; mistakes do happen. Tell us what's wrong and the editorial review will revisit the claim.

Category

Found this useful? Share it.