# Sending messages

> POST Meta's own message body plus from; bulk batches, read receipts, and what comes back.

Canonical: https://docs.whabuzz.com/guides/messages/

WhaBuzz does not have its own message format. You send **Meta's `/messages` body**, with one extra field, `from`.

## Send one message {#send}

[`POST /v1/messages`](https://docs.whabuzz.com/api/messages.md#sendMessage)

```json
{
  "from": "1013622581841046",
  "messaging_product": "whatsapp",
  "to": "16505551234",
  "type": "text",
  "text": { "body": "Your order has shipped." }
}
```

- `from` (required) is the `phone_number_id` of one of your numbers, or its E.164 number. WhaBuzz removes it and calls `POST /{phone_number_id}/messages` on the Graph API.
- `messaging_product` defaults to `whatsapp` if you leave it out.
- Nothing else is added, removed or reordered. Every message type Meta supports works the day Meta ships it: text, template, image, video, audio, document, sticker, location, contacts, interactive, reaction, flows.
- The response is Meta's, with Meta's HTTP status.

WhaBuzz does not check the 24-hour window, template category, opt-out status or content. Meta decides, and if Meta refuses you see Meta's error unchanged (see [Errors](https://docs.whabuzz.com/errors.md#reading-meta-errors)).

Free-form messages need an open customer service window (the customer messaged you in the last 24 hours). To start a conversation use a [template](https://docs.whabuzz.com/guides/templates.md).

## Headers you get back {#headers}

`X-WhaBuzz-Request-Id` (`req_…`), `X-WhaBuzz-Event-Id` (`evt_…`, the `message.outbound` event that records this send), `X-WhaBuzz-Processing-Ms` and `Server-Timing: wb;dur=…, meta;dur=…`.

## Retry safely {#idempotency}

Send an `Idempotency-Key` header. See [Idempotency](https://docs.whabuzz.com/guides/idempotency.md).

## Mark as read {#read}

[`POST /v1/messages/read`](https://docs.whabuzz.com/api/messages.md#markRead) takes `from` and `message_id` (the wamid of an inbound message), and shows the customer blue ticks. Add `"typing_indicator":{"type":"text"}` to also show the typing indicator.

## Bulk sends {#batch}

[`POST /v1/messages/batch`](https://docs.whabuzz.com/api/messages.md#createBatch) queues 1 to 10,000 messages in one call and answers `202` immediately:

```json
{ "id": "bat_01JAB3K9X2S8C4M7Q1V5R6T0YZ", "status": "queued", "total": 3 }
```

The body is `from`, an optional `client_ref` (your reference, unique per tenant; repeating it returns the existing batch) and `messages`, an array of Meta `/messages` bodies without `from`. Items are sent in order at the number's pacing, which is Meta's messaging throughput: 20 per second for a standard number, 50 for a high-throughput one. A scheduler runs every minute and sends up to 900 items per run, so a batch of 10,000 takes about 12 minutes on a standard number. See [Rate limits and pacing](https://docs.whabuzz.com/guides/rate-limits.md).

- [`GET /v1/messages/batch/{batch_id}`](https://docs.whabuzz.com/api/messages.md#getBatch): counts by status.
- [`GET /v1/messages/batch/{batch_id}/items?status=failed`](https://docs.whabuzz.com/api/messages.md#listBatchItems): per-item results, each with Meta's response.
- [`POST /v1/messages/batch/{batch_id}/cancel`](https://docs.whabuzz.com/api/messages.md#cancelBatch): stops items not yet sent.

Meta errors on an item are recorded on that item and **never retried by WhaBuzz**, because resending a rejected message is your decision, not ours. When the batch finishes you receive a `batch.completed` event ([WhaBuzz events](https://docs.whabuzz.com/events/whabuzz-events.md)). Each item also produces its own `message.outbound` event and, later, `message.status` events.

## What happens after a send {#after}

Meta's HTTP answer means "accepted", not "delivered". Delivery, read and failure arrive later as [`message.status`](https://docs.whabuzz.com/events/message-status.md) events matched on the wamid in the send response.
