shipped (runtime evidence pending)

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 SourceConnection with provider_slug=whatsapp_business, then the connection is created under the current org with a generated webhook_secret.
    • Given a WhatsApp connection, when the admin supplies a phone_number, then connect() 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.
  • 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 creates SourceEvent records with event_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.
  • 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:

  • SourceConnection
    • organization scopes ownership.
    • provider_slug="whatsapp_business".
    • config_json currently stores operational fields such as phone_number, optional market, and optional app_secret.
    • webhook_secret is the URL secret for ingress.
  • SourceEvent
    • stores normalized WhatsApp events.
    • external_event_id shape: whatsapp_msg_<timestamp>_<from_number>.
    • event_type="whatsapp_unanswered".

No new state machine is introduced.

5. API Contract

MethodPathAuth/RoleRequestResponseErrors
POST/api/source-connections/JWT, org owner/adminprovider_slug=whatsapp_business, category=support, config_json.phone_number201 SourceConnectionSerializer400 invalid market / duplicate provider / invalid config
GET/api/source-connections/catalog/?market=<code>JWToptional market filterconnector list including whatsapp_business when supported200
POST/api/source-connections/webhook/<secret>/URL secret + Meta signatureMeta WhatsApp webhook payload202 {ok: true, event_id}401 unauthorized / signature mismatch, 400 invalid JSON, 413 payload too large, 500 ingest_failed

Tenant scoping:

  • CRUD queries on SourceConnection are scoped by request.organization_id.
  • Webhook ingress resolves exactly one SourceConnection by webhook_secret and active/degraded status.
  • Resulting SourceEvents are attached to that connection and therefore to that org only.

6. UI / UX

  • Surface: /settings/sources via 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 sources namespace already used by the connector catalog.

7. Test Plan

#CaseTypeWhere
1Catalog includes whatsapp_business for supported marketsbackendbackend/tests/test_connectors.py
2Create WhatsApp source connectionbackendbackend/tests/test_connectors.py::TestConnectorCRUD
3WhatsApp webhook creates SourceEventbackendbackend/tests/test_connectors.py::TestWhatsAppConnector::test_whatsapp_webhook_received_creates_event
4WhatsApp Meta signature verificationbackendbackend/tests/test_connectors.py::TestWhatsAppConnector::test_whatsapp_meta_signature_verification
5Cross-tenant isolation for WhatsApp eventsbackendbackend/tests/test_connectors.py::TestRegionalConnectorTenantIsolation
6Replay/signature rejection pathbackendbackend/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 SourceEvent per inbound message.
  • Message age is computed from the inbound WhatsApp timestamp. If age is >= 7200 seconds, 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

#ErrorResponseNotes
ES1Unknown webhook_secret401 {"error": "unauthorized"}No event created
ES2Invalid Meta signature401 {"error": "signature_mismatch"}No event created
ES3Invalid JSON payload400 {"error": "bad_request"}Connection marked degraded
ES4Payload too large413 {"error": "payload_too_large"}Rejected before parsing
ES5Ingest exception during event creation500 {"error": "ingest_failed"}Connection marked degraded
ES6Missing 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.md
  • docs/specs/F45-regional-connectors.md
  • docs/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)

#ClaimStatusEvidence
1WhatsApp Business exists as a shipped connector✅ matchbackend/apps/connectors/whatsapp.py
2Connector is included in the runtime catalog✅ matchbackend/apps/connectors/registry.py, backend/tests/test_connectors.py
3Generic webhook ingress handles WhatsApp payload structure✅ matchbackend/apps/sources/views.py::webhook_ingest
4Meta X-Hub-Signature-256 verification is enforced✅ fixed 2026-07-02See §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.
5Webhook ingestion creates whatsapp_unanswered source events✅ matchtest_whatsapp_webhook_received_creates_event (now sends valid HMAC)
6Cross-tenant isolation is covered by tests✅ matchTestRegionalConnectorTenantIsolation
7Dedicated WhatsApp setup UX / embedded signup exists⚠️ gapNot present; current UX is generic source creation only
8Feature has a dedicated spec in docs/specs/✅ fixed by this filePrevious gap closed
9Connector market definitions are internally consistent with the runtime catalog✅ matchbackend/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
10Dead verify_signature method with hardcoded key removed✅ fixed 2026-07-02WhatsAppBusinessConnector.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_secret in config_json → REQUIRED: X-Hub-Signature-256 header must be present and HMAC-SHA256 of the raw body must match. Missing header → 401 missing_signature. Wrong HMAC → 401 signature_mismatch. No event is created.
  • Connection has NO app_secret in config_json (config gap) → verification is skipped. Operators must add app_secret to enforce integrity. This is a documented exception, not a silent bypass.

12. Open Questions

  1. 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.
  2. Should Meta embedded signup be added to reduce setup friction, or is a manual webhook/admin flow sufficient for the wedge?
  3. 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_ingest in views.py verified the X-Hub-Signature-256 header only if 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 create SourceEvent records — 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_secret is stored in the connection’s config_json, the X-Hub-Signature-256 header is now required. Missing header → 401 missing_signature. Invalid HMAC → 401 signature_mismatch. Only connections without app_secret skip verification (documented config gap).
  • Tests fixed/added:
    • test_whatsapp_webhook_received_creates_event — now sends a valid HMAC computed from the test app_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_business connector.
  • 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.