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_idon 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)
-
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).
-
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).
-
Webhook (Meta App Dashboard → WhatsApp → Configuration):
- Callback URL:
https://app.pmfriend.com/api/v1/internal/whatsapp/webhook - Verify token: the same
APP_WHATSAPP_VERIFY_TOKENvalue. - Subscribe to the messages webhook field.
- Callback URL:
-
Per-agency routing — attach the Meta
phone_number_idto the agency (admin session):PUT /api/v1/whatsapp/config
{ "phoneNumberId": "112233445566" }One agency per WhatsApp number (unique index).
GETthe 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.challengehandshake, 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:
- 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. - 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_idrouting — 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
- Email intake — the pipeline this rides
- Qatar Market Roadmap