# Send messages

> Text, media, location and templates: formats, the 24-hour window, statuses, history and errors.

`POST /messages` **accepts** a message and responds immediately (`202`, status `queued`). The actual delivery to Meta, any automatic retries and status tracking happen in the background.

## Common fields

| Field | Description |
|---|---|
| `phoneNumberId` | The sending number: the `id` returned by the [account connection](/docs/guides/connect-whatsapp). |
| `to` | The recipient, **in international format with a "+"**: `+237600000000`. Spaces, hyphens and parentheses are ignored. |
| `type` | `text`, `template`, `image`, `video`, `audio`, `document` or `location`. |

<Callout type="warning" title="The “+” is required">
A local number without a country code (`600000000`) is rejected, so that it never goes to the wrong country. In responses, the number is returned without the "+" (`237600000000`).
</Callout>

## A template (at any time)

A template **approved by Meta** can be sent at any time, even without a prior exchange. It is the only way to **start** a conversation. See [Templates](/docs/guides/templates).

<ApiExample method="POST" path="/messages" />

## A free-form message: the 24-hour window

A free-form message is only accepted if the recipient has **written to you within the last 24 hours** on this number. Otherwise:

<ErrorExample name="outside_service_window" />

Within the window, all message types are allowed.

### Text

<ApiExample method="POST" path="/messages" variant="text" />

The text body is 1 to 4096 characters long.

### Image

<ApiExample method="POST" path="/messages" variant="image" />

For media (`image`, `video`, `audio`, `document`), provide **either** `link` (a public `https://` URL) **or** `id` (media already hosted by Meta), never both. `caption` doesn't exist for audio. Uploading and downloading media through the API is not yet available.

### Document

<ApiExample method="POST" path="/messages" variant="document" />

### Location

<ApiExample method="POST" path="/messages" variant="location" />

## Track a message

<ApiExample method="GET" path="/messages/{id}" />

`error` is `null` as long as everything is fine; on failure, it contains Meta's `code` and `message`. To be notified without polling the API, use [webhooks](/docs/guides/webhooks) (`message.sent`, `message.delivered`, `message.read`, `message.failed`).

## History

<ApiExample method="GET" path="/messages" />

History is sorted from newest to oldest and **cursor-paginated**: as long as `nextCursor` is not `null`, pass it in `?cursor=` to get the next page. Filters: `phoneNumberId`, `direction` (`inbound`, `outbound`), `status`, `limit` (1 to 100).

## Sending errors

| Code | HTTP | Cause |
|---|---|---|
| `invalid_message` | 422 | Malformed number, empty text, ambiguous media (`id` and `link` together), non-`https` link |
| `outside_service_window` | 422 | Free-form message outside the 24-hour window |
| `template_not_approved` | 422 | The template exists but is not (or no longer) approved |
| `idempotency_conflict` | 422 | Same `Idempotency-Key` for a different request |
| `quota_exceeded` | 402 | Your plan's monthly quota has been reached (production) |
| `not_found` | 404 | Unknown number, or one from another environment |

<ErrorExample name="invalid_message" />

Meta's rejections during delivery (template not found, recipient unreachable…) don't show up in the `202` response: the message moves to `failed`, with Meta's reason, and you receive `message.failed`.

<Callout type="tip" title="Safe replays">
Always add an `Idempotency-Key` header to your sends: if your call times out or your server restarts, replay it as is, and the message will never be sent twice. See [Reliability](/docs/guides/reliability).
</Callout>
