ThunderPhone 2.0 is live.Self-serve, from 2¢/min.Read the announcement

Phone Numbers

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

MethodPathDescription
POST/v1/smsSend an SMS
GET/v1/smsList 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."
  }'
FieldTypeRequiredDescription
from_numberE.164 stringyesActive SMS-capable number on this organization's connected Twilio or Telnyx account
toE.164 stringyesRecipient
bodystringyesNon-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:

CodeMeaning
messaging_not_registeredThe customer sender needs carrier messaging registration (such as US A2P 10DLC) or a messaging profile
recipient_opted_outThe carrier refused the text because the recipient opted out from this sender
contact_opted_out409: the recipient is on your organization's opt-out list for texts. Nothing was sent
invalid_numberA sender or recipient is not valid for SMS
not_sms_capableThe carrier does not report SMS support for the sender
carrier_rejectedThe carrier rejected the request for another permanent reason
carrier_response_invalidThe carrier returned success without a readable message id; status is unknown, so check the carrier log before any manual resend
delivery_outcome_unknownThe 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_unavailableA 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.