# Official Facebook / Meta WhatsApp Cloud API

This guide describes the integration implemented in this repository. The official channel uses Meta's WhatsApp Business Platform Cloud API over HTTPS. QR-linked devices use whatsmeow and have a separate lifecycle. Connecting a Cloud number does not create a QR session. Facebook Page Messenger is a separate product and is not implemented here.

## 1. Prepare the Meta account

Create a Meta developer app with the WhatsApp product, a business portfolio, a WhatsApp Business Account (WABA), and a business phone number. Use the test number and an allowed test recipient for the initial test. Record the **phone number ID**, **WABA ID**, and **app secret**; these identify different resources.

Generate a system-user access token with the assigned WhatsApp assets and `whatsapp_business_messaging` and `whatsapp_business_management` permissions. The app dashboard's temporary user token is suitable for testing, but expires. Business portfolio operations can additionally require `business_management`. Complete the phone registration and business requirements shown in your Meta dashboard before production use. See [Meta's maintained Cloud API collection](https://www.postman.com/meta/whatsapp-business-platform/documentation/wlk6lh4/whatsapp-cloud-api).

## 2. Configure this backend

Set these variables in `backend/.env` or the deployment environment:

```dotenv
META_APP_SECRET=your_meta_app_secret
META_WEBHOOK_VERIFY_TOKEN=a_long_random_value_you_choose
META_GRAPH_VERSION=v21.0
PUBLIC_BASE_URL=https://api.example.com
```

`v21.0` is the repository's existing default, not a claim that it is the newest Graph API version. Set `META_GRAPH_VERSION` explicitly to a version supported by your Meta app and test upgrades before changing production. The same version is used by sending and template operations.

The three credentials have distinct jobs:

| Credential | Where it goes | Purpose |
| --- | --- | --- |
| WhatsAPI JWT | `Authorization: Bearer YOUR_JWT` on `/api` | Signed-in dashboard operations |
| WhatsAPI API key | `Authorization: Bearer YOUR_API_KEY` on `/api/v1` | Server-to-server integration |
| Meta system-user token | `access_token` when connecting a Cloud account | Backend calls to Graph API |
| Meta app secret | `META_APP_SECRET`, on the backend only | Incoming webhook signature verification |
| Webhook verify token | Backend environment and Meta webhook configuration | GET subscription handshake |

The Meta token is stored per Cloud account in SurrealDB and omitted from JSON responses. Native CBOR tags preserve it during database encoding and decoding. This implementation does **not** add application-level encryption at rest; database access must remain restricted. Never put the Meta token in frontend runtime config or public API examples.

## 3. Connect a number

Open **Dashboard → WhatsApp → Cloud API**, choose **Connect number**, and enter a descriptive name, phone number ID, WABA ID, and Meta access token. Verify the credentials, then save the account.

The equivalent integration request is:

```bash
curl 'https://api.example.com/api/v1/whatsapp/cloud/accounts' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Customer support",
    "phone_number_id": "YOUR_PHONE_NUMBER_ID",
    "waba_id": "YOUR_WABA_ID",
    "access_token": "YOUR_META_SYSTEM_USER_TOKEN"
  }'
```

Account creation verifies the phone number through Graph API before saving. Use the returned `data.id.ID` value as `{id}` in subsequent paths. Do not pass the complete JSON record ID object or prefix it with `whatsapp_cloud_account:`. `PUT /accounts/{id}` merges supplied account settings and preserves stored credentials when the token is omitted. Changing credentials triggers verification.

## 4. Configure webhooks

In the Meta app's WhatsApp webhook settings, use:

- Callback URL: `https://api.example.com/api/whatsapp/cloud/webhook`
- Verify token: the exact value of `META_WEBHOOK_VERIFY_TOKEN`
- Subscribe to the `messages` field.

Meta calls the GET endpoint with `hub.mode=subscribe`, `hub.verify_token`, and `hub.challenge`. The handler validates the token and returns the challenge as plain text. This handshake behavior is also described in [Meta's webhook receiver documentation](https://whatsapp.github.io/WhatsApp-Nodejs-SDK/receivingMessages/).

Subscribe the app to the WABA as well; saving a callback URL alone does not subscribe the business account. The Graph operation is `POST /{WABA_ID}/subscribed_apps` with the Meta token. [Meta's subscription example](https://www.postman.com/meta/whatsapp-business-platform/documentation/wlk6lh4/whatsapp-cloud-api?entity=request-13382743-20dd9252-0cbd-47e2-a9a2-477d38c8e240) covers this separate step.

POST notifications use `X-Hub-Signature-256`. The handler computes HMAC-SHA256 over the **original request bytes** with the app secret and compares the `sha256=<hex>` value in constant time. Missing app-secret configuration disables reception with HTTP 503; a missing or incorrect signature returns HTTP 401. JWTs and API keys are not required on this callback.

For each event, the implementation looks up the active Cloud account by `metadata.phone_number_id`, associates incoming messages with its owner, stores inbox messages, and dispatches workflow events. Delivery statuses update stored messages. If an account has `webhook_url`, the handler also forwards its normalized event to that URL.

Current limits: downstream work is not a durable webhook queue, application-level event deduplication is incomplete, and forwarding does not have a durable retry/dead-letter mechanism. Incoming non-text media is not automatically downloaded. Plan those additions before relying on exactly-once processing or high-volume webhook forwarding.

## 5. Send a message

For the initial test, use the approved `hello_world` template where it is available for your test number:

```bash
curl 'https://api.example.com/api/v1/whatsapp/cloud/accounts/ACCOUNT_ID/send' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "to": "254700000000",
    "type": "template",
    "template_name": "hello_world",
    "template_language": "en_US"
  }'
```

The backend translates this into Graph API's `messaging_product: "whatsapp"` payload and sends it to `/{PHONE_NUMBER_ID}/messages`. The recipient and token above are placeholders. Meta returns a provider message ID when it accepts the request; acceptance is not a delivery receipt.

For an eligible text conversation:

```json
{
  "to": "254700000000",
  "type": "text",
  "message": "Hello! How can we help?",
  "preview_url": false
}
```

The payload builder also supports media, location, contact, reaction, interactive replies, and templates. Consult the generated API reference for the precise request fields. Template approval, recipient eligibility, messaging windows, and delivery rules are enforced by Meta; this backend does not independently track or authorize a customer-service window.

## 6. Endpoint map

All paths below start with `/api/v1/whatsapp/cloud`. The same operations are available under `/api/whatsapp/cloud` with a dashboard JWT.

| Method | Path | Behavior |
| --- | --- | --- |
| GET / POST | `/accounts` | List or connect accounts |
| POST | `/accounts/verify` | Check credentials without saving |
| GET / PUT / DELETE | `/accounts/{id}` | Read, update, or remove an owned account |
| POST | `/accounts/{id}/send` | Send one message |
| POST | `/accounts/{id}/send-bulk` | Send multiple messages now, or schedule them for later |
| POST | `/accounts/{id}/mark-read` | Mark a provider message read |
| GET / POST / DELETE | `/accounts/{id}/templates` | List, create, or delete templates |

For a scheduled Cloud send, post to `/accounts/{id}/send-bulk` with `recipients` (one number is allowed), a message payload such as `{"type":"text","message":"Your order is ready"}`, and `scheduled_at` as a future RFC3339 time such as `2026-09-18T09:00:00Z`. Omit `scheduled_at` to send bulk messages immediately. The dashboard's Schedule Message and Schedule Bulk pages use this same endpoint when Cloud API is selected.

The webhook uses `/api/whatsapp/cloud/webhook`, not `/api/v1`. Template operations require a configured WABA ID. Scheduled sends run through Asynq and Valkey; run the workers as well as the API process. A queued response means accepted for processing, not delivered.

## 7. Troubleshooting

| Symptom | Check |
| --- | --- |
| Credential verification fails | Token expiry, assigned business assets, permissions, and phone number ID |
| Callback cannot be verified | Public HTTPS reachability and exact verify-token match |
| Webhook returns 401 | App secret belongs to the sending Meta app; proxy preserves body bytes and signature header |
| Webhook returns 503 | `META_APP_SECRET` is missing |
| No inbound events | Subscribe to `messages` and subscribe the app to the correct WABA; match saved phone number ID |
| Account returns 404 | Use the ID suffix and the owning account's JWT/API key |
| Send returns 429 | Application quota/rate limit; inspect the response message |
| Send returns 502 | Graph API rejected the request; inspect its error code and message |
| Template send fails | Exact approved name, language, component parameters, and recipient eligibility |
| Bulk job never runs | Asynq workers and Valkey connectivity |

## Code and verification

- Routes: `backend/routes/routes.go`
- Account and webhook handlers: `backend/internal/handler/whatsapp_cloud.go`
- Graph client and payloads: `backend/internal/services/whatsapp_cloud.go`
- Bulk worker: `backend/internal/tasks/whatsapp_cloud.go`
- Persistence model: `backend/internal/models/whatsapp_cloud.go`
- Frontend: `frontend/app/pages/dashboard/cloud/`
- Reference generation: `cd backend && make generate-swagger`

The automated tests use a disposable local database and mock webhook payloads. They do not send real Meta messages, register business assets, or prove production delivery. Complete one outbound template test, inbound reply, and delivery-status check with your own Meta test account before rollout.
