Spec: F76 — WhatsApp Business Connection
Status: shipped (runtime evidence pending)
1. Problem & CEO Job
Job: see clearly. In Malaysia, Indonesia, the Philippines, and Saudi Arabia, customer communication often lives in WhatsApp before it ever reaches a ticketing tool or CRM. If Lumi cannot ingest those messages, the CEO loses the most operationally important support signal: customers waiting without a reply.
This feature is not “WhatsApp inside Lumi.” It is narrower and more defensible: connect a WhatsApp Business webhook, turn inbound customer messages into SourceEvents, and route aged unanswered messages into the signal inbox. The product promise is earlier visibility, not a replacement chat client.
2. User Stories & Acceptance Criteria
-
As an owner or admin, I want to connect a WhatsApp Business source in
/settings/sources, so that Lumi can ingest customer-message signals from the Meta Business Platform.- Given an authenticated org admin, when they create a
SourceConnectionwithprovider_slug=whatsapp_business, then the connection is created under the current org with a generatedwebhook_secret. - Given a WhatsApp connection, when the admin supplies a
phone_number, thenconnect()returns success and the connection is treated as webhook-configured. - Given a duplicate create attempt for the same org and provider, when
POST /api/source-connections/is called, then the API rejects it.
- Given an authenticated org admin, when they create a
-
As a CEO, I want unanswered WhatsApp messages to surface as risks, so that response gaps show up before they become churn or escalation.
- Given a valid Meta-style webhook payload with one or more inbound messages, when it is posted to
/api/source-connections/webhook/<secret>/, then Lumi createsSourceEventrecords withevent_type=whatsapp_unanswered. - Given a WhatsApp message older than 2 hours at ingest time, when the webhook lands, then Lumi auto-routes it into the signal pipeline (
auto_route=True). - Given a malformed or invalidly signed webhook, when it is posted, then Lumi rejects it and does not create events.
- Given a valid Meta-style webhook payload with one or more inbound messages, when it is posted to
-
As a reviewer or operator, I want WhatsApp tenant isolation to be strict, so that one company’s customer messages never leak into another company’s view.
- Given two orgs with separate WhatsApp connections, when events are ingested for org A, then org B cannot list or view them via source APIs.
3. Scope Boundaries
- In: WhatsApp Business webhook-based source connection, connection creation through the generic source-connection flow, Meta signature verification, event normalization to
whatsapp_unanswered, market-aware catalog listing, signal routing for aged messages. - Out: sending WhatsApp messages from Lumi, two-way chat, message assignment in WhatsApp, WhatsApp consumer app integration, template-message sending, embedded signup UX, or a Telegram-style conversational bot. Those do not exist in the current code.
4. Data Model
No WhatsApp-specific model exists. The feature reuses the F33/F44 source-ingestion models:
SourceConnectionorganizationscopes ownership.provider_slug="whatsapp_business".config_jsoncurrently stores operational fields such asphone_number, optionalmarket, and optionalapp_secret.webhook_secretis the URL secret for ingress.
SourceEvent- stores normalized WhatsApp events.
external_event_idshape:whatsapp_msg_<timestamp>_<from_number>.event_type="whatsapp_unanswered".
No new state machine is introduced.
5. API Contract
| Method | Path | Auth/Role | Request | Response | Errors |
|---|---|---|---|---|---|
| POST | /api/source-connections/ | JWT, org owner/admin | provider_slug=whatsapp_business, category=support, config_json.phone_number | 201 SourceConnectionSerializer | 400 invalid market / duplicate provider / invalid config |
| GET | /api/source-connections/catalog/?market=<code> | JWT | optional market filter | connector list including whatsapp_business when supported | 200 |
| POST | /api/source-connections/webhook/<secret>/ | URL secret + Meta signature | Meta WhatsApp webhook payload | 202 {ok: true, event_id} | 401 unauthorized / signature mismatch, 400 invalid JSON, 413 payload too large, 500 ingest_failed |
Tenant scoping:
- CRUD queries on
SourceConnectionare scoped byrequest.organization_id. - Webhook ingress resolves exactly one
SourceConnectionbywebhook_secretand active/degraded status. - Resulting
SourceEvents are attached to that connection and therefore to that org only.
6. UI / UX
- Surface:
/settings/sourcesvia the existing source catalog. - Current UX is generic, not WhatsApp-specific: admins choose the connector, create the connection, then use the generated webhook URL/secret outside Lumi.
- No dedicated frontend WhatsApp wizard exists in the current repo.
- User-visible strings come from the
sourcesnamespace already used by the connector catalog.
7. Test Plan
| # | Case | Type | Where |
|---|---|---|---|
| 1 | Catalog includes whatsapp_business for supported markets | backend | backend/tests/test_connectors.py |
| 2 | Create WhatsApp source connection | backend | backend/tests/test_connectors.py::TestConnectorCRUD |
| 3 | WhatsApp webhook creates SourceEvent | backend | backend/tests/test_connectors.py::TestWhatsAppConnector::test_whatsapp_webhook_received_creates_event |
| 4 | WhatsApp Meta signature verification | backend | backend/tests/test_connectors.py::TestWhatsAppConnector::test_whatsapp_meta_signature_verification |
| 5 | Cross-tenant isolation for WhatsApp events | backend | backend/tests/test_connectors.py::TestRegionalConnectorTenantIsolation |
| 6 | Replay/signature rejection path | backend | backend/tests/test_connectors.py::TestConnectorErrorStates::test_whatsapp_webhook_replay_attack_rejected |
8. Current Behavior
WhatsApp Business is already implemented as a webhook-only source connector, not as a chat bot.
Confirmed current behavior from code and tests:
- Connector class:
backend/apps/connectors/whatsapp.py. - Registry loading:
backend/apps/connectors/registry.py. - Generic webhook ingress:
backend/apps/sources/views.py. - A valid webhook payload creates one
SourceEventper inbound message. - Message age is computed from the inbound WhatsApp timestamp. If age is
>= 7200seconds, the event is auto-routed as a signal. - Registered tests cover webhook ingestion, signature verification, tenant isolation, and replay rejection. This Contract Graph slice does not claim those tests executed in the current worktree, and repository-controlled payloads do not prove live Meta delivery or production ingress.
9. Error States
| # | Error | Response | Notes |
|---|---|---|---|
| ES1 | Unknown webhook_secret | 401 {"error": "unauthorized"} | No event created |
| ES2 | Invalid Meta signature | 401 {"error": "signature_mismatch"} | No event created |
| ES3 | Invalid JSON payload | 400 {"error": "bad_request"} | Connection marked degraded |
| ES4 | Payload too large | 413 {"error": "payload_too_large"} | Rejected before parsing |
| ES5 | Ingest exception during event creation | 500 {"error": "ingest_failed"} | Connection marked degraded |
| ES6 | Missing phone_number during connect() | connector returns {"success": false, "message": "Missing phone_number"} | No webhook setup happens inside Lumi |
10. Diagrams & Docs Touched
docs/specs/F44-api-integrations.mddocs/specs/F45-regional-connectors.mddocs/MVP_SCOPE.md
No new diagram is required because the feature uses the existing F33/F44 source-ingestion path rather than a new app or state machine.
11. Implementation Audit (2026-06-15, updated 2026-07-02)
| # | Claim | Status | Evidence |
|---|---|---|---|
| 1 | WhatsApp Business exists as a shipped connector | ✅ match | backend/apps/connectors/whatsapp.py |
| 2 | Connector is included in the runtime catalog | ✅ match | backend/apps/connectors/registry.py, backend/tests/test_connectors.py |
| 3 | Generic webhook ingress handles WhatsApp payload structure | ✅ match | backend/apps/sources/views.py::webhook_ingest |
| 4 | Meta X-Hub-Signature-256 verification is enforced | ✅ fixed 2026-07-02 | See §13. When app_secret is in config_json, the header is now REQUIRED — absent header → 401 missing_signature, invalid HMAC → 401 signature_mismatch. The previous if signature and ... condition silently skipped verification when no header was sent. |
| 5 | Webhook ingestion creates whatsapp_unanswered source events | ✅ match | test_whatsapp_webhook_received_creates_event (now sends valid HMAC) |
| 6 | Cross-tenant isolation is covered by tests | ✅ match | TestRegionalConnectorTenantIsolation |
| 7 | Dedicated WhatsApp setup UX / embedded signup exists | ⚠️ gap | Not present; current UX is generic source creation only |
| 8 | Feature has a dedicated spec in docs/specs/ | ✅ fixed by this file | Previous gap closed |
| 9 | Connector market definitions are internally consistent with the runtime catalog | ✅ match | backend/apps/connectors/whatsapp.py declares markets=["my","id","ph","sa"]; TestCatalogMarketFilter proves WhatsApp appears for my, id, ph, and sa, and no F76/F45 claim extends support beyond those four markets |
| 10 | Dead verify_signature method with hardcoded key removed | ✅ fixed 2026-07-02 | WhatsAppBusinessConnector.verify_signature() used b"whatsapp_webhook_secret" (hardcoded), was never called by any production path, and has been removed. HMAC verification now lives entirely in views.py::webhook_ingest using the app_secret from config_json. |
Signature enforcement behaviour (as of 2026-07-02):
Contract Graph boundary: local pytest/Vitest execution, live Meta delivery, and production webhook settlement remain explicit gaps even when proof records are collected.
- Connection has
app_secretinconfig_json→ REQUIRED:X-Hub-Signature-256header must be present and HMAC-SHA256 of the raw body must match. Missing header → 401missing_signature. Wrong HMAC → 401signature_mismatch. No event is created. - Connection has NO
app_secretinconfig_json(config gap) → verification is skipped. Operators must addapp_secretto enforce integrity. This is a documented exception, not a silent bypass.
12. Open Questions
- Should WhatsApp remain a pure source connector, or should Lumi eventually support reply workflows from inside the product? Current evidence supports the narrower connector only.
- Should Meta embedded signup be added to reduce setup friction, or is a manual webhook/admin flow sufficient for the wedge?
- Should aged unanswered-message severity be recalculated after ingest, instead of snapshotting age at webhook receipt time?
13. Bug / Change Log
2026-07-02 — Fix webhook-signature bypass: unsigned webhooks bypassed HMAC verification
Type: Security Fix
Summary:
- Root cause:
webhook_ingestinviews.pyverified theX-Hub-Signature-256header onlyif signature and conn.provider_slug == "whatsapp_business". When the header was absent (empty string), the condition was False and HMAC verification was skipped entirely, allowing unsigned requests to createSourceEventrecords — directly violating F76 §2 AC (“malformed or invalidly signed webhook → Lumi rejects it and does not create events”). - Dead code removed:
WhatsAppBusinessConnector.verify_signature()used a hardcoded key (b"whatsapp_webhook_secret"), was never called by any production code path, and masked the issue by implying signature verification was handled at the connector level. It has been removed. - Fix: When
app_secretis stored in the connection’sconfig_json, theX-Hub-Signature-256header is now required. Missing header → 401missing_signature. Invalid HMAC → 401signature_mismatch. Only connections withoutapp_secretskip verification (documented config gap). - Tests fixed/added:
test_whatsapp_webhook_received_creates_event— now sends a valid HMAC computed from the testapp_secret.test_whatsapp_webhook_unsigned_rejected— new test asserting missing-header requests return 401 and create no event.test_my_whatsapp_events_not_visible_to_id— updated to send a valid HMAC so a real event is created before verifying cross-tenant isolation.
- See postmortem:
docs/postmortems/2026-07-02-whatsapp-webhook-signature-bypass.md.
2026-07-01 — Close stale market-definition drift
Type: Documentation truth fix
Summary:
- Closed the F76 audit row that still treated WhatsApp market support as ambiguous.
- The shipped contract is now documented plainly: the runtime catalog supports WhatsApp Business in Malaysia, Indonesia, the Philippines, and Saudi Arabia (
my,id,ph,sa) — no more, no less.
2026-06-15 — Add dedicated WhatsApp connection spec
Type: Documentation Change
Summary:
- Added a dedicated spec for the already-shipped
whatsapp_businessconnector. - Clarified that the feature is a webhook-based source connection, not a Telegram-style bot.
- Documented the verified API contract, error states, and current implementation gaps.