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. 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

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, 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."
  }'
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 or a messaging profile
recipient_opted_outThe recipient opted out from this sender
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.

{
  "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.