Skip to main content
Status: In Development · Playground demo coming soon. The recipe below is complete and runnable today; only the hosted playground showcase is pending.
A single WABA number doing two jobs at once: pushing scheduled outbound campaigns (template messages) to opted-in recipients, and handling the inbound replies those campaigns generate, all with shared memory so the inbound agent knows what was sent, when, and to whom.

What you’ll build

A WhatsApp system that:
  • Schedules and sends outbound campaign templates to a segmented audience
  • Tracks delivery + read receipts + replies per recipient in memory
  • Routes inbound replies through a memory-aware agent that knows the campaign context
  • Respects opt-outs: STOP keywords flip a flag the campaigner reads before each send
Est. build time: 75-90 minutes.

When to use this recipe

Build this if:
  • You run outbound campaigns (announcements, re-engagement, reminders) on WhatsApp
  • You need the inbound response on those campaigns to feel coherent (“yes I want it” matches a specific template send)
  • Compliance (24-hour rule, opt-outs, template approval) matters
  • You want one number for both directions, not two separate systems

Architecture at a glance

Memory is the bridge. The inbound agent doesn’t need to be told “this person got campaign C-23 yesterday”; it pulls that from Synap.

Stack

Prerequisites

  • A Synap API key. See Authentication
  • A WABA number with approved template messages for your campaigns
  • A scheduler / queue (Celery, BullMQ, cron, your call)
  • Redis for opt-out flags, send dedupe, session windows
  • Python: Python 3.11+
  • TypeScript: Node 18+ and Python 3.11+ on the host
WhatsApp requires every outbound-to-cold-recipient message to use a pre-approved template. Free-form messages only work inside the 24-hour customer-initiated session window. Build this constraint into your scheduler, not the AI.

Install

Build it

1. Identity & scoping

  • customer_id = "<your-business>"
  • user_id = <recipient phone> (hashed if you prefer)
  • conversation_id = <one per phone>: rolling
conversation_id, user_id, and customer_id must be valid UUIDs. Since these key off the recipient phone, derive a deterministic UUID from it with uuid.uuid5(...) (Python) rather than passing the raw phone string.

2. The outbound side: campaign sender

Templates are pre-approved on Meta. Your scheduler picks the audience, fills in template variables, and sends.

3. Webhook: delivery / read receipts + inbound

The same webhook receives both delivery state updates and recipient replies.

4. The inbound reply agent

The agent reads the recent campaign-send and campaign-delivery memories before responding, so its reply matches the campaign that triggered the conversation.

5. Opt-out handling

Compliance-critical. Two layers: STOP keyword in the webhook (immediate), and a Redis flag the sender checks before every send.

Run & verify

Campaign C-may-relaunch fires for phone +1555...
The agent’s first reply references “the relaunch bundle” without being told which campaign because it pulled campaign-send: may_relaunch_v3 from memory.

Customize / extend

  • Add human handoff on top of campaigns → combine with Single-WABA Inbound + Human Handoff. Same memory, same scopes.
  • Multi-WABA for different campaign personas → see Multi-WABA Shared Memory.
  • Campaign performance memory → store reply rates and best-performing copy in a separate customer_id-scoped memory pool to inform next campaigns.
  • Replay opens / clicks from your CRM → use Patterns → Replay History to seed campaign engagement signal at launch.

Troubleshooting

Agent replies as if no campaign was sent
  • The agent’s synap_search isn’t fetching the recent campaign-send. Sharpen the system prompt to require a search first, or increase maxResults in the wrapper.
  • Verify record_delivery runs after send_campaign writes the campaign-send doc: race conditions can hide the outbound.
Sender keeps sending to opted-out recipients
  • The opt-out check belongs inside the worker task, not just at scheduling time. Audiences are computed in advance; opt-outs happen continuously.
Recipient gets duplicated outbound from the same campaign
  • Schedule with idempotency keys: (campaign_id, recipient_phone) → dedupe in Redis with a 30-day TTL.
Replies come in after the 24-hour window
  • Outside the window, free-form outbound is blocked by WhatsApp. Auto-fall-back to a “please-restart-conversation” template, or stay silent until the recipient initiates.