Send an SMS
POST /v1/messages
Sends an SMS from one of your numbers. In test mode the message is simulated: it reaches delivered but no text is sent (simulated: true, an X-SimpleSMS-Simulated header, and a notice when to is a real-looking number). The one exception is a send to your own verified phone, which is really delivered when a verification sender is available (simulated: false). from may be omitted when the account has exactly one number for the key's mode (the sandbox number with a test key, the one live sending number with a live key); with none or several the request is refused and the error lists the choices under numbers. Supports the Idempotency-Key header, including with scheduled_at. A live send from a US local number that is not yet linked to an approved registration (sender.state other than active) is delivered only to your verified numbers; to anyone else it answers 403 sender_not_registered with an X-SimpleSMS-Sender-State header (test_only | pending | action_needed).
Send your API key as a bearer token: Authorization: Bearer ssms_sk_.... Test keys run this endpoint against the sandbox; see Authentication for key modes and scopes.
Headers
| Name | Type | Required | Description |
|---|---|---|---|
Idempotency-Key | string | No | Makes retries safe for 24 hours: the same key and body replays the original successful response (with Idempotent-Replayed: true); a different body returns 409. Failed requests release the key. At most 255 characters. |
Request body
JSON (Content-Type: application/json).
| Field | Type | Required | Description |
|---|---|---|---|
to | string | Yes | Example: +15005550006. |
from | string | No | One of your numbers, or a sender pool id (pool_...): SimpleSMS picks a pool member, the same one for each recipient every time. Optional when the account has exactly one number for the key's mode. Example: +15005550100. |
body | string | Yes | At most 1600 characters. |
customer_id | string | No | Optional. Attribute to one of your customers. Defaults to the customer the from-number is assigned to; a number assigned to another customer is rejected (400). |
scheduled_at | string | No | Optional. Future ISO timestamp (or epoch ms), up to 30 days out. The message is queued and the response is a scheduled_message object instead of a message. |
media | array of string (uri) | No | Reserved for MMS; currently returns 400 mms_not_enabled. |
Responses
| Status | Meaning | Body |
|---|---|---|
| 201 | Message accepted | Message |
| 202 | Accepted and queued: a temporary carrier failure is being retried automatically (status queued, next_attempt_at set) | Message |
| 400 | Invalid request | Error |
| 401 | Missing, malformed, or revoked API key | Error |
| 402 | Billable live calls only: the prepaid credit balance cannot cover the call (insufficient_credits); nothing was done or charged | Error |
| 409 | Idempotency conflict | Error |
| 429 | Rate limit or quota exceeded | Error |
| 502 | The carrier permanently rejected the message (carrier_error) | Error |
201: Message fields
| Field | Type | Description |
|---|---|---|
id | string | Example: msg_a1B2c3D4e5F6g7H8. |
object | message | |
to | string | Example: +15005550006. |
from | string | Example: +15005550100. |
body | string | |
direction | outbound, inbound | |
status | queued, sent, delivered, failed, received | |
test | boolean | |
simulated | boolean | True when nothing was handed to a carrier: every test-key message except one sent to your own verified phone. Always false on live messages. A simulated message still reaches delivered. |
notice | string | Only on the response to POST /messages. Present when a test-key send to a real-looking number was simulated: says nothing was sent and how to send for real. |
schedule_id | string | The recurring schedule this message is an occurrence of. Present only when set. |
created_at | string (date-time) | |
media | array of string (uri) | MMS attachments (inbound today). |
sent_by | string | Console user or API key that composed it. |
failure_reason | string | The carrier's raw text for a failed send. Kept for compatibility; see failure. |
attempts | integer | Live sends: carrier submissions so far. More than 1 means it was retried. |
next_attempt_at | string (date-time) | Present while queued for an automatic carrier retry. |
receipt_status | missing | The carrier accepted the message but sent no delivery report within 72 hours. status stays sent. |
customer_id | string | The customer this is attributed to. Present only when set. |
timeline | array of object | Every status transition, oldest first. Inbound messages have a single received. |
timeline[].status | accepted, validated, queued, sent_to_carrier, carrier_accepted, retry_scheduled, delivered, failed, received | |
timeline[].at | string (date-time) | |
timeline[].attempt | integer | Carrier attempt this step belongs to (from sent_to_carrier on). |
segments | integer | Parts the body is split into (GSM-7: 160, then 153 each; UCS-2: 70, then 67). Example: 1. |
encoding | gsm7, ucs2 | |
price | object | What this message costs, in USD. One all-in rate per outbound message; carrier fees are included (a 0 line). Null on messages sent before prices were recorded. Nullable. |
price.total | number | Example: 0.009. |
price.currency | usd | |
price.breakdown | array of object | |
price.breakdown[].label | string | |
price.breakdown[].amount | number | |
destination_carrier | string | The recipient's mobile carrier, when a cached lookup knows it. Nullable. |
failure | Failure | Null unless status is failed. Nullable. |
Errors
| Status | When |
|---|---|
| 400 | Invalid request |
| 401 | Missing, malformed, or revoked API key |
| 402 | Billable live calls only: the prepaid credit balance cannot cover the call (insufficient_credits); nothing was done or charged |
| 409 | Idempotency conflict |
| 429 | Rate limit or quota exceeded |
| 502 | The carrier permanently rejected the message (carrier_error) |
Every error has the same JSON shape, and request_id matches the X-Request-Id response header. Errors lists every code and what to do about it.
{
"error": {
"code": "invalid_request",
"message": "What went wrong, in plain words.",
"param": "the_field",
"request_id": "req_a1B2c3D4e5F6g7H8"
}
}Idempotency
Send an Idempotency-Key header to make retries safe. For 24 hours the same key with the same body replays the original successful response (with Idempotent-Replayed: true); the same key with a different body returns 409. A failed request releases its key.
Examples
curl
curl -X POST "https://api.joinsimplesms.com/v1/messages" \
-H "Authorization: Bearer $SIMPLESMS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2f0e-order-1042" \
-d '{
"to": "+15005550006",
"from": "+15005550100",
"body": "Hello from SimpleSMS"
}'Node.js
import { SimpleSMS } from 'joinsimplesms'; // npm install joinsimplesms
const sms = new SimpleSMS(process.env.SIMPLESMS_API_KEY);
const message = await sms.messages.send({
from: '+15005550100',
to: '+15005550006',
body: 'Hello from SimpleSMS',
});
console.log(message.id, message.status);Python
import os
from joinsimplesms import SimpleSMS # pip install joinsimplesms
client = SimpleSMS(os.environ["SIMPLESMS_API_KEY"])
message = client.messages.send(
to="+15005550006",
body="Hello from SimpleSMS",
from_="+15005550100",
)
print(message["id"], message["status"])