# Mobile WhatsApp adapter — 2026-09-09

## Scope

Companion Flutter app reads existing petaV3 one-to-one WhatsApp conversations and sends text replies. No new WhatsApp account, provider, tables or migrations. Recording auto-matching is out of scope.

Reuses `LeadVisibility`, WhatsApp models, `MessagePresenter`, `RecipientResolver`, `WhatsappRepository`, and the existing `SendWhatsAppMessage` provider delivery path. Permissions are explicitly resolved against the existing **web** catalogue even for JWT/api callers. No permissions are granted by this change.

- Active staff with `view-whatsapp` or `manage-whatsapp` (or super admin) only; existing own/team/group/all lead visibility applies to every read and send.
- Production Cloud API/Bridge individual conversations only; sandbox and groups are excluded.
- Cloud free-text requires the customer's last inbound message to be less than 24 hours old, evaluated on the server. Null/future timestamps and the exact 24-hour boundary are denied. Bridge keeps petaV3's existing web behavior; this is not a statement that Bridge is an official Meta integration.
- The queue wrapper rechecks permission, current lead ownership, channel/contact status and the window immediately before invoking existing delivery.
- UUID replay returns the same message. Reuse with another body, actor or conversation is rejected. Unique queued jobs reduce duplicates; external delivery across a hard crash is not exactly-once guaranteed.
- Allowlisted responses omit provider payloads, private metadata and edit history. Revoked content is redacted. Existing signed media URLs are reused.

Policy reference: [WhatsApp Business Messaging Policy](https://whatsappbusiness.com/policy/) — free-form replies within 24 hours of the last customer message; approved templates otherwise. Mobile template sending is not included.

## API

All requests use the existing JWT and `/agent-api` base. Routes also inherit the existing legacy `/api` registration.

- `GET /agent-api/whatsapp/conversations?limit=30&search=...` — newest-first; limit 1–200, search name/phone; `{data: [...], meta: {has_more, server_time}}`.
- `GET /agent-api/whatsapp/conversations/{uuid}?before=<message-id>` — 50 messages, chronological, conversation reply eligibility, `older_cursor`, server time.
- `POST /agent-api/whatsapp/conversations/{uuid}/messages` — `{request_uuid: UUID, body: string}` (trimmed, 1–4096 chars); returns `{data: message}`. 200 means persisted/queued, not delivered.
- 401 unauthenticated/non-staff, 403 missing inbox permission, 404 inaccessible thread, 409 reply unavailable or UUID conflict, 422 invalid input.
- Separate limiter keys: inbox 120/min/user, sends 30/min/user; polling must not consume the diagnostics limiter.

## Mobile behavior / limits

Shared Android/iOS WhatsApp tab, search, message history/older loading, reply-window countdown using server time plus monotonic elapsed time, queued/delivery status, HTTPS attachments via external viewer. Inbox polls every 15 seconds while visible; open thread every 5 seconds. Background polling pauses. No new closed-app message push, read receipts, group messaging, template/media sending or offline persisted drafts. The list is capped at the latest 200 matches with an explicit search hint. Messages/drafts are cleared on account teardown or access rejection.

## Verification and release gate

Automated tests cover JWT lead scoping, exact expiry, blocked/disconnected state, cross-thread UUID reuse, retries, revoked text, older pagination, queue-time expiry/reassignment/revocation and mocked provider delivery. Existing lead/inbox visibility and diagnostics regressions are included. PHPUnit currently reports a pre-existing XML configuration deprecation.

Local result: 25 tests / 157 assertions passed (11 new mobile API tests plus existing regression suites); targeted Pint passed. Companion Flutter: 381 tests, clean analyzer and passing iOS scheme checks; Android 1.0.53 (53) built and verified.

Deployment has NOT been performed by this task. Before distributing the companion app:

1. Deploy this backend commit through normal review/release, refresh route caches and restart the existing queue workers/Horizon so the new wrapper job is loaded. Use the existing WhatsApp provider credentials and queues; no new secrets/configuration are needed.
2. Verify the staff account has existing inbox and appropriate lead visibility permissions and its WhatsApp contact is linked to an accessible lead.
3. With a specifically approved test recipient, receive a message, read it on the phone, send one reply, verify delivery on the recipient and the same message on the web. Verify expiry, reassignment and logout on a real device.
4. Only then publish the updated APK/TestFlight build. iOS signing/device acceptance remains a separate release gate.

No real customer message was sent during automated verification.
