Send a workspace's customer one of your application's own WhatsApp messages - a quote, a booking, a document ready - without a WhatsApp Business account of your own.

### Templates, never free text

Everything you send is a **template Meta has approved**, named for your application. Outside a customer's 24-hour window WhatsApp permits nothing else, and a free-form conversation with a workspace's customers on the workspace's own number belongs to the CosmoOps Messaging application, not to this API.

Templates are written and submitted to Meta by the platform team, in Console, under your application. Connect names each one `app_<yourApplicationKey>_<slug>` and resolves it from your key - so a request names only the slug (`quote-issued`), and can never reach another application's message or one of Connect's own.

Approval takes Meta anywhere from minutes to days, and a template can be paused later for quality. So **ask before you offer**: `GET /whatsapp/templates/{slug}` answers `live`, and the variables a send must carry. A screen that offers WhatsApp only when `live` is true never shows a button that cannot work.

### Consent is the customer's, and a STOP is final

Connect messages a number only with an opt-in on file, keyed by the number across the whole platform. Your application collects opt-ins Connect never sees - a lead form, a call, a signed quote - so a send may carry `consent.collectedVia`, saying where the customer agreed. Connect records it against the number, as yours, **only when nothing is on file**.

A number that has withdrawn - replied STOP, or turned WhatsApp off in their profile - stays withdrawn. Attesting consent cannot re-grant it; the send is refused with `opted_out`, and the honest next step is a different channel.

### Treat it as a second channel

A send can be refused for reasons nobody did anything wrong to cause - a template under review, an opted-out number. Send the same thing by email as the channel of record, and WhatsApp beside it.


## Endpoints

### POST `/api/v1/application/whatsapp/messages`

Send one of your WhatsApp templates to a customer

**Scope** `whatsapp:send` &nbsp;·&nbsp; **Rate limit** 30 requests / min per key, 10 requests / hour per key+recipient

Sends a Meta-approved template **your application owns** to a customer of a subscribing workspace, through Connect's WhatsApp sender. Server-to-server only - there is no browser path.

#### Implementation

**1. Prerequisite** - an API key scoped to `whatsapp:send`, and a template under your application in Console that Meta has approved. Ask `GET /whatsapp/templates/{slug}` first: offer WhatsApp only when it answers `live: true`.

**2. Send** - name the template by its slug, never its Meta name, and pass one value per variable:
```bash
curl -X POST https://connect.cosmoops.com/api/v1/application/whatsapp/messages \
  -H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: quote-Q-0042-whatsapp" \
  -d '{"workspaceId":"WORKSPACE_ID","template":"quote-issued","to":"+919876543210","values":{"number":"Q-0042","link":"..."},"consent":{"collectedVia":"Quote request form"}}'
```

**3. Consent.** Connect sends only to a number with an opt-in on file. `consent.collectedVia` records the one your application collected - **only when nothing is on file**. A number that withdrew (replied STOP, or turned WhatsApp off) stays withdrawn: the send answers `opted_out` whatever you attest.

#### Errors
`validation_error` (400 - bad body, a non-E.164 `to`, or a template variable without a value), `forbidden` (403 - key missing `whatsapp:send`, or your application is not installed in that workspace), `consent_required` (403 - no opt-in on file and none attested), `opted_out` (403 - the number withdrew, for everything or for this kind of message), `not_found` (404 - your application has no such template in that language), `template_not_approved` (409 - not approved by Meta yet, or paused), `idempotency_key_reused`/`request_in_progress` (409), `rate_limited` (429 - per key, and per key and recipient), `delivery_failed` (502 - retry).

**Request body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `workspaceId` | `string` | yes |  |
| `template` | `string` | yes |  |
| `language` | `string` | no |  |
| `to` | `string` | yes |  |
| `values` | `object` | no |  |
| `consent` | `object` | no |  |

**Responses**

| Status | Meaning |
| --- | --- |
| `201` | Sent |
| `400` | validation_error - bad body, phone number, or a missing template value |
| `403` | forbidden / consent_required / opted_out |
| `404` | not_found - no such template for your application |
| `409` | template_not_approved / idempotency_key_reused / request_in_progress |
| `429` | rate_limited |
| `502` | delivery_failed - the WhatsApp provider refused the message |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `status` | `string` | yes |  |
| `messageId` | `string` | yes |  |

### GET `/api/v1/application/whatsapp/templates/{slug}`

Check whether a WhatsApp template is live

**Scope** `whatsapp:send` &nbsp;·&nbsp; **Rate limit** 30 requests / min per key

Whether one of your application's templates would send right now, and the values a send must carry.

`live` is the one field to branch on: Meta has approved the template **and** is currently serving it. A template being re-reviewed after an edit, or paused for quality, answers `live: false` even if it was approved before. `variables` lists the names from the approved content, in order - the keys `values` must carry on a send.

**Parameters**

| Name | In | Required | Notes |
| --- | --- | --- | --- |
| `slug` | path | yes | The template's slug within your application, e.g. `quote-issued`. |
| `language` | query | no | Meta language code. Defaults to `en`. |

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | The template's live state |
| `403` | forbidden - key unknown or missing the whatsapp:send scope |
| `404` | not_found - your application has no such template in that language |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `slug` | `string` | yes |  |
| `language` | `string` | yes |  |
| `name` | `string` | yes |  |
| `status` | `DRAFT` \| `PENDING` \| `APPROVED` \| `REJECTED` \| `PAUSED` \| `DISABLED` | yes |  |
| `live` | `boolean` | yes |  |
| `variables` | `array` | yes |  |