Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.maximem.ai/llms.txt

Use this file to discover all available pages before exploring further.

Status: In Development · Playground demo coming soon. The recipe below is complete and runnable today — only the hosted playground showcase is pending.
A WhatsApp Business agent that handles inbound messages on one WABA number, knows when to hand off to a human, stays out of the way during the human conversation, and picks back up cleanly when the human releases. Memory carries across the whole arc — AI turns, human turns, and the resumption.

What you’ll build

A single-WABA inbound agent that:
  • Takes inbound WhatsApp messages and replies in-thread
  • Detects handoff signals — sentiment, keywords, explicit asks, repeat failure
  • Hands off to a human by surfacing the conversation in your agent console / Slack with full memory context
  • Goes quiet while the human owns the thread
  • Resumes when the human releases — full context, no recap
Est. build time: 60–75 minutes (WhatsApp Cloud API setup is most of it).

When to use this recipe

Build this if:
  • You have one WABA number for one product or business line
  • You have human agents available some of the time but want AI coverage the rest
  • The handoff in and out has to feel seamless to the customer
  • You’re okay with conversation continuity tied to phone number (which it is on WhatsApp)

Architecture at a glance

The “handoff state” is a single key per customer (AI_HANDOFF[phone]). When set, the AI agent stops responding. Human turns are still ingested into Synap so the AI has them when it resumes.

Stack

LayerChoice
Synap SDKmaximem-synap (Python) / @maximem/synap (TypeScript)
WhatsAppWhatsApp Cloud API (Meta), via heyoo (Python) or direct fetch (TS)
FrameworkOpenAI Agents SDK (Python) / Vercel AI SDK (TypeScript)
LLMOpenAI gpt-4o
Handoff stateRedis in production; in-memory dict for the demo

Prerequisites

  • A Synap API key — see Authentication
  • A WABA number, Meta Business app, verified, with a webhook configured to point at your backend
  • A System User access token with whatsapp_business_messaging scope
  • A human console / Slack channel for handoffs
  • Python: Python 3.11+
  • TypeScript: Node 18+ and Python 3.11+ on the host
TypeScript recipe runs on Node only. Pin Next.js route handlers to export const runtime = "nodejs". WhatsApp webhooks expect synchronous 200 responses within 20s — return fast and process async. See Installation → JavaScript / TypeScript SDK.

Install

pip install maximem-synap maximem-synap-openai-agents openai-agents heyoo fastapi uvicorn

Configure

# .env
SYNAP_API_KEY=...
SYNAP_SERVER_URL=<maximem-server>
OPENAI_API_KEY=...
WABA_PHONE_NUMBER_ID=...
WABA_ACCESS_TOKEN=...
WABA_WEBHOOK_VERIFY_TOKEN=...

Build it

1. Identity & scoping

WhatsApp gives you a stable identifier: the customer’s phone number. Use it as user_id.
  • customer_id = "<your-business>" — your business, single tenant
  • user_id = <E.164 phone, hashed if you prefer>
  • conversation_id = <one per phone> — WhatsApp doesn’t have explicit sessions; treat the whole relationship as one rolling conversation
If your privacy posture requires it, hash phone numbers (e.g., sha256("e164:" + phone)) before using them as user_id. Synap will treat the hash as the stable identifier; you keep raw phones in your own DB.

2. Handoff state

# Set when AI hands off; cleared when human releases. Use Redis in production.
AI_HANDOFF: set[str] = set()

def is_handoff(phone: str) -> bool:
    return phone in AI_HANDOFF

def start_handoff(phone: str) -> None:
    AI_HANDOFF.add(phone)

def end_handoff(phone: str) -> None:
    AI_HANDOFF.discard(phone)

3. The handoff tool

The agent decides when to hand off. The tool fans out: persist a structured memory record + notify the human console.
@function_tool
async def hand_off_to_human(phone: str, reason: str, summary: str) -> dict:
    """Bring a human into the thread. Reason: 'customer_asked' | 'frustration' | 'out_of_scope' | 'sensitive'."""
    start_handoff(phone)
    await sdk.memories.create(
        document=f"AI handed off to human. Reason: {reason}. Summary: {summary}",
        document_type="support-handoff",
        user_id=phone,
        customer_id=CUSTOMER_ID,
        metadata={"handoff_reason": reason},
    )
    await notify_human_console(phone, reason, summary)  # your Slack/console push
    return {"status": "handed_off"}

4. The inbound webhook

WhatsApp webhooks deliver inbound messages. Return 200 fast, process async.
from fastapi import FastAPI, Request, BackgroundTasks
from heyoo import WhatsApp

app = FastAPI()
wa = WhatsApp(token=os.environ["WABA_ACCESS_TOKEN"], phone_number_id=os.environ["WABA_PHONE_NUMBER_ID"])
CUSTOMER_ID = "your-business"

@app.post("/webhook/whatsapp")
async def webhook(request: Request, bg: BackgroundTasks):
    body = await request.json()
    msg = wa.get_message(body)
    phone = wa.get_mobile(body)
    if msg and phone:
        bg.add_task(handle_inbound, phone, msg)
    return {"ok": True}

async def handle_inbound(phone: str, text: str):
    # Always ingest the inbound — even during handoff
    await sdk.memories.create(
        document=f"Customer: {text}",
        document_type="ai-chat-conversation",
        user_id=phone,
        customer_id=CUSTOMER_ID,
        metadata={"channel": "whatsapp", "direction": "inbound"},
    )

    if is_handoff(phone):
        return  # human owns the thread; AI stays quiet

    reply = await run_ai(phone, text)
    wa.send_message(reply, phone)

    await sdk.memories.create(
        document=f"Agent: {reply}",
        document_type="ai-chat-conversation",
        user_id=phone,
        customer_id=CUSTOMER_ID,
        metadata={"channel": "whatsapp", "direction": "outbound", "tier": "ai"},
    )

5. The AI agent

SYSTEM = """You are a WhatsApp business agent.

- Be concise — WhatsApp users want short replies.
- Recall the customer's history and prior preferences from memory.
- Hand off to a human when: the customer explicitly asks, they're frustrated, the issue is sensitive
  (financial, medical, legal), or you've tried twice without resolution. Use hand_off_to_human.
- Don't promise anything you can't deliver via tools."""

async def run_ai(phone: str, text: str) -> str:
    synap_search = create_search_tool(sdk=sdk, user_id=phone, customer_id=CUSTOMER_ID)
    synap_store  = create_store_tool(sdk=sdk, user_id=phone, customer_id=CUSTOMER_ID)

    agent = Agent(
        name="wa_agent",
        instructions=SYSTEM,
        tools=[
            FunctionTool(synap_search, name_override="synap_search"),
            FunctionTool(synap_store,  name_override="synap_store"),
            hand_off_to_human,
            # ...your business tools
        ],
    )
    result = await Runner.run(agent, input=text)
    return result.final_output

6. The human side

Two affordances in your console / Slack:
  1. Reply to the customer → posts a message via WABA, ingests it into memory tagged tier: "human".
  2. Release the thread → calls end_handoff(phone). Next customer message routes to the AI again, with full handoff history loaded from memory.
async def human_reply(phone: str, text: str):
    wa.send_message(text, phone)
    await sdk.memories.create(
        document=f"Human agent: {text}",
        document_type="ai-chat-conversation",
        user_id=phone,
        customer_id=CUSTOMER_ID,
        metadata={"channel": "whatsapp", "direction": "outbound", "tier": "human"},
    )

async def human_release(phone: str):
    end_handoff(phone)
    await sdk.memories.create(
        document="Human released thread back to AI.",
        document_type="support-handoff",
        user_id=phone,
        customer_id=CUSTOMER_ID,
        metadata={"handoff_event": "released"},
    )

Run & verify

Customer:  Hi, my order #ORD-22 hasn't arrived
AI:        Hey! ORD-22 is showing delivered 5/14 to 12 Oak St. If that's not your address,
           tell me your correct delivery address and I'll trace it.

Customer:  I never got it. This is the second time. I want to talk to a person.
AI:        Of course — connecting you with a teammate now.
           [hand_off_to_human(reason=customer_asked, summary="2nd missing-order issue,
            ORD-22 marked delivered. Customer wants human.")]

[AI goes silent. Human picks up in console:]

Customer:  ...

Human (in console):  Hi Anish — I see ORD-22 was marked delivered to 12 Oak. Apologies you didn't get it. I'm reissuing #ORD-22-R immediately, free shipping, you'll have it in 2 days. Also flagging your account so this can't happen a third time.
Customer:  Thank you.

[Human clicks "Release" in console. AI_HANDOFF cleared.]
Next day, new message
Customer:  Hey, did the reissue ship?
AI:        Yes — ORD-22-R shipped 5/15, tracking #1Z999. Carrier ETA 5/17.
           Want me to send tracking updates here as they come in?
The next-day reply comes from the AI again, with the human’s resolution and the reissue tracked in memory.

Customize / extend

  • Multiple WABA numbers → see Multi-WABA Shared Memory for the routing pattern.
  • Outbound campaigns + inbound → see Single-WABA Campaign + Inbound.
  • Slack as the human console → post handoff alerts into a #wa-support channel; replies posted in-thread relay back via WABA. Same pattern as Patterns → Slack Bot.
  • Auto-release on inactivity → after N minutes of no human reply, clear AI_HANDOFF automatically (Redis TTL).
  • Tier-escalation flavor → for AI→AI handoff instead of AI→human, see Tier-1 → Tier-2 Escalation.

Troubleshooting

AI replies during a human handoff
  • Race condition: webhook fired before AI_HANDOFF was set. Check is_handoff inside the async task, not in the webhook handler.
AI loses context after the human releases
  • Confirm human turns are being ingested into memory with tier: "human" metadata. If they’re missing, the AI sees a gap and may re-ask basic questions.
Webhook timeouts on Vercel
  • WhatsApp expects a 200 within 20s. Return immediately from the route handler and run handleInbound in the background. Don’t await it.
Customer gets dupe messages
  • WhatsApp redelivers webhooks that don’t 200 fast enough. Idempotency: track message_id in Redis with a short TTL and skip duplicates.
Template messages required for outbound > 24h
  • WhatsApp’s 24-hour rule: outside the customer-initiated session window, you can only send approved template messages. Track session start in Redis; refuse to send free-form replies outside the window.