Conventions
Requests accept application/json or form-encoded bodies; responses are always JSON. Timestamps are ISO 8601 in UTC. Money is a decimal string in USD with four places. Phone numbers are E.164 with the leading plus. Identifiers are prefixed: msg_, bat_, evt_, cmp_. Every response carries X-Request-Id; quote it when you write to support.
Authentication
Authorization: Bearer sk_live_9f2c...a71eScopes: send (create messages), read (everything GET). A key may carry an IP allowlist; requests from other addresses receive 401 unauthorized. Test keys (sk_test_) behave identically but never send and never bill.
Idempotency
Any POST accepts Idempotency-Key, a string of up to 128 characters chosen by you. Same key and same payload within 24 hours: the original response is returned, with the header Idempotent-Replayed: true. Same key, different payload: 409 idempotency_conflict.
Create a message
POST/v1/messages
| Parameter | Type | Description |
|---|---|---|
| torequired | string | Destination in E.164, for example +14155550142. Unsupported prefixes are rejected before sending. |
| textrequired | string | Message body. GSM-7 or Unicode is detected automatically; up to 6 parts per message. |
| from | string | Sender: alphanumeric (3 to 11 characters, A-Z, 0-9, space) or a rented number. Omit to let the platform pick per destination. Replaced automatically where the route requires it. |
| send_at | datetime | Schedule for a fixed moment in UTC. Mutually exclusive with window. |
| window | object | { local_start, local_end, quiet_hours }. Delivers inside the recipient's local window; quiet_hours is default, off or bypass for transactional traffic. |
| validity | integer | Minutes before an undelivered message expires. Default 2880 (48 hours), maximum 4320. |
| campaign | string | Free tag, up to 64 characters, to group messages in reports and webhooks. |
| reference | string | Your own identifier, up to 128 characters, echoed in responses and webhooks. |
| webhook_url | string | Override the account endpoints for this message only. Must be HTTPS. |
POST /v1/messages
Authorization: Bearer sk_live_...
Idempotency-Key: order-4821-ship
Content-Type: application/json
{
"to": "+14155550142",
"from": "METEOR",
"text": "Your order #4821 has shipped. Track it: smsmtr.io/t/8fK2",
"campaign": "shipping",
"reference": "order-4821"
}{
"id": "msg_9Kd2fQ",
"status": "queued",
"to": "+14155550142",
"from": "METEOR",
"text": "Your order #4821 has shipped. Track it: smsmtr.io/t/8fK2",
"segments": 1,
"encoding": "gsm7",
"price": "0.0084",
"currency": "USD",
"country": "us",
"campaign": "shipping",
"reference": "order-4821",
"created_at": "2026-09-21T11:42:07Z",
"sent_at": null,
"delivered_at": null,
"carrier_code": null
}Create a batch
POST/v1/messages/batch
Up to 1,000 messages per API request, each with its own to, text and optional from, reference. Top-level from, campaign, send_at, window and pace apply to every message that does not override them. The whole batch is accepted or rejected atomically on syntax; individual messages can still be rejected for destination or content and are listed with their reason.
| Parameter | Type | Description |
|---|---|---|
| messagesrequired | array | List of message objects, 1 to 1,000 items. |
| from, campaign, send_at, window | mixed | Defaults applied to each message; see Create a message. |
| pace | object | { per_minute } or { over_hours }: drip the batch at a fixed rate or spread it over N hours. Combines with window. |
{
"batch_id": "bat_3Hq9Zr",
"accepted": 2, "rejected": 1,
"total_price": "0.0214", "currency": "USD",
"messages": [
{ "id": "msg_9Kd2fQ", "to": "+14155550142", "status": "queued", "price": "0.0084" },
{ "id": "msg_9Kd2fR", "to": "+447700900123", "status": "queued", "price": "0.0130" },
{ "to": "+99912345", "status": "rejected", "error": { "code": "unsupported_destination" } }
]
}Scheduling parameters
Three ways to time a send, on single messages and batches. They combine: a window with a pace is the usual setting for marketing.
| Parameter | Type | Description |
|---|---|---|
| send_at | datetime | ISO 8601 in UTC. Everyone receives at that instant regardless of local time. |
| window.local_start | time | HH:MM in the recipient's local time. The campaign opens in each zone when its clock reaches it. |
| window.local_end | time | HH:MM. Messages not sent by then wait for the next day's window. |
| window.quiet_hours | string | default applies the destination's quiet hours, off disables them, bypass marks the traffic transactional. |
| pace.per_minute | integer | Maximum messages per minute for the batch. |
| pace.over_hours | number | Spread the batch evenly over this many hours instead. |
Retrieve a message
GET/v1/messages/{id}
Returns the Message object with its current status, timestamps, price and carrier code when a failure has one.
List messages
GET/v1/messages
| Parameter | Type | Description |
|---|---|---|
| status | string | One of the six statuses. |
| country | string | ISO 3166-1 alpha-2, lower case. |
| campaign | string | Campaign tag. |
| from_date, to_date | datetime | Creation time bounds, inclusive, UTC. |
| limit | integer | 1 to 200, default 50. |
| cursor | string | Opaque cursor from the previous page's next_cursor. |
{ "data": [ { "id": "msg_9Kd2fQ", "status": "delivered", "...": "..." } ],
"next_cursor": "eyJpZCI6Im1zZ185S2QyZlEifQ", "has_more": true }Rates
GET/v1/ratesandGET/v1/rates/{iso}
The public rate card, identical to the pricing page. Each entry has the destination, dial code, price per segment, sender-ID rule and quiet-hours window. Cacheable for an hour; rates change at most once a day. No key required.
{ "country": "gb", "name": "United Kingdom", "dial": "44",
"price": "0.0130", "currency": "USD",
"sender": "alphanumeric", "quiet_hours": { "start": "08:00", "end": "21:00" },
"updated_at": "2026-09-21" }Balance
GET/v1/balance
{ "balance": "1240.60", "currency": "USD", "pending": "0.00",
"low_threshold": "100.00", "updated_at": "2026-09-21T11:40:00Z" }The Message object
| Parameter | Type | Description |
|---|---|---|
| id | string | Unique identifier, msg_ prefix. |
| status | string | queued, sent, delivered, failed, expired, rejected, or test for test keys. |
| to, from, text | string | As sent. from reflects the sender actually used after any replacement. |
| segments, encoding | integer, string | Segment count and gsm7 or ucs2. |
| price, currency | string | Total price for all segments, USD. |
| country | string | Destination ISO code resolved from the number. |
| campaign, reference | string | Your tags, echoed back. |
| created_at, sent_at, delivered_at | datetime | Lifecycle timestamps; null until reached. |
| carrier_code, failure_reason | string | Raw carrier code and its plain-English reason on failures. |
Statuses
- queuedAccepted and priced, waiting for a route slot.
- sentHanded to the carrier. Billing point.
- deliveredHandset confirmed receipt. Final.
- failedCarrier could not deliver;
carrier_codeset. Final. - expiredValidity elapsed without delivery. Final.
- rejectedRefused before sending. Never billed. Final.
Errors
{ "error": { "code": "insufficient_balance", "message": "Balance 0.42 USD is below the message price 0.85 USD", "field": null } }| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Malformed JSON or missing required field; field names it. |
| 400 | invalid_number | to is not a valid E.164 number. |
| 400 | invalid_sender | from is not a valid alphanumeric sender or a number you rent. |
| 400 | text_too_long | More than 6 parts per message. |
| 401 | unauthorized | Missing, revoked, wrongly scoped or IP-restricted key. |
| 402 | insufficient_balance | The message would take the balance below zero. |
| 404 | not_found | Unknown message, batch or country. |
| 409 | idempotency_conflict | Key reused with a different payload. |
| 422 | unsupported_destination | No route for that prefix. |
| 422 | sender_not_allowed | Alphanumeric sender not permitted and replacement disabled. |
| 422 | content_blocked | Text matches the acceptable use blocklist (phishing patterns, impersonation). |
| 429 | rate_limited | Over 50 requests per second per key. Honour Retry-After. |
| 5xx | internal | Our side; nothing was sent. Retry with the same idempotency key. |
Rate limits
50 requests per second per key, measured on a sliding window. Batch requests count as one request each. Responses include X-RateLimit-Limit, X-RateLimit-Remaining and, on 429, Retry-After in seconds.
Changelog
- 2026-09
window.quiet_hoursacceptsbypassfor transactional traffic. - 2026-07 Batch responses list rejected items with their error instead of failing the whole batch.
- 2026-06
referencefield echoed in webhooks. - 2026-04
GET /v1/rates/{iso}added.