---
title: "Sending messages"
description: "Send SMS and MMS in batches, attach metadata, schedule messages and handle per-message errors."
---

> Documentation Index
> Fetch the complete documentation index at: https://dev.touchsms.com.au/llms.txt
> Use this file to discover all available pages before exploring further.

# Sending messages

All outbound messages go through one endpoint, [Send SMS](/api/sms/send/) (`POST /v2/sms`). The body is a JSON object with a `messages` array, so you can send one message or many in the same request.

```sh
curl -u "ACCESS_TOKEN:TOKEN_ID" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -X POST https://app.touchsms.com.au/api/v2/sms \
  -d '{
    "messages": [
      { "to": "+61412345678", "from": "SHARED_NUMBER", "body": "Your order has shipped." },
      { "to": "+61412345679", "from": "SHARED_NUMBER", "body": "Your order has shipped." }
    ]
  }'
```

Requests must use `Content-Type: application/json`, because the body is nested.

## Each message

| Field | Required | Notes |
|---|---|---|
| `to` | Yes | Recipient. Use E.164 format with the leading `+`, for example `+61412345678`. Local formats are parsed but not recommended. |
| `from` | Yes | A sender ID your account may use: a dedicated number, a verified phone number, or a verified alphanumeric sender ID. Use `SHARED_NUMBER` to send from the shared pool. List yours with [List sender IDs](/api/account/list-sender-ids/). |
| `body` | Yes, unless sending MMS media | Up to 1600 characters. Unicode is accepted. |
| `campaign` | No | Campaign name shown in reports. Defaults to `API Campaign (<date>)`. |
| `reference` | No | Your reference, included in reports and delivery receipts. |
| `metadata` | No | Key/value pairs returned with delivery receipts and replies. See [Metadata](#metadata). |
| `date` | No | Schedule for later. See [Scheduling](#scheduling). |
| `media` | No | Image URL to send as MMS. See [MMS](#mms). |

The [API reference](/api/sms/send/) has the full schema.

## Batching

You can send up to **1000 messages** per request. Each message is validated on its own: if some fail, the valid ones are still sent and the failures come back in `data.errors`. See [Per-message errors](#per-message-errors).

For larger sends, split them across several requests and keep within your [rate limit](/concepts/rate-limits/).

## Metadata

Attach `metadata` to a message to tie it back to records in your own system. It is returned in the `message_dlr` webhook and in `message_received` webhooks for replies to that message. See [Webhooks](/guides/webhooks/).

```json
{
  "to": "+61412345678",
  "from": "SHARED_NUMBER",
  "body": "Your appointment is tomorrow at 10am. Reply Y to confirm.",
  "metadata": {
    "appointment_id": "c5969b2c-d33a-402d-9d7e-f82c98f85be7",
    "clinic": 42
  }
}
```

- Up to 10 key/value pairs per message.
- Keys are strings of up to 50 characters.
- Values are strings or integers of up to 100 characters.

Invalid metadata rejects that message with `INVALID_METADATA`.

## Scheduling

Set `date` to send a message later. Use ISO 8601 in UTC, for example `2025-02-17T22:52:07Z`. The date must be in the future and no more than three years ahead, or the message is rejected with `INVALID_SCHEDULE`.

Scheduled messages are charged when they are sent, not when you submit them.

## MMS

To send an MMS, add a `media` array with the URL of one image. Use `metadata.mms_subject` to set a subject of up to 40 characters.

```json
{
  "messages": [
    {
      "from": "+61412345678",
      "to": "+61412345679",
      "body": "Here's your ticket",
      "metadata": { "mms_subject": "Your ticket" },
      "media": ["https://example.com/ticket.jpg"]
    }
  ]
}
```

- **One image per message**, publicly accessible, up to 1 MB. JPEG, PNG and GIF are supported.
- **Send from a dedicated number** that supports MMS. Alphanumeric sender IDs and `SHARED_NUMBER` can't send MMS.
- **Australian recipients only.** MMS to other countries is rejected.
- Keep MMS batches small: send no more than 10 MMS per request.

Failures are reported per message with `INVALID_MEDIA`, `INVALID_SENDER_ID` or `INVALID_RECIPIENT`. To find sender IDs that can send MMS to a given number, use [List sender IDs for a number](/api/account/list-sender-ids-for-number/).

## Request-level options

These fields go at the root of the request body, alongside `messages`, and apply to every message in it.

| Field | Default | Notes |
|---|---|---|
| `opt_out_list_id` | | Check recipients against this [opt-out list](/guides/opt-outs/). Recipients on it are rejected with `RECIPIENT_OPTED_OUT`. Can't be combined with `opt_out_list_name`. |
| `opt_out_list_name` | | As above, but by list name. |
| `reject_duplicates` | `false` | Drop messages in this request with the same `from`, `to` and `body` as an earlier one. |
| `reject_duplicates_by_recipient` | `false` | Allow only the first message to each `to` in this request, whatever its `from` or `body`. |

If the list you name doesn't belong to your account, the request fails with a `422` rather than silently skipping the check.

### Opt-out list example

```json
{
  "opt_out_list_name": "Marketing Opt-Outs",
  "messages": [
    { "from": "+61412345678", "to": "+61412345679", "body": "20% off this weekend. Reply STOP to opt out." }
  ]
}
```

### Duplicate prevention example

```json
{
  "reject_duplicates_by_recipient": true,
  "messages": [
    { "from": "+61412345678", "to": "+61412345679", "body": "First message" },
    { "from": "+61412345680", "to": "+61412345679", "body": "Second message" }
  ]
}
```

Only the first message is sent, because both go to the same recipient. Duplicate checks only look within the one request.

## The response

Accepted messages come back in `data.messages`, with the same fields you sent plus a `meta` object describing the message:

```json
{
  "status": "VALID",
  "to": "61412345678",
  "from": "SHARED_NUMBER",
  "body": "Your order has shipped.",
  "campaign": "API Campaign (2025-07-05)",
  "reference": null,
  "date": null,
  "meta": {
    "size": 23,
    "parts": 1,
    "is_unicode": false,
    "is_mms": false,
    "cost": 1,
    "country": "AU"
  }
}
```

`parts` is the number of SMS segments the body needs. Long messages and messages containing Unicode use more parts.

`cost` is the number of credits the message uses.

Acceptance means the message is queued, not delivered. Subscribe to the `message_dlr` [webhook](/guides/webhooks/) to track delivery.

## Per-message errors

Rejected messages come back in `data.errors`, with the fields you sent plus `error_code` and `error_help`:

```json
{
  "status": "ERROR",
  "error_code": "INVALID_SENDER_ID",
  "error_help": "MMS messages must be sent from a dedicated number.",
  "to": "+61412345679",
  "from": "SHARED_NUMBER",
  "body": "Here's your ticket",
  "media": ["https://example.com/ticket.jpg"]
}
```

Log `error_help`: it explains in plain language how to fix the problem.

| Code | Meaning |
|---|---|
| `INVALID_BODY` | The body is missing, empty or not valid UTF-8. |
| `MESSAGE_TOO_LONG` | The body needs more than 30 SMS parts. |
| `INVALID_MEDIA` | MMS media failed validation. See [MMS](#mms). |
| `INVALID_METADATA` | Metadata failed validation. See [Metadata](#metadata). |
| `INVALID_RECIPIENT` | The recipient is invalid or couldn't be parsed. Use E.164 format where possible. |
| `INVALID_SCHEDULE` | The schedule date is invalid, in the past, or too far ahead. |
| `INVALID_SENDER_ID` | The sender ID is invalid, or can't be used for this message (for example, MMS from a non-dedicated number). |
| `UNAPPROVED_SENDER_ID` | The sender ID isn't approved for your account. Register it under **Settings** > **Sender IDs** in [touchSMS](https://app.touchsms.com.au). |
| `RECIPIENT_OPTED_OUT` | The recipient has opted out. See [Opt-outs](/guides/opt-outs/). |
| `DUPLICATE_MESSAGE` | Rejected by `reject_duplicates` or `reject_duplicates_by_recipient`. |

Errors that apply to the whole request, such as authentication or validation of the request shape, use HTTP status codes instead. See [Errors](/concepts/errors/).

If your account doesn't have enough credits for the whole request, nothing is sent and the API returns `406`. See [Insufficient balance](/concepts/errors/#insufficient-balance).

## Dry run

[Dry run](/api/sms/send-dry-run/) (`POST /v2/sms/dry`) takes the same body as Send SMS and runs the same validation, but sends nothing and charges nothing. Use it to:

- check numbers, sender IDs and bodies before a large send,
- preview how many parts each message needs.

The dry run response also includes the total credit cost and whether your balance covers it.

The response has the same shape as a real send, without message IDs.

## Templates

Templates you save in [touchSMS](https://app.touchsms.com.au) or through the API are available through [List templates](/api/templates/list/). You can also [create](/api/templates/create/), [update](/api/templates/update/) and [delete](/api/templates/delete/) them over the API.

To send one, read its `message` and pass it as `body`, and use its `sender_id` as `from`. Merge fields in the template are not filled in for you, so replace them with your own values before sending.

Source: https://dev.touchsms.com.au/guides/sending-messages/index.mdx
