All outbound messages go through one endpoint, Send SMS (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.
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. |
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. |
date |
No | Schedule for later. See Scheduling. |
media |
No | Image URL to send as MMS. See MMS. |
The API reference 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.
For larger sends, split them across several requests and keep within your rate limit.
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.
{
"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.
{
"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_NUMBERcan’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.
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. 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
{
"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
{
"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:
{
"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 to track delivery.
Per-message errors
Rejected messages come back in data.errors, with the fields you sent plus error_code and error_help:
{
"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. |
INVALID_METADATA |
Metadata failed validation. See 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. |
RECIPIENT_OPTED_OUT |
The recipient has opted out. See 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.
If your account doesn’t have enough credits for the whole request, nothing is sent and the API returns 406. See Insufficient balance.
Dry run
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 or through the API are available through List templates. You can also create, update and 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.