---
title: Sending messages | API Docs
description: Choose a send route, respect the customer window, and retry safely.
---

There are two ways to send. `POST /v1/messages` takes a destination and lets Linq select the eligible line and existing chat.

`POST /v1/chats/{chat}/messages` sends within a chat you already know. Both return the same accepted-message shape and support idempotency.

For every part type you can send, with examples and limits, see [Message types](/channel/whatsapp/guides/messaging/message-types/index.md). To message a customer outside the customer window, see [Templates](/channel/whatsapp/guides/templates/index.md).

WhatsApp separates service messages, sent during the customer window, from template messages in marketing, utility, and authentication categories. See [service messages](https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-messages/) and [marketing, utility, and authentication templates](https://developers.facebook.com/docs/whatsapp/business-management-api/message-templates/) for the channel rules. The Linq part schemas remain the authority for shapes accepted by this API.

## Pricing

Since October 1, 2026, Meta charges for service messages as well as templates. Each business phone number gets 1,000 free service messages per month. The allowance resets monthly and does not roll over. After that, Meta charges each service message at the market’s utility rate. A utility template sent while the customer window is open is also charged. Meta’s rules are on its [non-template message pricing](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing/non-template-messages) page.

Meta bills these charges to the payment method on your WhatsApp Business Account. Replies inside the customer window count toward your Linq spend limits. A daily cap still lets those replies through. The monthly account cap stops every send, replies included, until the cap resets at the start of the next UTC month.

## Always make sends idempotent

Set `Idempotency-Key` to a stable value for the logical send. If a timeout leaves the response unknown, retry the same route with the same key and body. Do not generate a new key for a transport retry, and do not reuse a key for different content.

With zero-day retention, an idempotent retry returns empty `parts` and `zero_retention: true`; the original response still includes the content.

## Choose content from the customer window

The chat read exposes `customer_window`. While it is open, you may send free-form parts such as text or media. Outside it, send one approved `template` part. An out-of-window free-form request is refused with HTTP `422` and `error.code` [2036](/channel/whatsapp/error/codes/2xxx/2036/index.md); Linq does not replace your content.

```
{
  "to": "+15551234567",
  "parts": [{ "type": "text", "body": "How can we help?" }]
}
```

```
{
  "to": "+15551234567",
  "parts": [
    {
      "type": "template",
      "name": "order_update",
      "language": "en_US",
      "parameters": { "order_number": "A-1042" }
    }
  ]
}
```

The values above are illustrative. Read `GET /v1/templates` and validate each send against the selected template’s `send_schema`. The part schema served by `GET /v1/parts/{type}` defines the common template fields. Validation rejects invalid content and returns JSON Pointers to the most specific identifiable field.

## Customers who use a username

A customer can use a WhatsApp username and keep their phone number hidden. For that customer, the chat’s `customer` is a username id rather than a phone number. Events such as `message.received` identify the chat by `chat_id`; read the chat with `GET /v1/chats/{chat}` to get its `customer`:

```
{
  "to": "US.13491208655302741918",
  "parts": [{ "type": "text", "body": "Thanks, we're on it." }]
}
```

The id is a two-letter country code, a period, and up to 128 letters and digits; a longer form such as `US.ENT.…` also exists. Treat every customer id as opaque:

- store it as a string, and never parse it, validate it, or format it as a phone number;
- reply by copying `customer` into `to` exactly as delivered, or send on `POST /v1/chats/{chat}/messages`, which needs no address.

Every message type works with a username id except one-time-code authentication templates (copy code, one-tap, or zero-tap autofill), which can be delivered only to a phone number. Such a send is refused with HTTP `422` and `error.code` [4011](/channel/whatsapp/error/codes/4xxx/4011/index.md), and nothing is sent; deliver the code another way. If the refusal can only be known after acceptance, the message fails with `recipient_phone_number_required` instead.

## Follow the outcome

HTTP `202` means accepted, not delivered. Consume event signals, then read

`GET /v1/chats/{chat}/events` for the message’s current state. Terminal outcomes are delivered, read, or failed. If no terminal evidence arrives, keep the outcome unresolved rather than blindly resending.

## Why a message failed

A message whose `status` is `failed` may carry a `failure` object beside it: a `code` you can branch on and a `message` written for a person reading it. It is present only when the channel’s refusal has a reason in the published vocabulary, so a message that failed on the wire may carry no `failure` at all. Its absence means “no published reason”, never “no reason”.

`status` stays the thing to branch on. Codes are added over time, so treat a code you do not recognise exactly as you treat a message with no `failure`: the message failed, terminally, and `status` already told you that. Never branch on `message` — the wording is written for a person and is not stable.

The reason lives on the message itself, not on the event that announces it. Read it from the message rows of `GET /v1/chats/{chat}/events`, including the message a `message.failed` [webhook](/channel/whatsapp/guides/webhooks/index.md) or [stream event](/channel/whatsapp/guides/events/index.md) points you to; the event carries only the chat id and its sequence range.

```
{
  "id": "msg_example",
  "chat_id": "chat_example",
  "direction": "outbound",
  "status": "failed",
  "failure": {
    "code": "recipient_country_restricted",
    "message": "the business behind the sending number is not currently permitted to message customers in this recipient's country, so the message was not delivered and sending it again cannot succeed"
  },
  "parts": [{ "type": "text", "body": "How can we help?" }],
  "created_at": "2026-09-16T14:02:11Z"
}
```

The ids and `message` above are illustrative. The published codes today:

| Code                              | Meaning                                                                                                                                                                                                                                                                                                                                                        | What to do                                                                                                                                                                                                                                                                                                                                                                                                                 |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sender_deregistered`             | The number this message was going out from is no longer a registered sending number, and the message never left. Terminal: it is not a pause and it does not clear, so retrying the same message cannot succeed and that conversation is over.                                                                                                                 | Reach this customer by starting a new conversation on another of your numbers with a template, exactly as you would any customer whose 24-hour window has closed. `GET /v1/chats` shows which number each conversation uses.                                                                                                                                                                                               |
| `quality_hold_expired`            | This marketing message remained held by an automatic safety control on its sending line for ten minutes and was not sent. Terminal: retrying the same message cannot succeed on that conversation.                                                                                                                                                             | Send marketing from another of your numbers with a template, or contact support about the sending line.                                                                                                                                                                                                                                                                                                                    |
| `recipient_country_restricted`    | The business behind the sending number is not currently permitted to message customers in this recipient’s country, so the message was not delivered. Terminal for the message: sending it again cannot succeed while the restriction stands. It can arrive as an immediate `failed` or after the message was first reported `sent`; both mean the same thing. | Most often the business has not yet completed business verification: complete it, and until then message customers in the sending number’s own country. If the business is already verified, review the messaging policy for the business’s category. There is no way to ask about the restriction ahead of a send.                                                                                                        |
| `payment_method_required`         | The business behind the sending number has no working payment method on its messaging account, so the message was not sent. Terminal for the message: sending it again cannot succeed until a payment method is added. It can arrive as an immediate `failed` or after the message was first reported `sent`; both mean the same thing.                        | Add or update the payment method in the messaging account’s billing settings, then send again as a new message. A number’s replies inside an open 24-hour customer window keep being delivered while they stay within its 1,000 free service messages for the month, so a working free-form reply beside a failed template is possible. Past that allowance, replies need the payment method too. See [pricing](#pricing). |
| `recipient_phone_number_required` | This customer is known only by a username id, not a phone number, and this message can be delivered only to a phone number, so it was not sent. Terminal for the message: sending it again to this customer cannot succeed. It can arrive as an immediate `failed` or after the message was first reported `sent`; both mean the same thing.                   | One-time-code authentication templates are the usual cause; deliver the code another way. Other messages, including templates without a one-time-code button, can still be sent to this customer. See [customers who use a username](#customers-who-use-a-username).                                                                                                                                                       |
| `business_restricted`             | The business behind the sending number is most likely restricted on its messaging account, so the message was not sent. Terminal for the message: sending it again will not succeed while the restriction stands. It can arrive as an immediate `failed` or after the message was first reported `sent`; both mean the same thing.                             | Review the status of the business’s messaging account. If the business believes it complies with the messaging policies, request a review of the restriction.                                                                                                                                                                                                                                                              |
| `channel_refused`                 | The message was refused before delivery and was not sent, for a reason that has no more specific code yet. It is not retried automatically. It can arrive as an immediate `failed` or after the message was first reported `sent`; both mean the same thing. A more specific code may be published for some of these later.                                    | Contact support with the message’s `id` to learn the reason and whether sending it again can succeed. Treat a code you do not recognise exactly as you treat this one.                                                                                                                                                                                                                                                     |

You are not billed for a message that failed with any of these codes.

## Send media

A `media` part names its bytes either by an `upload_ref` from

`POST /v1/media` or by a public `https` address in `source_url`, which the API fetches once before accepting the send. The [attachments guide](/channel/whatsapp/guides/messaging/attachments/index.md) covers both routes, the address policy, and the refusal codes.

## React to a message

A `reaction` part carries an emoji and the message it belongs to. Address the target with `message_id` — this API’s own `Message.id`, the value returned when the message was accepted and shown in the transcript — not the channel’s `channel_message_id`, which is inbound-only and refused on a send with `error.code` [1006](/channel/whatsapp/error/codes/1xxx/1006/index.md).

```
{
  "parts": [{ "type": "reaction", "message_id": "msg_example", "emoji": "👍" }]
}
```

Send the same target with an empty `emoji` to remove your earlier reaction. An absent `emoji` is rejected rather than read as a removal.

A reaction is a free-form part, so it follows the customer window like text. The target must also be a message this account can see in the chat you are sending to, and one the channel has acknowledged if this API sent it:

| Refusal                                                  | Meaning                                                                    |
| -------------------------------------------------------- | -------------------------------------------------------------------------- |
| [2049](/channel/whatsapp/error/codes/2xxx/2049/index.md) | No message with that id is visible to this account.                        |
| [1040](/channel/whatsapp/error/codes/1xxx/1040/index.md) | The message belongs to a different chat than the one you are sending to.   |
| [2050](/channel/whatsapp/error/codes/2xxx/2050/index.md) | This API accepted the message but the channel has not acknowledged it yet. |

All three are HTTP `422` pointered at `/parts/0/message_id`.

Two behaviours differ from other sends and are easy to get wrong.

**A reaction ends at `sent`.** The channel reports no delivered or read receipt for one, so do not wait for a terminal outcome beyond `sent` the way you would for a text. It can still end `failed` if the channel refuses it, and a test key simulates `delivered` as it does for every send.

**For [2050](/channel/whatsapp/error/codes/2xxx/2050/index.md), poll the target’s `status` until it reads `sent`** — the same moment its `channel_message_id` appears — and then resend. Do not wait on the target’s `message.sent` event: that fires when this API accepts a send, not when the channel acknowledges it, so it has usually already arrived when the refusal happens.

The channel will not deliver a reaction whose target is more than 30 days old, deleted, missing from the thread, or itself a reaction. That is refused on the wire after your send was accepted, so it surfaces as a failed message rather than a `422`.

## Mark a chat read

`POST /v1/chats/{chat}/read` marks the customer’s messages read, and with `{"typing": true}` also shows a typing indicator. It targets the newest unread inbound message the channel can mark read. HTTP `202` means the command was accepted; the `chat.read` event names the message it targeted.

**Unsupported messages are never targeted.** Some things a customer can send, such as a poll, reach you as an `unsupported` part, and the channel refuses to mark those read. The command skips them and targets the newest supported message instead. An unsupported message stays unread on the customer’s phone even after a later message is marked read.

| Refusal                                                  | Meaning                                                          |
| -------------------------------------------------------- | ---------------------------------------------------------------- |
| [2035](/channel/whatsapp/error/codes/2xxx/2035/index.md) | `nothing_unread`: the chat has no unread inbound message.        |
| [2056](/channel/whatsapp/error/codes/2xxx/2056/index.md) | `nothing_readable`: every unread inbound message is unsupported. |

Both are HTTP `409`, and neither changes anything. If you mark a chat read after every inbound message, treat both as “nothing to do” rather than a failure.

## Download incoming attachments

A fresh `message.received` [webhook](/channel/whatsapp/guides/webhooks/index.md) includes `data.message`, so you can read its `parts` without fetching the message again. Look for a `media` part with a `url`. For example, an incoming document:

```
{
  "type": "media",
  "kind": "document",
  "filename": "invoice.pdf",
  "media_id": "example-inbound-media-id",
  "url": "https://whatsapp.messages.api.linqapp.com/v1/attachments/media_example/content",
  "availability": "available"
}
```

Use the returned `url` with `GET /v1/attachments/{attachment}/content`. It requires your account’s Bearer API key on every download; it is not a public file link. Download from your server and keep the key out of browser code and logs. Only attach credentials to the trusted WhatsApp API origin shown below, and do not follow redirects.

Terminal window

```
# Use the document part's returned URL, not its media_id or upload_ref.
ATTACHMENT_URL='https://whatsapp.messages.api.linqapp.com/v1/attachments/media_example/content'


file=$(mktemp) || exit 1
trap 'rm -f "$file"' EXIT
status=$(curl --silent --show-error \
  --proto '=https' --max-time 30 \
  --header "Authorization: Bearer ${LINQ_WHATSAPP_API_KEY}" \
  --output "$file" --write-out '%{http_code}' \
  "$ATTACHMENT_URL") || exit 1


if [ "$status" = 200 ]; then
  mv "$file" invoice.pdf
else
  printf 'Attachment download returned HTTP %s\n' "$status" >&2
  exit 1
fi
```

This example makes one request and saves the original bytes on HTTP `200`. It intentionally omits `--location`, so credentials are not forwarded through a redirect. In your application, accept only `200` as a successful download and choose the local output filename yourself. See the [attachment reference](/channel/whatsapp/api/resources/attachments/index.md) for the response contract and [error handling](/channel/whatsapp/guides/platform/errors/index.md).

### When the file is ready

A `message.received` event for a message with attachments can be recorded once every attachment has been captured or has failed, or ten minutes after the message arrived, whichever comes first. This is being enabled gradually, so an event can still arrive before its attachment is downloadable.

Each incoming `media` part may carry two response-only fields:

| `availability` | `unavailable_reason` | Download                                                                             |
| -------------- | -------------------- | ------------------------------------------------------------------------------------ |
| `available`    | —                    | `200` at once.                                                                       |
| `unavailable`  | `timed_out`          | Not captured within ten minutes. Capture continues: `409` until ready, then `200`.   |
| `unavailable`  | `capture_failed`     | Could not be retrieved. `410`, or `404` once the file is no longer known.            |
| `unavailable`  | `expired`            | No longer retained, or no longer known. `410` or `404`.                              |
| `pending`      | —                    | Capture not finished. Returned only by reads of the conversation, never by an event. |
| absent         | —                    | May still be capturing. Retry the `409` as `Retry-After` directs.                    |

- `unavailable_reason` is an open string. Treat an unknown value as `capture_failed`.
- The fields describe the file, not the exact answer of a later download, which can also refuse for capacity or authorization.
- An event keeps the values it was recorded with. Reads of the conversation, such as `GET /v1/chats/{chat}/events`, return the current ones.
- A missing `byte_size` is not final: it appears once the bytes are captured.
- Both fields are refused on sends.

Because an event can wait for its attachments, it can arrive after events for later messages of the same chat. See [Events and recovery → Messages with attachments](/channel/whatsapp/guides/events#messages-with-attachments/index.md).

### Download responses

| Response                                                             | Action                                                                                                                                                                                                                                                                                                      |
| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `409`, code [2007](/channel/whatsapp/error/codes/2xxx/2007/index.md) | Capture is still pending. Wait at least `Retry-After` before retrying. Use a bounded policy, such as at most three attempts within 30 seconds; if the requested wait exceeds the remaining budget, stop and schedule a later attempt. A `timed_out` attachment can keep answering `409` and later download. |
| `410`, code [2043](/channel/whatsapp/error/codes/2xxx/2043/index.md) | The attachment failed or expired. This is final: stop retrying this download.                                                                                                                                                                                                                               |
| `404`, code [2003](/channel/whatsapp/error/codes/2xxx/2003/index.md) | No downloadable attachment is visible to this key, including missing or pruned files. Check the URL and account credentials; do not retry blindly.                                                                                                                                                          |

Older events may contain only a chat and sequence range. Read

`GET /v1/chats/{chat}/events` when `data.message` is absent, and download only parts that actually include a `url`. The URL is response-only; do not include it when sending a message.
