# A WhatsApp bot in 15 minutes

Your server already talks to Meta's Cloud API; Texa AI answers your customers through it.


**No code at all?** Connect your WhatsApp number in Texa Desk (Settings → Inboxes → Add
inbox → WhatsApp) and switch Texa AI on for it under **Texa AI → Agent and inboxes**.
This guide is for when your own server already talks to WhatsApp through Meta's Cloud
API and you want Texa AI to answer.

## What you need

- A WhatsApp Business number on Meta's Cloud API: its **phone number id** and an
  **access token**. Meta must send its webhook to your server.
- A Texa test key (`texa_test_…`), from Texa AI → Developers.
- Python 3.10+ with `fastapi`, `uvicorn` and `httpx`.

## The whole bot

```python
# bot.py: run with  uvicorn bot:app --port 8000
import os
import httpx
from fastapi import FastAPI, Request

TEXA = httpx.Client(base_url="https://api.texadesk.com/v1", timeout=90,
                    headers={"Authorization": f"Bearer {os.environ['TEXA_API_KEY']}"})
META = httpx.Client(base_url="https://graph.facebook.com/v21.0", timeout=30,
                    headers={"Authorization": f"Bearer {os.environ['META_TOKEN']}"})
PHONE_ID = os.environ["META_PHONE_NUMBER_ID"]
conversations: dict[str, str] = {}      # customer's number -> Texa conversation (use your DB)
app = FastAPI()


def send_whatsapp(to: str, text: str) -> None:
    META.post(f"/{PHONE_ID}/messages", json={
        "messaging_product": "whatsapp", "to": to, "type": "text", "text": {"body": text}})


def conversation_for(number: str, name: str | None) -> str:
    if number not in conversations:
        r = TEXA.post("/conversations", headers={"Idempotency-Key": f"wa-open-{number}"},
                      json={"channel": "whatsapp", "customer_ref": f"wa:{number}",
                            "first_name": name})
        conversations[number] = r.json()["conversation_id"]
    return conversations[number]


@app.post("/whatsapp")                  # Meta's webhook (verify its signature too)
async def incoming(request: Request):
    body = await request.json()
    for entry in body.get("entry", []):
        for change in entry.get("changes", []):
            value = change.get("value", {})
            names = {c["wa_id"]: c["profile"]["name"] for c in value.get("contacts", [])}
            for m in value.get("messages", []):
                if m.get("type") != "text":
                    continue
                number = m["from"]
                cid = conversation_for(number, names.get(number))
                r = TEXA.post(f"/conversations/{cid}/messages",
                              json={"message_id": m["id"], "text": m["text"]["body"]})
                if r.status_code == 409 and r.json()["error"]["code"] == "conversation_closed":
                    conversations.pop(number)          # it ended: start a new one
                    cid = conversation_for(number, names.get(number))
                    r = TEXA.post(f"/conversations/{cid}/messages",
                                  json={"message_id": m["id"], "text": m["text"]["body"]})
                for event in r.json().get("events", []):
                    if event["type"] == "reply":
                        send_whatsapp(number, event["text"])
    return {"ok": True}
```

Run it, point Meta's webhook at `https://your-server/whatsapp`, and message your number.

## Why it is safe to retry

Meta sends a webhook again when your server is slow. Passing Meta's message id as
`message_id` means Texa answers a repeat with the first answer (`"replayed": true`) and
never replies twice. Opening with an `Idempotency-Key` means one number gets one
conversation, however often the first message arrives.

## Things WhatsApp decides

- **The 24-hour window.** You can send a free-form message only within 24 hours of the
  customer's last one. After that, Meta takes only an approved template. A reply that
  arrives later (see below) may need one.
- **Replies that come later.** A voice note or photo is understood before Texa AI answers,
  so its reply can come after your request has returned. Subscribe a
  [webhook](/guides/webhooks) to `conversation.reply` and send those too. Use `reply_id` to
  make sure you send each reply once.
- **People taking over.** When an event is a `handover`, Texa AI stops replying. With
  handover to Texa Desk, your team answers there, and each answer reaches you as the
  `conversation.human_message` webhook: send its `text` to the customer on WhatsApp. See
  [handing over](/guides/handover).
- **Voice notes and photos.** Upload the file with `POST /v1/media` (with the
  `conversation_id`), then send a message whose `attachments` holds `{"media_id": ...}`.

## Going live

Make a live key, swap it in, and nothing else changes. Everything is counted against your
prepaid credits. Watch it in Texa Desk under **Texa AI → Credits and billing**.
