Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesWhatsapp MessagingWhatsApp Messaging

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

SurfaceBehavior
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 infrastructureWhatsAppSenderService.enqueueSend → whatsapp_messages ledger row → whatsapp-send queue → Cloud API. Statuses (sent/delivered/read/failed) arrive by webhook and update the row by wamid.
TemplatesPer-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.
NotificationsBooking confirmation + class-reminder cron send WhatsApp alongside email/push, gated by notification prefs (whatsapp channel, zero-migration catalog change).
AutomationsReal 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.
BroadcastsTemplate-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 layerEvery 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 → dropped no-retry, 131047 → failed outside-window, 130429/131056/5xx → retry, 190 → connection error), whatsapp-window.service.ts (24h window from conversations.last_inbound_at), whatsapp-events.service.ts (webhook fan-out: statuses, conversations, echoes, account_update PARTNER_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 (single POST /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; migration 0086.
  • Shared: libs/shared/src/lib/schemas/whatsapp.schema.ts (DTOs), notification-prefs.ts (whatsapp channel), 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).