# Sending WhatsApp messages

> Send template messages safely, follow their statuses, and understand costs, limits, and retention.

Send a WhatsApp message with one request to [`POST /v1/messaging/messages`](/docs/api/messaging/send), the endpoint every messaging channel shares, with `channel` set to `whatsapp`. You need a connected [sender](/docs/whatsapp/senders), an approved [template](/docs/whatsapp/templates), and an API key with the `whatsapp.send` permission.

## Compose a message

| Field | Type | Description |
| --- | --- | --- |
| `channel` | string · required | `whatsapp`. |
| `from` | string · required | The phone number of your connected WhatsApp number, in international format, such as `+213555000111`. |
| `to` | string · required | The recipient's phone number in international format, with `+` and the country code, such as `+213555123456`. |
| `template.id` | string · required | The Template ID of an approved template of that sender's WhatsApp Business Account, such as `wat_…`. |
| `template.variables` | object · optional | Values for the template's `header` and `body` variables. See [Variables](/docs/whatsapp/templates#variables). |

```json
{
  "channel": "whatsapp",
  "from": "+213555000111",
  "to": "+213555123456",
  "template": {
    "id": "wat_example",
    "variables": {
      "body": {
        "1": "Amina",
        "2": "A-1234"
      }
    }
  }
}
```

The template decides the language, the category, and everything the recipient sees except the variable values.

## Recipients

Each request sends one message to one recipient. To message several customers, send one request per recipient, each with its own idempotency key.

Write numbers in E.164 format: `+`, the country code, then the number without spaces, dashes, or a leading `0`. `0555 12 34 56` in Algeria becomes `+213555123456`. The same format applies to `from`.

The recipient must have a WhatsApp account and must have [agreed to receive your messages](/docs/whatsapp/best-practices#get-and-respect-opt-in). WhatsApp doesn't let businesses check in advance whether a number uses WhatsApp: a message to a number without WhatsApp is accepted, then becomes `failed`.

## Idempotency keys

Every API send requires an `Idempotency-Key` header. It guarantees that one operation sends at most one message, even when your request times out and you retry it.

- Use a key that identifies the operation in your system, such as `order-A-1234-shipped`. Up to 200 characters.
- Retrying with the same key and the same request returns the original message as it is now, without sending again or charging again. Its status may have moved on, for example to `delivered`.
- Reusing a key for a different message returns `409` with the code `whatsapp_idempotency_key_reused`.
- Keys are scoped to your project.

Store the key with the operation that triggers the message, so a retry after a crash or deploy uses the same key. See [Retry safely](/docs/learn/retry-safely).

## Message statuses

A successful send returns `200 OK` with the message, usually with the status `accepted`. WhatsApp then reports how far the message got.

| Status | Meaning | Final |
| --- | --- | --- |
| `sending` | The message is being handed to WhatsApp. | No |
| `accepted` | WhatsApp accepted the message. It is not delivered yet. | No |
| `sent` | WhatsApp sent the message to the recipient's phone. | No |
| `delivered` | The message reached the recipient's phone. | No |
| `read` | The recipient opened the message. | Yes |
| `failed` | WhatsApp refused or could not deliver the message. `failure_code` and `failure_reason` say why. | Yes |
| `uncertain` | WhatsApp didn't confirm whether it accepted the message. See below. | No |

A status only moves forward: `accepted`, `sent`, `delivered`, `read`. Some steps can be skipped; a message can go straight from `sent` to `read`. `read` is reported only for recipients who share read receipts, so many messages stay `delivered`. A message that failed after it was delivered keeps its `delivered` status.

Each status has its own timestamp on the message: `accepted_at`, `sent_at`, `delivered_at`, `read_at`, and `failed_at`.

### When a message is `uncertain`

If WhatsApp doesn't answer in time, the message may or may not have been sent. Nexiom Connect never resends it on its own, so the recipient never gets it twice. The response is still `200 OK`, with the status `uncertain`.

When WhatsApp later reports a status for the message, it moves on from `uncertain`, and the matching webhook fires. If it stays `uncertain`, check with the recipient before sending it again with a **new** idempotency key: retrying with the same key returns the same `uncertain` message and sends nothing.

## Handle errors

A refused request sends nothing and charges nothing, except where noted.

| Response | Code | What to do |
| --- | --- | --- |
| `400` | `invalid_argument` and validation details | Fix the request: `channel`, field names, the `from` or `to` format, or the `Idempotency-Key` header. |
| `400` | `whatsapp_template_variable_missing`, `whatsapp_template_variable_unknown` | Send exactly the template's variables, each with a non-empty value. |
| `403` | `not_allowed` | Use a key with the `whatsapp.send` permission. With the code `FEATURE_NOT_AVAILABLE`, WhatsApp is not turned on for your organization. |
| `404` | `whatsapp_number_not_found`, `whatsapp_sender_removed` | Check the `from` phone number, and that the number is still connected in this project. |
| `404` | `whatsapp_template_not_found` | Check the Template ID, and that the template belongs to the sender's WhatsApp Business Account. |
| `409` | `whatsapp_idempotency_key_reused` | Use a new key for a new message. |
| `422` | `whatsapp_number_not_ready` | Open the sender's page: the number is not connected or WhatsApp is limiting it. |
| `422` | `whatsapp_template_not_approved`, `whatsapp_template_not_supported` | Wait for approval, or use a template Nexiom Connect can send. |
| `422` | `whatsapp_message_rejected` | WhatsApp refused the message. The message is stored as `failed` with WhatsApp's reason. |
| `422` | `whatsapp_provider_unavailable` | WhatsApp is busy. Retry later with a new idempotency key; the refused message is stored as `failed`. |
| `429` and `403` | Plan and wallet codes | Your allowance or wallet needs attention. See [Costs](#costs). |

Every code is listed in the [Send a message](/docs/api/messaging/send#errors) reference, and WhatsApp's own failure codes in [Best practices and FAQ](/docs/whatsapp/best-practices#whatsapp-failure-codes).

## Follow your messages

- **API**: [Retrieve a message](/docs/api/messaging/retrieve) by its ID for its current status, cost, and content.
- **Webhooks**: subscribe to [whatsapp.message.sent](/docs/webhooks/whatsapp/sent), [delivered](/docs/webhooks/whatsapp/delivered), [read](/docs/webhooks/whatsapp/read), and [failed](/docs/webhooks/whatsapp/failed) to be notified of each change.
- **Dashboard**: **Messaging → Logs** lists every message with its timeline and content; **Messaging → Analytics** shows volume and delivery rates by day.

## Costs

Two parties charge for a WhatsApp message:

| Charged by | What | Where |
| --- | --- | --- |
| Nexiom Connect | Each message you send, against your plan | Your Nexiom plan, then your Nexiom wallet |
| Meta | WhatsApp's own fee, which depends on the template category and the recipient's country | The payment method of your WhatsApp Business Account, in WhatsApp Manager |

Your plan includes a number of WhatsApp messages each billing period. Beyond them, each message is paid from your wallet at your plan's price per message:

- An included message counts once WhatsApp accepts it, even if it fails later.
- A message beyond the included ones reserves its price in your wallet, and is charged only when it is delivered or read. If it fails, or is not delivered within 24 hours, nothing is charged.
- Owners and admins are notified when 80% and 100% of the included messages are used.

Each message shows what Nexiom Connect charged for it: `is_billable`, `cost` as a decimal string such as `"0.1000"`, and `cost_currency`. `cost` is `"0.0000"` for included messages and for messages that were not charged. It never includes Meta's fees.

When a send is refused for your plan or wallet, nothing is sent or charged:

| Response | Code | Returned when |
| --- | --- | --- |
| `429` | `included_usage_exhausted` | Your plan's included messages are used up and the plan has no paid messages. |
| `403` | `paid_usage_disabled` | The included messages are used up and paid usage is turned off. |
| `429` | `usage_spend_limit_exceeded` | The message would go past your spending limit. |
| `429` | `wallet_balance_insufficient` | Your wallet cannot cover the message. Top it up. |
| `429` | `service_not_in_plan` | Your plan doesn't include WhatsApp messages. Change your plan. |

## Limits

- **Requests**: your organization's API requests per second depend on your plan. A request past it returns `429` with the code `api_rate_limit_per_second_exceeded`; wait a moment and retry with the same idempotency key.
- **Messaging limit**: WhatsApp limits how many different customers your business portfolio can message in 24 hours. Past it, messages become `failed`. See [Messaging limits](/docs/whatsapp/key-concepts#messaging-limits).
- **Throughput**: each number sends up to its throughput, usually 80 messages per second. Spread large sends over time.

## Retention

Message logs and their content are kept for 30 days. After that, the message is deleted and [Retrieve a message](/docs/api/messaging/retrieve) returns `404`. Daily totals in **Messaging → Analytics** are kept beyond 30 days. To keep a longer history, store the message ID and the statuses from webhooks in your own system.

## Next steps

- [Send a message](/docs/api/messaging/send) Parameters, responses, and every error code.
- [WhatsApp webhooks](/docs/webhooks/event-types#whatsapp-events) Get notified when messages are delivered, read, or fail.
- [Best practices and FAQ](/docs/whatsapp/best-practices) Opt-in, quality, and a go-live checklist.

---

Source: https://connect.nxiom.com/docs/whatsapp/sending
