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 business with multiple WABA numbers — say, one for sales, one for support, one for VIPs — where a customer who pings any of them sees a coherent agent that knows the full relationship. Memory lives at the business level; the WABA number is metadata, not identity.

What you’ll build

A multi-WABA setup where:
  • One business owns N WABA numbers (sales, support, account management, regional lines)
  • Customer memory is shared across all numbers — same user_id, same customer_id
  • Source WABA is preserved in metadata so you can still segment by which line the customer came in on
  • Persona per WABA — the sales agent and the support agent can have different system prompts but the same memory
Est. build time: 60–75 minutes (similar to single-WABA + a router).

When to use this recipe

Build this if:
  • One business runs multiple WhatsApp numbers for different functions
  • A customer who texts your sales line should be recognized when they later text your support line
  • You want each number to act like a specialist (different persona) but with full shared customer history
  • You’re okay with the customer phone being the cross-number identifier (they’re using one phone to reach you)

Architecture at a glance

The customer always gets a reply on the number they texted. Internally, all numbers see the same memory.

Stack

LayerChoice
Synap SDKmaximem-synap (Python) / @maximem/synap (TypeScript)
WhatsAppWhatsApp Cloud API — one webhook covers all WABA numbers under the same Meta Business
FrameworkOpenAI Agents SDK / Vercel AI SDK
LLMOpenAI gpt-4o
RouterSwitch on the incoming phone_number_id (the WABA the message landed on)

Prerequisites

  • A Synap API key — see Authentication
  • Multiple WABA numbers under one Meta Business Manager (so they share one webhook)
  • A System User token with whatsapp_business_messaging scope, valid across all WABAs
  • Python: Python 3.11+
  • TypeScript: Node 18+ and Python 3.11+ on the host
If your WABA numbers are spread across separate Meta Businesses, you’ll need separate webhooks and access tokens — the routing pattern below still works, you just maintain a token map keyed by phone_number_id.

Install

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

Build it

1. The persona registry

One config block defines all the numbers and their personas.
PERSONAS = {
    "1066…sales":   {"name": "sales",   "model": "gpt-4o",      "system": SALES_SYSTEM},
    "1066…support": {"name": "support", "model": "gpt-4o-mini", "system": SUPPORT_SYSTEM},
    "1066…vip":     {"name": "vip",     "model": "gpt-4o",      "system": VIP_SYSTEM},
}

def persona_for(phone_number_id: str) -> dict:
    persona = PERSONAS.get(phone_number_id)
    if not persona:
        raise ValueError(f"Unknown WABA phone_number_id: {phone_number_id}")
    return persona

2. Shared scoping

The customer’s phone is the user_id. The business is the customer_id. The WABA number is in metadata.
  • customer_id = "<your-business>" — single, across all WABAs
  • user_id = <customer phone>
  • conversation_id = <customer phone> — rolling, shared across WABAs
This is the whole point of the recipe: same identity, same memory, regardless of which number they used.

3. The unified webhook

Meta sends every inbound to the same webhook URL with phone_number_id indicating which WABA received it.
@app.post("/webhook/whatsapp")
async def webhook(request: Request, bg: BackgroundTasks):
    body = await request.json()
    for inbound in iter_inbound_with_waba(body):
        bg.add_task(
            handle_inbound,
            phone=inbound["from"],
            text=inbound["text"],
            phone_number_id=inbound["phone_number_id"],  # the WABA that received it
        )
    return {"ok": True}

async def handle_inbound(phone: str, text: str, phone_number_id: str):
    persona = persona_for(phone_number_id)
    CUSTOMER_ID = "your-business"

    # Ingest inbound with source-WABA metadata
    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",
            "source_waba": persona["name"],
            "source_waba_id": phone_number_id,
        },
    )

    reply = await run_persona(persona, phone, text)
    wa_for(phone_number_id).send_message(reply, phone)

    await sdk.memories.create(
        document=f"{persona['name'].capitalize()} agent: {reply}",
        document_type="ai-chat-conversation",
        user_id=phone,
        customer_id=CUSTOMER_ID,
        metadata={
            "channel": "whatsapp",
            "direction": "outbound",
            "source_waba": persona["name"],
        },
    )

4. The persona-aware agent

Each persona has its own system prompt. They all share the same memory pool.
SALES_SYSTEM = """You are the WhatsApp sales agent for <Business>.
You see the same memory as our support and VIP lines. If the customer has had recent
support issues, acknowledge that briefly before pivoting. Don't oversell.
Be concise. Always confirm intent before sending a paywall / signup link."""

SUPPORT_SYSTEM = """You are the WhatsApp support agent for <Business>.
You see the same memory as sales and VIP. If they recently bought, focus on onboarding-style
help. If they're frustrated, prioritize de-escalation over deflection.
Be concise."""

VIP_SYSTEM = """You are the WhatsApp VIP-line agent for <Business>.
The customer reached out on the VIP number — they expect white-glove. You see their
full history including recent sales/support interactions. Match that level of attention."""

async def run_persona(persona: dict, 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=f"wa_{persona['name']}",
        instructions=persona["system"],
        model=persona["model"],
        tools=[
            FunctionTool(synap_search, name_override="synap_search"),
            FunctionTool(synap_store,  name_override="synap_store"),
            # ...persona-specific business tools
        ],
    )
    result = await Runner.run(agent, input=text)
    return result.final_output

5. Source-aware retrieval (optional)

Sometimes a persona wants to look at only its own history (e.g., the support agent reviewing prior support tickets specifically). Filter by metadata:
# All recent support-line interactions for this customer
docs = await sdk.memories.search(
    user_id=phone,
    customer_id=CUSTOMER_ID,
    query="prior support tickets",
    metadata_filter={"source_waba": "support"},
    max_results=10,
)

Run & verify

Sales WABA (yesterday)
Customer (via sales line): How much is the Pro plan?
Sales agent:               Pro is $49/mo, includes priority support + advanced analytics.
                           Want a 14-day trial link?
Customer:                  Maybe later.
Support WABA (today)
Customer (via support line): My export is failing.
Support agent:               I see you were looking at Pro yesterday — for context, exports
                             on Free are limited to 1k rows. Your export was 1,500 rows.
                             Want me to walk you through a workaround, or send you the
                             Pro trial link?
The support agent picked up the sales-line conversation from yesterday — no engineering required to bridge the two numbers. It’s just shared memory.

Customize / extend

  • Regional WABAs sharing memory → use the same pattern, with source_waba: "us" / "eu" etc. Add a language preference memory and the persona auto-greets in the right language.
  • Outbound campaigns per persona → combine with Single-WABA Campaign + Inbound, but scoped per source_waba.
  • Cross-persona handoff → if the sales agent decides the customer is really a support issue, write a memory note and have the next inbound on the support line lean into it.
  • B2B multi-tenant → if the same business has multiple corporate customers, layer in Patterns → Multi-Tenant SaaS by pushing tenant ID into customer_id.

Troubleshooting

Persona ignores cross-line history
  • Confirm customer_id is the same across all personas (it should be the business ID, not the WABA ID).
  • If you accidentally set customer_id = waba_id, you’ve created separate memory pools — fix by re-keying.
Customer gets replied to on the wrong number
  • Make sure wa_for(phone_number_id) uses the right access token / sender ID. Sending from the wrong WABA looks deeply weird to the customer.
Source-WABA metadata missing on some turns
  • Audit the webhook parsing. phone_number_id lives at the entry level in the WhatsApp webhook payload, not on every individual message.
Memory shows wrong persona’s name on past turns
  • The direction: "outbound" documents tag persona name. If you renamed a persona, the historical tag stays — that’s fine, it’s an audit trail, not a routing key.