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. ThunderPhone does not provide a local inbound STOP handler or suppression list in this release; opt-out enforcement depends on the customer's carrier configuration and carrier rejection behavior.
A carrier capability failure is cached for at most five minutes. After fixing a Twilio capability or attaching a Telnyx messaging profile, retry after that window; the next configuration or send check refreshes the negative result.
An accepted response means the carrier accepted the request. It does not
confirm handset delivery. 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, and post-call follow-up 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 or a messaging profile |
recipient_opted_out | The recipient opted out from this sender |
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.
{
"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",
"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.