---
title: "SMS"
description: "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

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

<Warning>
  These are real sends, including from public widgets and test/simulation
  sessions. A visitor or tester can ask the agent to text any configured
  contact. The three-request per-call cap and shared organization rate limit
  bound this exposure; use only labels and contacts suitable for callers.
</Warning>

`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

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

```json
{
  "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

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