# Twilio

> Give an assistant on SMS, or on WhatsApp through Twilio, a memory of each person who texts it. What is known about them goes in front of the model before each reply, and each exchange is saved.

Give an assistant that people text a memory of each of them. Twilio sends every message to your webhook,
for SMS and for WhatsApp alike; your server puts what is known about the sender in front of Claude and
answers in the same request. The next message starts knowing this one.

## Install

```bash
pip install fastapi uvicorn python-multipart anthropic twilio geniffy
```

```bash
uv add fastapi uvicorn python-multipart anthropic twilio geniffy
```

Set `GENIFFY_API_KEY` from **API keys** in the Geniffy app and `ANTHROPIC_API_KEY`. From Twilio, set
`TWILIO_AUTH_TOKEN`, and set `TWILIO_WEBHOOK_URL` to the address you give Twilio below, exactly as you give
it: Twilio signs every request over that address.

## Your server

```python
import os

from anthropic import AsyncAnthropic
from fastapi import BackgroundTasks, FastAPI, HTTPException, Request, Response
from geniffy import AsyncGeniffy
from twilio.request_validator import RequestValidator
from twilio.twiml.messaging_response import MessagingResponse

geniffy = AsyncGeniffy()                          # reads GENIFFY_API_KEY
claude = AsyncAnthropic()                         # reads ANTHROPIC_API_KEY
twilio = RequestValidator(os.environ["TWILIO_AUTH_TOKEN"])
app = FastAPI()
recent: dict[str, list] = {}                      # each person's last few turns, for "yes" and "that one"


def space_for(number: str) -> str:
    """The sender, by the digits of their number: the same person whether they text or use WhatsApp."""
    digits = "".join(ch for ch in number if ch.isdigit())
    if not digits:
        raise HTTPException(400, "This message has no sender number.")
    return f"user_{digits}"


@app.post("/twilio")
async def receive(request: Request, background: BackgroundTasks):
    form = {k: str(v) for k, v in (await request.form()).items()}
    if not twilio.validate(os.environ["TWILIO_WEBHOOK_URL"], form, request.headers.get("X-Twilio-Signature", "")):
        raise HTTPException(403)
    text, reply = (form.get("Body") or "").strip(), MessagingResponse()
    if text:
        mem = geniffy.space(space_for(form.get("From", "")))
        turn = {"role": "user", "content": text}
        past = recent.setdefault(form["From"], [])
        response = await claude.messages.create(
            model="claude-opus-5-5",
            max_tokens=600,
            system=f"You are a helpful assistant over text messages. Keep replies short.\n\n"
                   f"<memory>\n{await mem.context(text)}\n</memory>",
            messages=[*past[-10:], turn],
        )
        said = "".join(block.text for block in response.content if block.type == "text")
        reply.message(said)
        past += [turn, {"role": "assistant", "content": said}]
        background.add_task(mem.memories.add, messages=[turn, {"role": "assistant", "content": said}])
    return Response(str(reply), media_type="application/xml")
```

Run it with `uvicorn server:app`, somewhere Twilio can reach over HTTPS.

Twilio waits up to 15 seconds for the answer, so the reply goes back in the same request and the exchange is
saved after it has gone. The last few turns of each conversation are kept in memory, which is right for one
process; with more than one, keep them where every process can see them.

## In Twilio

Set the address where a message comes in to `https://your-server/twilio` (HTTP POST):

- **SMS:** on your phone number, under **Messaging**, **A message comes in**.
- **WhatsApp:** on your WhatsApp sender, or the WhatsApp Sandbox while you build, under **When a message
  comes in**.

Each person is the space named by their number's digits, so the same person is one memory over SMS and
WhatsApp. If you know them as customers, use your customer id instead. When nothing is known about the
sender, the memory says so in one sentence, so Claude says it doesn't know instead of guessing.

Source: https://docs.geniffy.com/integrations/twilio
