WhatsApp API & Developers

Connect an AI Agent to WhatsApp: Build, Policy, Handoff

The same twenty questions arrive on WhatsApp every day: opening hours, prices, "can I move my booking", "where is the clinic". A language model can answer most of them in seconds, at 11 pm, in the customer's language. So the plan writes itself: connect an AI agent to WhatsApp, let it handle the routine, and let people handle the rest.

The plumbing is the easy part. A webhook brings the message in, a model writes a reply, an API sends it out. What decides whether the agent helps or embarrasses you is everything around that loop: Meta's policy on AI, a handoff that actually reaches a person, and guardrails that keep the agent inside its job. This guide covers all of it, with a working Python agent you can adapt.

Start with the policy line

Before writing code, know what Meta allows. As of October 2026, the WhatsApp Business Solution Terms bar general-purpose AI assistants from AI model providers from the platform, effective January 15, 2026 (and applied to new users from October 15, 2025). A chatbot whose product is "ask me anything" is out.

Businesses using AI for their own customer service are still allowed. A dental clinic's booking assistant, a store's order-status helper or a SaaS company's support bot are all on the right side of the line. The test is whether the AI serves your customers about your business, or is itself the product. Our explainer on the WhatsApp AI chatbot policy covers the edge cases.

Two more facts shape the build:

  • Meta now sells its own agent. Meta Business Agent, launched globally in June 2026, is Meta's AI customer-service agent for WhatsApp, Messenger and Instagram. It is billed per token, at $2.00 per million tokens per Meta's pricing page, with a typical message around 20,000 to 25,000 tokens, or roughly 4 to 5 cents. Compare that with building your own before you start.
  • Linked numbers are not exempt from common sense. If your agent runs on an ordinary WhatsApp number through a linked-device API, the Business Solution Terms are not what governs you, but WhatsApp's own terms and spam enforcement are. An agent that messages people unprompted, or floods a chat, is the fastest way to lose the number.

The architecture

Every WhatsApp agent, on any API, is the same loop:

  1. Receive the inbound message on a webhook.
  2. Verify the signature, deduplicate on the event or message id, and acknowledge with a 2xx immediately.
  3. Filter. Skip groups, media you cannot handle, and chats a human has taken over.
  4. Load context: recent turns of the conversation, plus the customer's record if you have one.
  5. Call the model with a tight system prompt and a small set of tools, one of which is "hand off to a human".
  6. Check the result. If the model handed off, refused or ran out of tokens, do not send its text.
  7. Send the reply, with an idempotency key so retries cannot double-send.
  8. Record what was sent, so the next turn has history and humans can audit it.

Steps 2 and 7 are where most homemade bots break: slow handlers that make the webhook retry, and retries that make the bot answer twice. For the inbound side in detail, see receiving WhatsApp messages by API.

A working agent in Python

This agent uses Flask, the Anthropic Python SDK with a small, fast model, and Tenashi's API and webhooks. Swap the send function for Cloud API if you are on the official platform; the logic is the same. Keep secrets in environment variables.

import hashlib, hmac, json, os, threading, time
import anthropic, requests
from flask import Flask, request

app = Flask(__name__)
claude = anthropic.Anthropic()                       # reads ANTHROPIC_API_KEY
SECRET = os.environ["TENASHI_WEBHOOK_SECRET"]        # whsec_... from Settings
API = "https://usetenashi.com/api/v1/messages"
AUTH = {"Authorization": f"Bearer {os.environ['TENASHI_KEY']}"}

SYSTEM = """You answer WhatsApp messages for Brightsmile Dental, Pune.
Only help with: opening hours, the price list below, booking or moving
appointments, and directions. Never give medical advice. Never promise
refunds or discounts. Call handoff_to_human if the customer asks for a
person, is upset, mentions pain or an emergency, or asks anything else.
Reply in the customer's language, in under 60 words.

Hours: Mon to Sat, 9:00 to 19:00. Cleaning Rs 1,500. Consultation Rs 500."""

HANDOFF = {
    "name": "handoff_to_human",
    "description": "Pass this chat to a human teammate when the request is out of scope, sensitive, or the customer wants a person.",
    "input_schema": {
        "type": "object",
        "properties": {"reason": {"type": "string"}},
        "required": ["reason"],
        "additionalProperties": False,
    },
    "strict": True,
}

history, paused, bot_ids, seen = {}, set(), set(), set()   # use a database in production

def verify(headers, raw: bytes) -> bool:
    ts = headers.get("X-Tenashi-Timestamp", "0")
    sig = headers.get("X-Tenashi-Signature", "").removeprefix("v1=")
    expected = hmac.new(SECRET.encode(), ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
    return abs(time.time() - int(ts)) < 300 and hmac.compare_digest(sig, expected)

def remember(conv, role, text):
    turns = history.setdefault(conv, [])
    if turns and turns[-1]["role"] == role:
        turns[-1]["content"] += "\n" + text
    else:
        turns.append({"role": role, "content": text})
    del turns[:-20]                                  # keep the recent turns
    while turns and turns[0]["role"] != "user":
        turns.pop(0)

def send(conv, text, key):
    r = requests.post(API, headers={**AUTH, "Idempotency-Key": key},
                      json={"conversationId": conv, "text": text}, timeout=15)
    r.raise_for_status()
    bot_ids.add(r.json()["message"]["id"])

def handle(msg):
    conv = msg["conversationId"]
    remember(conv, "user", msg["text"])
    resp = claude.messages.create(
        model="claude-haiku-5-5",
        max_tokens=4096,
        system=SYSTEM,
        tools=[HANDOFF],
        output_config={"effort": "low"},
        messages=history[conv],
    )
    text = "".join(b.text for b in resp.content if b.type == "text").strip()
    if resp.stop_reason != "end_turn" or not text:  # handoff, refusal or cut off
        paused.add(conv)
        send(conv, "Thanks, I'm passing this to a teammate who will reply here shortly.",
             f"handoff-{msg['id']}")
        return
    remember(conv, "assistant", text)
    send(conv, text, f"agent-{msg['id']}")

@app.post("/hooks/tenashi")
def webhook():
    raw = request.get_data()
    if not verify(request.headers, raw):
        return "", 401
    event = json.loads(raw)
    msg = event.get("data", {}).get("message")
    if event["id"] in seen or not msg:
        return "", 200
    seen.add(event["id"])
    conv = msg["conversationId"]
    if event["type"] == "message.sent" and msg["id"] not in bot_ids:
        paused.add(conv)                             # a person replied, so the agent steps back
    elif (event["type"] == "message.received" and msg["chat"]["type"] == "dm"
          and msg["type"] == "text" and conv not in paused):
        threading.Thread(target=handle, args=(msg,)).start()
    return "", 200

What the design choices buy you:

  • Acknowledge, then work. The handler returns 200 at once and calls the model on a thread. Tenashi expects a 2xx within 10 seconds and retries failures; Cloud API also retries. In production, use a job queue with one worker per conversation so two quick messages are answered in order.
  • Handoff is a tool, not a keyword. The model calls handoff_to_human with a reason. Any stop other than a normal end of turn, including a refusal or a cut-off reply, also hands off, so half-formed text never reaches a customer.
  • The agent notices people. Tenashi fires message.sent for every outgoing message, including replies typed in the inbox or on the phone. Any sent message the bot did not send means a teammate is talking, so the agent pauses that chat.
  • DMs only. The filter skips groups. An agent answering in a customer group is a different product with different risks.

Our WhatsApp API Python tutorial covers the Cloud API send and webhook verification in Python if you are on the official platform.

Design the handoff before the happy path

The handoff decides whether customers trust the agent. Define it explicitly:

  • Triggers. The customer asks for a person, sounds upset, mentions money disputes, health or safety, repeats a question the agent already failed, or goes outside scope.
  • What the customer sees. One honest line: a teammate will reply here, ideally with when ("within the hour, 9 to 7").
  • What the teammate sees. The full chat, the handoff reason and the customer's record in one place, not a Slack ping with a link to logs.
  • When the agent comes back. Only when a person closes or releases the chat. An agent that jumps back in mid-conversation undoes the handoff.

Guardrails that keep it useful

  • Narrow scope, written down. List what the agent may do. Anything else is a handoff.
  • Ground answers in your data. Put prices, hours and policies in the prompt or fetch them from your system. Never let the model guess a price.
  • No commitments. Refunds, discounts, delivery promises and medical or legal advice go to people.
  • Treat messages as untrusted input. Customers can type "ignore your instructions". Keep tools narrow so a hijacked prompt cannot do damage.
  • Rate-limit per customer. Cap turns per hour so a loop or a prankster cannot run up your model bill.
  • Review transcripts weekly. Read handoffs and low-rated chats, then adjust the prompt.

Running it on Cloud API instead

On Meta's official platform, the loop is the same with three differences. The agent can send free-form replies only inside the 24-hour customer service window, which opens each time the customer writes; outside it you need a template, as the guide to the WhatsApp 24-hour window explains. Those replies are service messages, and from October 1, 2026 each number gets 1,000 free a month before they are billed. And you parse Meta's webhook shape, entry[].changes[].value.messages[], verified with X-Hub-Signature-256.

Where Tenashi fits

Tenashi gives an agent the two things it needs, a signed webhook for every inbound DM and an API to reply, on the WhatsApp number your customers already use, with no template approvals or per-message fees, from $18/mo. Every reply the agent sends lands in the shared inbox, so a teammate who takes over sees the whole conversation, and the CRM record sits under the chat.

If you would rather keep people in charge, Tenashi's built-in AI on Growth and Scale drafts suggested replies, summarizes long chats, reads text from images and translates inline, so your team answers faster without a bot answering for them. See AI replies for WhatsApp.

The honest limits: Tenashi is not Meta's official API, sends are paced at one message every 4 to 12 seconds per number, so replies are not instant under load, and WhatsApp can still restrict numbers that behave like spam bots. Text, media and quoted replies only, no buttons. For the official route, see the complete guide to the WhatsApp API.

Read the API docs · Get started

FAQ

Is it allowed to connect an AI chatbot to WhatsApp?

Yes, for your own customer service. Meta's Business Solution Terms, effective January 15, 2026, bar general-purpose AI assistants from AI model providers, but businesses using AI to answer their customers about bookings, orders or support are still allowed. Keep the agent scoped to your business and give customers a route to a person.

Which AI model should I use for a WhatsApp agent?

A small, fast model is usually enough for scoped customer service: short replies, a narrow prompt and one or two tools. Latency and cost per message matter more than raw capability. Test on a few hundred real past messages before going live, and move up a model size only where answers fall short.

How does an AI agent hand a WhatsApp chat to a human?

Give the model a handoff tool it calls when a request is out of scope, sensitive or the customer asks for a person. When it fires, send a short holding message, pause the agent for that chat and put the conversation in front of a teammate with the reason and full history.

Can an AI agent reply in WhatsApp groups?

Technically, on a linked-number API that sees groups, yes. In practice it is a bad default: groups mix many people, consent is murky and mistakes are public. Meta's Groups API only covers groups created through it. Start with direct messages and let people handle groups.

How much does a WhatsApp AI agent cost to run?

Three parts: model tokens for each reply, WhatsApp messaging costs and your hosting. On Cloud API, agent replies are service messages, billed after 1,000 free per number per month from October 2026. On a flat-fee tool like Tenashi there are no per-message fees, so model tokens dominate.

An agent that knows when to step aside is worth more than one that answers everything. Read the Tenashi API docs and wire yours to the inbox your team already works.