Skip to main content

WhatsApp Intake

Qatar tenants report by WhatsApp, not email. WhatsApp messages ride the existing email-intake pipeline end to end: same inbox, same AI classification, same DRAFT / UNRESOLVED / NOT_MAINTENANCE review flow, same promote-to-request action. Only two things differ, and both are contained in the WhatsApp layer:

  • Routing — the Meta phone_number_id on the incoming webhook (the agency's WhatsApp Business number) maps to an agency, the way an email recipient local-part does.
  • Tenant matching — by the sender's phone digits against tenant mobiles, suffix-tolerant (WhatsApp sends 97455330301; the tenant record might say +974 5533 0301 — both normalise to the same match; at least 8 shared trailing digits required).

Storage conventions inside inbound_emails: sender_email carries the E.164 phone (+97455330301), recipient_local_part is wa:<phone_number_id>, raw_s3_key is whatsapp/<meta message id> (no S3 object behind it — the full text lives in body_text).

Setup (per deployment)

  1. Meta side — create a Meta app with the WhatsApp product, get a WhatsApp Business phone number, and find the App Secret (App Dashboard → Settings → Basic).

  2. Backend env (App Runner):

    • APP_WHATSAPP_VERIFY_TOKEN — any random string you choose.
    • APP_WHATSAPP_APP_SECRET — the Meta app secret. Both empty = the feature is dormant and the webhook refuses everything (fail-closed).
  3. Webhook (Meta App Dashboard → WhatsApp → Configuration):

    • Callback URL: https://app.pmfriend.com/api/v1/internal/whatsapp/webhook
    • Verify token: the same APP_WHATSAPP_VERIFY_TOKEN value.
    • Subscribe to the messages webhook field.
  4. Per-agency routing — attach the Meta phone_number_id to the agency (admin session):

    PUT /api/v1/whatsapp/config
    { "phoneNumberId": "112233445566" }

    One agency per WhatsApp number (unique index). GET the same endpoint to read it back; send blank to detach.

Security

  • POST authentication: X-Hub-Signature-256 — HMAC-SHA256 of the raw body with the app secret, constant-time compared. No secret configured → 403 for everything.
  • GET verification: Meta's hub.challenge handshake, token-matched.
  • Rate-limited per IP (whatsapp-webhook-hourly, 600/h) so forged-post floods can't burn HMAC cycles.
  • The endpoint always answers 200 to authenticated notifications even if ingest fails (Meta disables webhooks that keep failing; we prefer one lost message + an ERROR log).

Multi-agency model (how this scales)

Routing is by the receiving number, never the sender: a tenant texts the number their agency gave them, the webhook payload says which business number received it (phone_number_id), and that maps to exactly one agency (unique index). The sender is then matched inside that agency, RLS-scoped — one agency's tenants can never land in another's inbox. Same shape as email intake: one shared platform entry point, per-agency routing key.

Two growth stages, same code path:

  1. First agencies (now): our single Meta WhatsApp Business Account holds multiple phone numbers — one per customer agency. Onboarding a customer = add a number to our WABA + one PUT /api/v1/whatsapp/config. No code changes per customer.
  2. Many agencies (Meta "tech provider" model): agencies connect their own WhatsApp Business Account to our Meta app via Embedded Signup, keeping ownership of their number and their Meta billing. Still one webhook, still phone_number_id routing — the ingestion code shipped today does not change; only the onboarding UX does (a "Connect WhatsApp" button in Settings instead of an admin API call). Stage 3+ alongside media and replies.

The sales-relevant consequence: an agency keeps the WhatsApp number its tenants already know. If reception already runs a WhatsApp number, that exact number becomes the AI intake — nothing changes for tenants, and the receptionist stops being the triage bot.

v1 limitations (roadmap)

  • Text messages only. Media (tenant photos!) requires a Cloud API access token to download — Stage 3, and the natural next step since photo-triage is the product's signature move.
  • No outbound. No auto-confirmation reply to the tenant; also needs the access token.
  • No threading — each message is a separate inbox row.

See also