Send messages
POST /v1/messages sends a single SMS or MMS. The same endpoint handles both; attaching media_urls makes it an MMS.
Request
| Field | Type | Required | Notes |
|---|---|---|---|
from |
string | Yes | E.164 number on your account, attached to an approved campaign |
to |
string | Yes | E.164 destination |
body |
string | Conditional | Required unless media_urls is present. Max 1,600 characters |
media_urls |
array | No | Up to 10 publicly reachable HTTPS URLs |
campaign_id |
string | Conditional | Required for 10DLC. Inferred from the number if omitted |
reference |
string | No | Your own identifier, returned on every webhook for this message |
fallback_to_sms |
boolean | No | For MMS only. Delivers text with a link if MMS is unsupported |
validity_period |
integer | No | Seconds before an undelivered message expires. Default 86400 |
curl https://api.anthonyoliva.com/v1/messages \
-u "$AO_KEY_ID:$AO_KEY_SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8f14e45f-ea4c-4f52-a0b1-2c9e7d3b6a10" \
-d '{
"from": "+13155550142",
"to": "+13155550188",
"body": "Anthony Oliva: your appointment is Thursday at 2pm. Reply STOP to cancel.",
"campaign_id": "C7X4M2P",
"reference": "appt-88213"
}'
Response
{
"id": "msg_01J8ZQ4X7K2M9N3P",
"status": "queued",
"direction": "outbound",
"from": "+13155550142",
"to": "+13155550188",
"body": "Anthony Oliva: your appointment is Thursday at 2pm. Reply STOP to cancel.",
"segments": 1,
"encoding": "GSM-7",
"campaign_id": "C7X4M2P",
"reference": "appt-88213",
"price": { "amount": "0.0049", "currency": "USD" },
"created_at": "2026-07-27T18:02:11Z"
}
Status lifecycle
| Status | Meaning |
|---|---|
queued |
Accepted by us, waiting for throughput |
sent |
Handed to the carrier |
delivered |
Carrier confirmed handset delivery |
undelivered |
Carrier accepted then failed to deliver. Reason in error_code |
failed |
Rejected before reaching the carrier. Reason in error_code |
expired |
Validity period elapsed while queued |
sent is not delivered. Treat only delivered as confirmation, and treat the absence of a receipt as unknown rather than as success.
Segments and encoding
We detect the encoding from the body and return both the encoding and the resulting segment count.
| Encoding | Single segment | Each concatenated segment |
|---|---|---|
| GSM-7 | 160 characters | 153 characters |
| UCS-2 | 70 characters | 67 characters |
A single emoji, a curly quote, or an em dash forces the whole message to UCS-2 and can more than double your segment count. If your templating pipeline substitutes typographic quotes, that alone can double your bill. Check the segments value on a test send before rolling a template out.
Encoding a compliant message
Carrier filtering is content-sensitive. In practice:
- Lead with your brand name so the recipient knows who is texting.
- Include
Reply STOP to cancelon recurring programs. - Use a branded link domain. Public shorteners such as bit.ly are blocked.
- Do not use unicode lookalikes to dodge filters. It is detected and it damages your sender reputation.
Retrieve a message
curl https://api.anthonyoliva.com/v1/messages/msg_01J8ZQ4X7K2M9N3P \
-u "$AO_KEY_ID:$AO_KEY_SECRET"
List messages
GET /v1/messages supports from, to, status, campaign_id, reference, created_after, created_before, and cursor pagination through starting_after and limit (max 200).
curl "https://api.anthonyoliva.com/v1/messages?status=undelivered&limit=50" \
-u "$AO_KEY_ID:$AO_KEY_SECRET"
Suppression
Sending to a number on your suppression list returns:
{
"error": {
"code": "recipient_opted_out",
"message": "This recipient opted out of campaign C7X4M2P on 2026-07-19T14:22:03Z.",
"status": 422
}
}
This cannot be overridden through the API. If a recipient asks to resubscribe, they must opt in again through a documented flow, and you remove the suppression from the console with a record of the new consent.