> ## 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.

# WhatsApp: Multi-WABA Same-Business Shared Memory

> Multiple WhatsApp Business numbers under one business sharing a single customer memory pool.

<Info>
  **Status:** In Development · Playground demo coming soon.
  The recipe below is complete and runnable today; only the hosted playground showcase is pending.
</Info>

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

```mermaid theme={null}
flowchart TD
    Customer[Customer phone] --> Sales[Sales WABA]
    Customer --> Support[Support WABA]
    Customer --> VIP[VIP WABA]
    Sales --> Webhook[Unified webhook]
    Support --> Webhook
    VIP --> Webhook
    Webhook --> Router[Identify source WABA<br/>route to persona]
    Router --> Fetch[(Synap context fetch<br/>all numbers share memory)]
    Fetch --> Agent[Persona agent<br/>system prompt varies by WABA]
    Agent -->|reply via same WABA| Customer
    Agent -.-> Ingest[("Synap ingest turn<br/>metadata: source_waba")]
```

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

## Stack

| Layer         | Choice                                                                                          |
| ------------- | ----------------------------------------------------------------------------------------------- |
| **Synap SDK** | `maximem-synap` (Python) / `@maximem/synap-js-sdk` (TypeScript)                                 |
| **WhatsApp**  | WhatsApp Cloud API: one webhook covers all WABA numbers under the same Meta Business            |
| **Framework** | [OpenAI Agents SDK](/integrations/openai-agents) / [Vercel AI SDK](/integrations/vercel-ai-sdk) |
| **LLM**       | OpenAI `gpt-4o`                                                                                 |
| **Router**    | Switch on the incoming `phone_number_id` (the WABA the message landed on)                       |

## Prerequisites

* A Synap API key. See [Authentication](/setup/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

<Note>
  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`.
</Note>

### Install

<CodeGroup>
  ```bash Python theme={null}
  pip install maximem-synap maximem-synap-openai-agents openai-agents heyoo
  ```

  ```bash uv theme={null}
  uv add maximem-synap maximem-synap-openai-agents openai-agents heyoo
  # pip-compatible (existing venv): uv pip install maximem-synap maximem-synap-openai-agents openai-agents heyoo
  ```

  ```bash TypeScript theme={null}
  npm install @maximem/synap-js-sdk @maximem/synap-vercel-adk ai @ai-sdk/openai
  ```
</CodeGroup>

## Build it

### 1. The persona registry

One config block defines all the numbers and their personas.

<CodeGroup>
  ```python Python theme={null}
  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
  ```

  ```typescript TypeScript theme={null}
  const PERSONAS: Record<string, { name: string; model: string; system: string }> = {
    "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 },
  };

  function personaFor(phoneNumberId: string) {
    const p = PERSONAS[phoneNumberId];
    if (!p) throw new Error(`Unknown WABA phone_number_id: ${phoneNumberId}`);
    return p;
  }
  ```
</CodeGroup>

### 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.

<Note>
  `conversation_id`, `user_id`, and `customer_id` must be valid UUIDs. Since these key off the customer phone, derive a deterministic UUID from it with `uuid.uuid5(...)` (Python) rather than passing the raw phone string. The same phone always maps to the same UUID, preserving the shared-identity behavior.
</Note>

### 3. The unified webhook

Meta sends every inbound to the same webhook URL with `phone_number_id` indicating which WABA received it.

<CodeGroup>
  ```python Python theme={null}
  @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"],
          },
      )
  ```

  ```typescript TypeScript theme={null}
  export const runtime = "nodejs";
  const CUSTOMER_ID = "your-business";

  export async function POST(req: Request) {
    const body = await req.json();
    for (const inbound of iterInboundWithWaba(body)) {
      handleInbound(inbound.from, inbound.text, inbound.phoneNumberId)
        .catch(console.error);
    }
    return Response.json({ ok: true });
  }

  async function handleInbound(phone: string, text: string, phoneNumberId: string) {
    const persona = personaFor(phoneNumberId);

    await synap.sdk.memories.create({
      document: `Customer: ${text}`,
      documentType: "ai-chat-conversation",
      userId: phone,
      customerId: CUSTOMER_ID,
      metadata: {
        channel: "whatsapp",
        direction: "inbound",
        sourceWaba: persona.name,
        sourceWabaId: phoneNumberId,
      },
    });

    const reply = await runPersona(persona, phone, text);
    await sendWaMessage(phoneNumberId, phone, reply);

    await synap.sdk.memories.create({
      document: `${persona.name} agent: ${reply}`,
      documentType: "ai-chat-conversation",
      userId: phone,
      customerId: CUSTOMER_ID,
      metadata: {
        channel: "whatsapp",
        direction: "outbound",
        sourceWaba: persona.name,
      },
    });
  }
  ```
</CodeGroup>

### 4. The persona-aware agent

Each persona has its own system prompt. They all share the same memory pool.

<CodeGroup>
  ```python Python theme={null}
  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
  ```

  ```typescript TypeScript theme={null}
  const SALES_SYSTEM = `You are the WhatsApp sales agent for <Business>. ...`;
  const SUPPORT_SYSTEM = `You are the WhatsApp support agent for <Business>. ...`;
  const VIP_SYSTEM = `You are the WhatsApp VIP-line agent for <Business>. ...`;

  async function runPersona(
    persona: { name: string; model: string; system: string },
    phone: string,
    text: string,
  ): Promise<string> {
    const model = synap.wrap(openai(persona.model), {
      userId: phone,
      customerId: CUSTOMER_ID,
      conversationId: phone,
    });

    const { text: reply } = await generateText({
      model,
      system: persona.system,
      prompt: text,
      // tools: { ...persona-specific tools }
    });
    return reply;
  }
  ```
</CodeGroup>

### 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:

<CodeGroup>
  ```python Python theme={null}
  # 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,
  )
  ```

  ```typescript TypeScript theme={null}
  const docs = await synap.sdk.memories.search({
    userId: phone,
    customerId: CUSTOMER_ID,
    query: "prior support tickets",
    metadataFilter: { sourceWaba: "support" },
    maxResults: 10,
  });
  ```
</CodeGroup>

## Run & verify

```text Sales WABA (yesterday) theme={null}
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.
```

```text Support WABA (today) theme={null}
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](/cookbook/whatsapp-single-campaign), 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](/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.

## Related

* **Integrations:** [OpenAI Agents SDK](/integrations/openai-agents) · [Vercel AI SDK](/integrations/vercel-ai-sdk)
* **Concepts:** [Customer Context](/concepts/context-end-to-end#customer-context) · [Memory Scopes](/concepts/memory-scopes) · [Customers and Users](/concepts/memory-scopes#customers-and-users)
* **Patterns:** [Multi-Tenant SaaS](/patterns/multi-tenant-saas) · [Slack Bot](/patterns/slack-bot)
* **Other recipes:** [WhatsApp + Human Handoff](/cookbook/whatsapp-single-handoff) · [WhatsApp Campaign + Inbound](/cookbook/whatsapp-single-campaign)
