> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# WhatsApp

> Send messages via the WhatsApp Business API through Orbit

# WhatsApp

Reach 2+ billion WhatsApp users through Orbit's WhatsApp Business API integration. Send template messages, start interactive sessions, and handle rich media — all through a single API.

## Send a Template Message

WhatsApp requires pre-approved templates for business-initiated conversations.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/whatsapp \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552671",
    "type": "template",
    "template": {
      "name": "order_confirmation",
      "language": { "code": "en" },
      "components": [
        {
          "type": "body",
          "parameters": [
            { "type": "text", "text": "ORD-12345" }
          ]
        }
      ]
    }
  }'
```

## Send a Session Message

Once a user messages you, a 24-hour session window opens for free-form replies. Use the [window-status check](#check-the-24-hour-window) to confirm the window is still open before sending.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/whatsapp \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552671",
    "type": "text",
    "text": { "body": "Thanks for reaching out! How can we help?" }
  }'
```

## Check the 24-Hour Window

Before composing a free-form (session) message, call the window-status endpoint to confirm the recipient is still inside Meta's 24-hour re-engagement window. The send pipeline enforces the same check, but this pre-flight lets you gate the UI (force template-only when the window has closed) and avoid a wasted send attempt.

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/messages/whatsapp/window-status?to=+14155552671&from=+18005551234" \
  -H "X-API-Key: dv_live_sk_..."
```

* **`to`** (required) — the contact's E.164 number.
* **`from`** (optional) — the WABA number that will be the sender. When omitted, the check spans any of your WABA numbers; when supplied it is scoped to that sender pair (matching the send-time gate).

### Response

```json theme={null}
{
  "data": {
    "within_window": true,
    "last_inbound_at": "2026-03-08T09:12:00Z",
    "hours_since_last_inbound": 3,
    "reason": "within_window"
  },
  "meta": {
    "request_id": "req_xyz789",
    "timestamp": "2026-03-08T12:12:00Z"
  }
}
```

`reason` is one of:

| Reason               | Meaning                                                                                                            |
| -------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `within_window`      | An inbound was received in the last 24 hours — free-form replies are allowed.                                      |
| `window_expired`     | The last inbound is older than 24 hours — template-only.                                                           |
| `no_inbound_history` | No inbound has ever been received from this contact — `last_inbound_at` and `hours_since_last_inbound` are `null`. |

When the window is closed, `within_window` is `false` and you must fall back to an approved [template message](#send-a-template-message).

## Send a Broadcast

Fan a single approved template out to up to **256 recipients** in one operation (Meta's documented broadcast-list cap). Each recipient receives a normal 1:1 conversation — there is no shared thread, so per-recipient STOP / opt-out handling is preserved.

The endpoint is **persist-then-attempt**: every recipient is pre-inserted as a `pending` row in one transaction before the per-recipient send pipeline runs, so a pre-create rejection (template not approved, parameter-count mismatch, 24-hour-window block, fraud guard, quota, sender validation) flips that row to `failed` rather than throwing without persisting a record.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/whatsapp/broadcast \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "template_name": "order_confirmation",
    "template_language": "en",
    "from": "+18005551234",
    "template_params": { "1": "Acme Co" },
    "recipients": [
      { "to": "+14155552671", "template_params": { "1": "Alice" } },
      { "to": "+14155552672" }
    ]
  }'
```

* **`recipients`** (required) — 1–256 entries, each with a `to` (E.164) and optional per-row `template_params` / `metadata`. Per-row `template_params` override the batch-level shared `template_params`. Duplicate numbers in one request are rejected (`422`).
* **`template_name`** (required) — an approved WhatsApp template.
* **`template_language`**, **`from`**, **`template_params`**, **`scheduled_at`**, **`webhook_url`**, **`metadata`** (optional) — shared across the batch. With `scheduled_at` set, every recipient lands as `scheduled` for the worker queue.

### Response

Returns a per-recipient result array plus a summary. Inspect `summary.failed` and each `results[].status` — the HTTP status code signals whether to look at the body.

```json theme={null}
{
  "data": {
    "batch_id": "batch_abc123",
    "results": [
      { "message_id": "msg_1", "to": "+14155552671", "status": "sent" },
      {
        "message_id": "msg_2",
        "to": "+14155552672",
        "status": "failed",
        "error_code": "TEMPLATE_NOT_APPROVED",
        "error_message": "Template is not approved"
      }
    ],
    "summary": { "total": 2, "succeeded": 1, "failed": 1, "scheduled": 0 }
  },
  "meta": {
    "request_id": "req_xyz789",
    "timestamp": "2026-03-08T00:00:00Z"
  }
}
```

| Status | When                                                                                 |
| ------ | ------------------------------------------------------------------------------------ |
| `200`  | Every recipient succeeded.                                                           |
| `207`  | At least one recipient failed (partial **or** all-failed) — always inspect the body. |
| `402`  | Org balance can't cover the broadcast floor.                                         |
| `429`  | The recipient count would breach the WABA's daily `messaging_limit_tier` cap.        |
| `500`  | Bulk pre-insert failed — no recipients were processed.                               |

Broadcasts are rate-limited to 30 requests/minute per tenant. Larger or recurring sends belong in the campaigns module.

## Features

* **Template messages** — pre-approved message templates for outbound notifications
* **Session messaging** — free-form replies within the 24-hour conversation window, with a pre-flight window-status check
* **Broadcast lists** — fan one approved template out to up to 256 recipients with per-recipient results
* **Rich media** — images, videos, documents, audio, stickers, and location sharing
* **Interactive messages** — buttons, list pickers, and quick replies
* **Read receipts** — know when messages are delivered and read
* **Catalog & product messages** — share product listings directly in chat

## Media Support

Send images, video, documents, audio, and stickers over WhatsApp. Each media type has its **own** size limit, set and enforced by WhatsApp (Meta) at the Graph API — there is no single blanket ceiling. Orbit forwards your media to WhatsApp, so keep each file within the limit for its type or WhatsApp rejects the send.

| Type     | Max Size | Formats                                   |
| -------- | -------- | ----------------------------------------- |
| Image    | 5 MB     | JPEG, PNG                                 |
| Video    | 16 MB    | MP4, 3GPP                                 |
| Audio    | 16 MB    | AAC, MP3 (MPEG), MP4, AMR, OGG            |
| Document | 100 MB   | PDF, DOC, DOCX, PPT, PPTX, XLS, XLSX, TXT |
| Sticker  | 100 KB   | WebP                                      |

Limits are per type, not shared: a 90 MB PDF is accepted as a document but rejected if the same file is sent as an image, and a sticker must stay under 100 KB. Check each file against the limit for its media type before sending.

## Platform WABA credentials (operators)

Most tenants connect their own WhatsApp Business Account through the dashboard Embedded Signup flow, and Orbit uses that tenant's stored credentials at send time. The variables below configure Orbit's **own** platform-owned WABA — the shared fallback / send path used for the unverified test account, platform-initiated template management, and WhatsApp Calling registration.

Set all three in Secret Manager (or `.env` for local dev). They are optional at boot: when `DEVOTEL_WHATSAPP_ACCESS_TOKEN` is set, `messaging-bootstrap.ts` registers one platform-wide WhatsApp provider; when it is unset, only per-tenant connections resolve at send time.

| Variable                               | Required                    | Purpose                                                                                                                              |
| -------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `DEVOTEL_WHATSAPP_ACCESS_TOKEN`        | To enable the platform path | Graph API access token for the platform-owned WABA. Presence of this token is what registers the platform WhatsApp provider at boot. |
| `DEVOTEL_WHATSAPP_PHONE_NUMBER_ID`     | With the access token       | Phone-number id of the platform WABA's sender. Used as the `from` for platform / test-account sends.                                 |
| `DEVOTEL_WHATSAPP_BUSINESS_ACCOUNT_ID` | With the access token       | The platform WABA id. Used to scope template management and to match inbound webhooks routed to the platform account.                |

Inbound webhook HMAC for WhatsApp still uses the shared `DEVOTEL_META_APP_SECRET` / `DEVOTEL_META_VERIFY_TOKEN` regardless of whether the platform credentials are set.

### Dashboard Embedded Signup (build-time client config)

The dashboard's WhatsApp Embedded Signup card and the Meta Ads channel card load Meta's JS SDK in the browser, so they need a Meta App ID and an Embedded Signup configuration ID available on the client. Two `NEXT_PUBLIC_*` variables carry those values. Because they are `NEXT_PUBLIC_*`, they are baked into the dashboard when it is built — changing them requires rebuilding the dashboard, not just updating a server environment variable.

Both are optional. Leave them unset for standard production deployments; the defaults connect tenants to Orbit's production Meta app.

| Variable                     | Required | Purpose                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| ---------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NEXT_PUBLIC_META_APP_ID`    | Optional | Meta App ID used by every Meta channel card (WhatsApp Embedded Signup, Instagram, Messenger, Meta Ads) to initialise the Facebook JS SDK. When unset, the SDK loader and the Meta Ads card fall back to Orbit's built-in production Meta app. In the WhatsApp Embedded Signup card the effective app ID is normally taken from the server-side WhatsApp status (`DEVOTEL_META_APP_ID`); this build-time value only backs the in-tab OAuth fallback before that status resolves.    |
| `NEXT_PUBLIC_META_CONFIG_ID` | Optional | Embedded Signup configuration ID for the WhatsApp Embedded Signup flow. Unlike the App ID, it has **no** built-in production fallback: the effective value is normally taken from the server-side WhatsApp status, and this build-time value only backs the brief window before that status resolves. When both the server value and this variable are empty, the Connect action surfaces a misconfiguration message instead of opening Meta's signup dialog with an empty config. |

Override both when you build the dashboard against a non-production Meta app — a **staging / preview** environment pointed at a test Meta app and Embedded Signup config, or a **white-label deployment** that runs its own Meta app rather than Orbit's. Provide both as build-time variables when you build the dashboard, and set the matching server-side `DEVOTEL_META_APP_ID` so the runtime and build-time values agree.

## Template Management

Manage templates through the Orbit dashboard under **Channels > WhatsApp > Templates**, or via the API:

```bash theme={null}
curl https://api.orbit.devotel.io/api/v1/whatsapp/templates \
  -H "X-API-Key: dv_live_sk_..."
```

Templates go through Meta's approval process (typically 1–24 hours).
