Open in
SMS
Send and inspect SMS messages through your connected VoIP carrier account.
SMS uses a number on your organization's connected Twilio or Telnyx account.
ThunderPhone-owned numbers cannot send SMS. Your carrier account must have the
required messaging profile, registration, consent, and funds.
Every text honors your organization's opt-out list:
a text to a number opted out of sms is refused with 409
contact_opted_out, and so are the agent tool's texts, follow-ups and
retries. ThunderPhone does not handle inbound STOP replies in this release;
the person's carrier and your carrier configuration enforce those. When the
carrier refuses a text because the recipient opted out with it, the number is
recorded on the opt-out list as a carrier row.
To send a text later, such as an appointment reminder, use scheduled sends.
A US local number on Twilio can send only once it is registered for A2P 10DLC:
the number must be in a Twilio Messaging Service whose A2P campaign Twilio has
approved (VERIFIED). US carriers block texts from unregistered local numbers.
ThunderPhone sends Twilio messages through the number's Messaging Service, and
the phone number's sms_registration_status and sms_not_ready_reason say
what is missing. Toll-free numbers are not checked for toll-free verification.
A carrier capability failure is cached for at most five minutes. After fixing a Twilio capability or registration, or attaching a Telnyx messaging profile, retry after that window; the next configuration or send check refreshes the negative result. A number waiting on its 10DLC registration is also rechecked hourly.
An accepted response means the carrier accepted the request. It does not
confirm handset delivery. For Twilio senders, ThunderPhone records Twilio's
later delivery report on the message: delivery_status (queued, sending,
sent, delivered, undelivered or failed), delivered_at or
delivery_failed_at, and Twilio's numeric code in provider_error_code. An
undelivered or failed report also sets error_code and error_message and
emits sms.failed; status stays accepted. Telnyx delivery receipts and
inbound messages are not part of this release.
Endpoints
| Method | Path | Description |
|---|---|---|
POST | /v1/sms | Send an SMS |
GET | /v1/sms | List SMS messages |
GET | /v1/sms/{message_id} | Retrieve one SMS message |
OAuth clients need sms:write to send and sms:read to list or retrieve.
Send requests are limited per organization. The default is 30 requests per
minute across REST, agent-tool, post-call follow-up, campaign pre-call and
scheduled sends. Each call is
also limited to three requests that reach the shared send service, including
validation, capability, and carrier failures. Tool requests rejected before
that service, such as a malformed argument or unauthorized label, do not use a
slot.
Send a message
curl -X POST https://api.thunderphone.com/v1/sms \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Idempotency-Key: appointment-confirmation-123" \
-H "Content-Type: application/json" \
-d '{
"from_number": "+15551234567",
"to": "+14155550199",
"body": "Your appointment is confirmed for Tuesday at 2 PM."
}'| Field | Type | Required | Description |
|---|---|---|---|
from_number | E.164 string | yes | Active SMS-capable number on this organization's connected Twilio or Telnyx account |
to | E.164 string | yes | Recipient |
body | string | yes | Non-empty plain text, up to 10 SMS segments |
Idempotency-Key is a required request header containing 1–128 characters.
It is scoped to your organization. Replaying the same key with the identical
sender, recipient, and body returns the original outcome without sending
again. Reusing it with different content returns 409 Conflict.
The body is capped at 1600 characters and 10 encoded SMS segments. The segment limit is usually reached first.
Returns 201 Created when the carrier accepts the message. The response
includes status: "accepted", the carrier provider_id, and segment_count.
Carrier acceptance is not a delivery receipt.
An identical idempotent replay may return 202 Accepted while the original
durable message is still pending. Poll GET /v1/sms/{message_id} for its
terminal outcome; the replay never starts a second send.
Permanent carrier rejection returns an actionable error and the durable failed message record when an attempt reached the carrier. Stable error codes include:
| Code | Meaning |
|---|---|
messaging_not_registered | The customer sender needs carrier messaging registration (such as US A2P 10DLC) or a messaging profile |
recipient_opted_out | The carrier refused the text because the recipient opted out from this sender |
contact_opted_out | 409: the recipient is on your organization's opt-out list for texts. Nothing was sent |
invalid_number | A sender or recipient is not valid for SMS |
not_sms_capable | The carrier does not report SMS support for the sender |
carrier_rejected | The carrier rejected the request for another permanent reason |
carrier_response_invalid | The carrier returned success without a readable message id; status is unknown, so check the carrier log before any manual resend |
delivery_outcome_unknown | The carrier may have accepted the message, but ThunderPhone did not receive or persist a definitive outcome; check the carrier log and do not blindly resend |
rate_limited / carrier_unavailable | A documented carrier non-acceptance or failure proven to occur before request bytes may receive a bounded retry; capability preflight failures share the same attempt bound, while an organization limit does not contact the carrier |
status: "unknown" is terminal. It covers read timeouts, post-send connection
resets, and interruptions after sending may have started but before the outcome
was recorded. ThunderPhone never retries an unknown message automatically.
sms.failed is emitted with status: "unknown" so webhook consumers can
distinguish it from a confirmed carrier rejection.
Before manually resending an unknown message, reconcile it with the carrier;
an empty carrier log alone is not proof of non-delivery, and if the original
attempt cannot be confirmed finished, a resend may create a duplicate. Waiting
for a fixed amount of time does not prove that the original attempt has ended.
ThunderPhone may retry only when it can establish that no carrier request
started. Once a request may have reached the carrier, any unconfirmed outcome
is terminal unknown. HTTP 5xx and gateway responses are also terminal
unknown because they do not prove that the carrier rejected the request.
ThunderPhone never falls back to a platform-owned sender.
Agent recipient permissions
The built-in send_sms tool always permits the other party on a telephone
call. Set sms_allowed_recipients on the agent to authorize up to 10 named
contacts, such as an on-call technician or office manager. Each entry contains
a unique E.164 number and a unique normalized, case-insensitive label;
labels are 1–60 characters and cannot look like phone numbers or contain
control, bidi, or invisible characters. Labels are caller-visible public
metadata. The model sees only labels; the server resolves a label to the
private number from the immutable call-time routing snapshot.
Configured contacts also make the tool available to widget, mic, realtime, and simulation sessions, which have no telephone other party. The call-time recipient list is immutable for that session, so editing the agent does not change an active call's authority.
sms_allow_other_recipients is the broadest tier. When it is true, the agent
can text any valid E.164 number, including any number a caller dictates. Leave
it false when the caller and configured contacts are sufficient.
List messages
curl 'https://api.thunderphone.com/v1/sms?status=accepted&limit=50' \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"Optional filters are status, direction, from_number, to, call_id,
and agent_id. limit is 1–200 and offset is at most 100000. Results are
newest first and scoped to the authenticated organization.
source says what sent the message: api, agent_tool, followup (a
post-call follow-up), pre_call (a campaign's
text before calling) or
scheduled (a scheduled send).
Pre-call texts are never retried automatically.
{
"count": 1,
"results": [
{
"id": "313a3117-827b-46e9-a01a-ac1493838724",
"from_number": "+15551234567",
"to": "+14155550199",
"body": "Your appointment is confirmed for Tuesday at 2 PM.",
"direction": "outbound",
"source": "api",
"status": "accepted",
"provider": "twilio",
"provider_id": "SM0123456789abcdef0123456789abcdef",
"segment_count": 1,
"error_code": "",
"error_message": "",
"call_id": null,
"agent_id": null,
"attempt_count": 1,
"attempt_started_at": null,
"next_attempt_at": null,
"accepted_at": "2026-09-19T15:42:10.113Z",
"delivery_status": "delivered",
"delivered_at": "2026-09-19T15:42:13.402Z",
"delivery_failed_at": null,
"provider_error_code": "",
"created_at": "2026-09-19T15:42:09.901Z",
"updated_at": "2026-09-19T15:42:10.113Z"
}
]
}Retrieve a message
curl https://api.thunderphone.com/v1/sms/313a3117-827b-46e9-a01a-ac1493838724 \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"Returns the same message object, or 404 when the id is not in the
authenticated organization.