# Send a message

`POST /v1/messages`

Checks a message and accepts it for sending; it reaches WhatsApp a moment later. Follow it with the `message.sent` and `message.status` webhooks, or with `GET /v1/messages/{id}`.

Needs `messages:send`. Counts against `api_messages_per_second`.

> Outside the 24-hour window, only a template can be sent.

## Body

- `type` (string, required): One of `text`, `image`, `video`, `audio`, `document`, `template`, `interactive`.
- `to` (string): The phone number, with its country code. Spaces, dashes, dots and brackets are ignored, and `00` counts as `+`. Send this or `contact_id`, not both.
- `contact_id` (string, uuid): The contact to send to. Send this or `to`, not both.
- `number_id` (string, uuid): The number to send from. Leave it out to let Soosh choose.
- `text` (object): For `type: text`.
  - `body` (string, required): 1 to 4,096 characters.
  - `preview_url` (boolean): Show a preview of the first link in the text. Default `false`.
- `media` (object): For `type: image`, `video`, `audio` or `document`. The file is exactly one of `media_id` and `url`.
  - `media_id` (string, uuid): An upload from `POST /v1/media`. Send this or `url`, not both.
  - `url` (string, uri): An `https://` link to the file. Send this or `media_id`, not both.
  - `caption` (string): A caption for the file. Not for audio. Up to 1,024 characters.
  - `filename` (string): For `type: document`. The name the customer sees.
- `template` (object): For `type: template`. The template, by `id` or by `name` and `language`, and its variables.
  - `id` (string, uuid): The template's ID. Send this, or `name` and `language`.
  - `name` (string): The template's name. Send it with `language`, or send `id` instead.
  - `language` (string): The template's language, such as `en`. Needed with `name`.
  - `body` (object): Keys are the variable names in the template's `variables.body`. A map; each value is a string.
  - `header` (object): The header's variable as `text`, or the file of an image, video or document header as one of `media_id` and `url`.
    - `text` (string)
    - `media_id` (string, uuid)
    - `url` (string, uri)
  - `buttons` (object): Keys are button indexes, as in the template's `variables.buttons`. A value is a link's suffix or a coupon code. A map; each value is a string.
- `interactive` (object): For `type: interactive`. Either up to 3 reply buttons (`type: buttons`), or one link button (`type: cta_url`, with `url` and `display_text`).
  - `type` (string, required): One of `buttons`, `cta_url`.
  - `body` (string, required): 1 to 1,024 characters.
  - `buttons` (array of objects): Each `id` is unique. 1 to 3 items.
    - `id` (string, required): 1 to 256 characters.
    - `title` (string, required): 1 to 20 characters.
  - `url` (string, uri): An `https://` link.
  - `display_text` (string): 1 to 20 characters.
- `reply_to_message_id` (string, uuid): A message of the same chat to quote.

## Headers

- `Idempotency-Key` (string): Send one so a retry doesn't send the message twice. See [Retrying safely](/reference/api/#retrying-safely). 1 to 255 characters.

## How it works

### Choosing the customer

A template sent to a number with no contact creates one, messaged from the sending number. That counts against your `contacts` limit and sends `contact.created`. Any other type to such a number is `window_closed`. A contact you deleted is brought back.

### Which number it's sent from

Soosh uses `number_id` if you send it. Otherwise it uses the template's number, then the contact's number, then your default number.

### The window is per number

It counts from the contact's last message to the sending number, not to your other numbers. If WhatsApp disagrees with Soosh's record, the message fails later with `error.code` 131047.

### Closed chats

Sending anything but a template reopens a closed chat and sends `chat.reopened`.

### Sending a template

- With `number_id`, the template must be that number's. If two of your numbers have a template with the same name and language, `number_id` is required.
- Send exactly the keys in the template's `variables`, no more and no fewer. Read them with `GET /v1/templates`.
- A template with an image, video or document header needs its file in `header.media_id` or `header.url`. A header file must be JPEG or PNG (image), MP4 or 3GPP (video), or PDF (document).
- For an authentication template, `body` is `{"1": "<code>"}`. The code button is filled in for you.

### Sending a file

A `media_id` must be an upload that hasn't expired. Soosh downloads a `url` once, within 10 seconds, without following redirects, and reads the type from the file. Either way the file is kept with the message.

## Example: Text to a contact

```sh
curl -X POST https://app.soosh.io/v1/messages \
  -H "Authorization: Bearer $SOOSH_API_KEY" \
  -H "Idempotency-Key: send-message-text-to-a-contact-1" \
  -H "Content-Type: application/json" \
  -d '{
    "contact_id": "4ea0d485-435d-407f-837a-c7d67095f23a",
    "type": "text",
    "text": {
      "body": "Your order has shipped."
    }
  }'
```

Response 201:

```json
{
  "id": "8c2e4f6a-1d3b-4a5c-9e7f-2b4d6f8a0c1e",
  "contact_id": "4ea0d485-435d-407f-837a-c7d67095f23a",
  "number_id": "0f6d3c1e-5b7a-4c52-9a55-3f6a2d5c8b10",
  "direction": "outgoing",
  "type": "text",
  "status": "pending",
  "text": {
    "body": "Your order has shipped."
  },
  "media": null,
  "template": null,
  "interactive": null,
  "reply_to_message_id": null,
  "whatsapp_message_id": null,
  "error": null,
  "sent_by": {
    "type": "api_key",
    "id": "5e1d2c3b-4a59-4f68-8e7d-6c5b4a392817",
    "name": "Order system"
  },
  "broadcast_id": null,
  "created_at": "2026-10-14T09:30:12.481Z",
  "updated_at": "2026-10-14T09:30:12.481Z"
}
```

## Example: Template order_update to a phone number

```sh
curl -X POST https://app.soosh.io/v1/messages \
  -H "Authorization: Bearer $SOOSH_API_KEY" \
  -H "Idempotency-Key: send-message-template-order-1" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+919876543210",
    "type": "template",
    "template": {
      "name": "order_update",
      "language": "en",
      "header": {
        "text": "A-1042"
      },
      "body": {
        "1": "Priya",
        "2": "A-1042"
      },
      "buttons": {
        "0": "A-1042"
      }
    }
  }'
```

Response 201:

```json
{
  "id": "8c2e4f6a-1d3b-4a5c-9e7f-2b4d6f8a0c1e",
  "contact_id": "4ea0d485-435d-407f-837a-c7d67095f23a",
  "number_id": "0f6d3c1e-5b7a-4c52-9a55-3f6a2d5c8b10",
  "direction": "outgoing",
  "type": "template",
  "status": "pending",
  "text": {
    "body": "Hi Priya, your order A-1042 is on its way."
  },
  "media": null,
  "template": {
    "id": "2f9a6c1d-5e3b-4a78-8c04-b1d7e5f3a920",
    "name": "order_update",
    "language": "en"
  },
  "interactive": null,
  "reply_to_message_id": null,
  "whatsapp_message_id": null,
  "error": null,
  "sent_by": {
    "type": "api_key",
    "id": "5e1d2c3b-4a59-4f68-8e7d-6c5b4a392817",
    "name": "Order system"
  },
  "broadcast_id": null,
  "created_at": "2026-10-14T09:30:12.481Z",
  "updated_at": "2026-10-14T09:30:12.481Z"
}
```

## Example: An image by media_id

```sh
curl -X POST https://app.soosh.io/v1/messages \
  -H "Authorization: Bearer $SOOSH_API_KEY" \
  -H "Idempotency-Key: send-message-an-image-by-media-id-1" \
  -H "Content-Type: application/json" \
  -d '{
    "contact_id": "4ea0d485-435d-407f-837a-c7d67095f23a",
    "type": "image",
    "media": {
      "media_id": "5a9c3e7b-2d4f-4b61-8c0e-7f1a3d5b9e24",
      "caption": "Your parcel"
    }
  }'
```

Response 201:

```json
{
  "id": "8c2e4f6a-1d3b-4a5c-9e7f-2b4d6f8a0c1e",
  "contact_id": "4ea0d485-435d-407f-837a-c7d67095f23a",
  "number_id": "0f6d3c1e-5b7a-4c52-9a55-3f6a2d5c8b10",
  "direction": "outgoing",
  "type": "image",
  "status": "pending",
  "text": null,
  "media": {
    "url": "/v1/messages/8c2e4f6a-1d3b-4a5c-9e7f-2b4d6f8a0c1e/media",
    "mime_type": "image/jpeg",
    "filename": "parcel.jpg",
    "caption": "Your parcel"
  },
  "template": null,
  "interactive": null,
  "reply_to_message_id": null,
  "whatsapp_message_id": null,
  "error": null,
  "sent_by": {
    "type": "api_key",
    "id": "5e1d2c3b-4a59-4f68-8e7d-6c5b4a392817",
    "name": "Order system"
  },
  "broadcast_id": null,
  "created_at": "2026-10-14T09:30:12.481Z",
  "updated_at": "2026-10-14T09:30:12.481Z"
}
```

## Returns

201. The message, in `status: pending`. Nothing is saved when a check fails.

## Errors

Besides the ones every request can get (https://docs.soosh.io/reference/api/errors/):

- 422 `window_closed`: The window is closed. Send a template instead.
- 422 `invalid_phone`: The `to` isn't a valid phone number.
- 422 `invalid_field`: A field breaks a rule: for example both `to` and `contact_id`, a `contact_id` or `number_id` that isn't in your workspace, an unknown or expired `media_id`, a file type the message can't carry, or a `url` that can't be fetched.
- 422 `template_params`: The template's variables are missing or extra. `details.missing` and `details.unexpected` say which.
- 422 `template_ambiguous`: Two of your numbers have a template with that name and language, and you sent no `number_id`.
- 422 `template_not_approved`: The template isn't approved.
- 422 `contact_opted_out`: A marketing template goes to a contact who opted out.
- 422 `number_unavailable`: The number you send from isn't `active`.
- 422 `limit_reached`: The number has no contact yet, and your workspace is at its `contacts` limit.
- 409 `idempotency_in_progress`: A request with the same `Idempotency-Key` is still running.
- 422 `idempotency_mismatch`: The `Idempotency-Key` was used before for a different request, or by another of your API keys.