WhatsApp Messaging
Outbound WhatsApp for every org from their own business number — transactional notifications, the automations whatsapp channel, and bulk broadcasts — over the Meta Cloud API directly (Tech Provider; no Twilio/BSP). Inbound CTWA lead ingestion is documented separately in leads-crm/whatsapp-lead-ingestion.md.
Start with the implementation plan — it holds the full architecture, the Meta research digest (Tech Provider, Embedded Signup v4, Coexistence, pricing), and the phase log.
Resuming after Meta App Review? Read the end-to-end handoff — session state, decision log, file inventory, what’s verified vs not, and the exact go-live runbook.
What ships in v1
| Surface | Behavior |
|---|---|
| Connect (settings → integrations) | Embedded Signup v4, two flows: coexistence (default — gym keeps their WhatsApp Business app; we skip /register, resolve the number from the WABA, one-shot contacts sync) and classic (dedicated number, registered with a deterministic PIN). |
| Send infrastructure | WhatsAppSenderService.enqueueSend → whatsapp_messages ledger row → whatsapp-send queue → Cloud API. Statuses (sent/delivered/read/failed) arrive by webhook and update the row by wamid. |
| Templates | Per-org mirror of the WABA registry (whatsapp_templates), create/submit + sync + delete UI, review results via webhook. System templates (taikan_booking_confirmed, taikan_class_reminder × he/en/ru, UTILITY) auto-provisioned on connect. |
| Notifications | Booking confirmation + class-reminder cron send WhatsApp alongside email/push, gated by notification prefs (whatsapp channel, zero-migration catalog change). |
| Automations | Real WhatsAppChannelStrategy: free-form body inside the 24h service window, configured approved template outside it, else skipped (outside_service_window) → fallback channel. Lead consent fail-closed (ADR-0013); member prefs respected. |
| Broadcasts | Template-only bulk sends to member/lead audiences with per-recipient gates, scheduled or immediate (Shabbat warning in the wizard), delivery stats recomputed from the message ledger. |
| Conversations data layer | Every inbound message + coexistence app echo is recorded into conversations/conversation_messages — this drives the 24h window clock and makes the future inbox UI a frontend-only phase. |
Feature flag
whatsapp-messaging (PostHog, organization group, default off, fail-off). Gates every send path and isConfigured. Local dev: FEATURE_FLAGS=whatsapp-messaging:true.
Temporary: Green API transport
While Meta Business Verification is pending, an unofficial transport (green-api.com, QR/linked-device) can carry the same surfaces for opted-in orgs. It is ToS-violating and the connected number can be banned, so it is gated by its own per-org flag whatsapp-green-transport and carries explicit anti-ban controls. See green-api-stopgap.md — including the removal checklist for the day Meta approves.
Go-live gates (ops, not code)
Business Verification → Tech Provider onboarding → App Review (whatsapp_business_management + whatsapp_business_messaging, two screencasts) → ES config (WHATSAPP_EMBEDDED_SIGNUP_CONFIG_ID) with the coexistence customization → app-level webhook fields (messages, message_template_status_update, account_update, phone_number_quality_update, smb_message_echoes, smb_app_state_sync, history). Orgs add their own payment method to their WABA (Tech Provider model — Taikan never touches Meta billing). Everything is testable against Meta’s free test number meanwhile.
Code map
- API — outbound core:
apps/api/src/whatsapp/—whatsapp-graph.service.ts(Cloud API client +appsecret_proof),whatsapp-sender.service.ts(gate + ledger + queue),whatsapp-send.processor.ts(delivery, error-code mapping: 131049/130472 →droppedno-retry, 131047 → failed outside-window, 130429/131056/5xx → retry, 190 → connectionerror),whatsapp-window.service.ts(24h window fromconversations.last_inbound_at),whatsapp-events.service.ts(webhook fan-out: statuses, conversations, echoes,account_updatePARTNER_REMOVED → revoke, quality, template status),whatsapp-templates.service.ts+ controller,whatsapp-broadcasts.service.ts+ controller (+ 5-min scheduler cron),whatsapp-notifications.service.ts,whatsapp-system-templates.ts,whatsapp-template-params.ts. - API — inbound/connect (existing module):
apps/api/src/lead-ingestion/—whatsapp-webhook.controller.ts(singlePOST /webhooks/whatsapp, routes fields),whatsapp-connect.service.ts(ES finalize, both flows, health),whatsapp-ingestion.service.ts(CTWA leads, unchanged). - API — call sites:
bookings.service.ts(confirmation leg),notifications/notification-scheduler.service.ts(class-reminder leg),automations/channels/whatsapp.channel.ts. - DB:
libs/db/src/lib/schema/whatsapp.ts(whatsapp_templates,whatsapp_messages,whatsapp_broadcasts) + enums; migration0086. - Shared:
libs/shared/src/lib/schemas/whatsapp.schema.ts(DTOs),notification-prefs.ts(whatsappchannel),automation.schema.ts(whatsappTemplateId,SUPPORTED_CHANNELS). - Web:
components/settings/whatsapp-connect.tsx+fb-embedded-signup.ts(coexistence flow),components/settings/whatsapp-templates.tsx,components/broadcasts/, automations editor template picker.
QA plan
- Unit: send processor error mapping, channel strategy branching (window/template/prefs/consent), connect service both flows, webhook controller routing, events service handlers, broadcast audience gating. (
make test-unit-api) - Integration (web): connect card both flows + error codes, templates manager, broadcast wizard incl. preview count + Shabbat warning. (
make test-integration-web) - Live: Meta test number end-to-end via ngrok — send text + template, receive status webhooks, verify ledger transitions; coexistence connect on a real number post-App-Review (Erez’s org first).
Deliberate v1 boundaries
No inbox UI (data layer only) · no chat-history sync on coexistence (one-shot API, revisit at inbox) · no media/interactive sends · marketing broadcast pacing left to Meta (131049 drops counted as skipped) · Graph API version stays pinned at the module default (META_GRAPH_API_VERSION override available).