> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sempera.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Text messages (SMS)

> The numbers an agent texts from, its two-way text threads, and how the agent answers an inbound text.

A text sent to a number bound to an agent is stored on a two-way thread and answered by the agent, and a workspace owner can take a thread over in the App. Texting is turned on per agent in the Sempera App, as part of its phone setup (Send text messages), and it starts off. That one setting decides which numbers `GET /v1/sms/numbers` lists, whether the agent answers a text, and whether it sends one during or after a call: a text that reaches any bound number is always stored on its thread, and with texting off the agent never answers it and never texts anyone (below). Public sends use this same SMS path and its limits.

Routes answer in the compatibility envelope `{"data": ..., "errors": null}`. Phone numbers in a thread are masked to their last four digits, exactly as the owner sees them; full numbers never leave the thread.

## Public sends and handoff

Every write below requires `calls:write`, owner outbound permission and an `Idempotency-Key`. Reads require `calls:read`. All routes check workspace and agent scope, including cached idempotent responses. A customer database is required for writes and individual conversation reads.

* `POST /v1/sms/send`: `agent_id`, `agent_number`, `user_number`, `agent_message` and `consent_basis` are required. The sender must be the agent's existing enabled SMS sender. Unsupported fields get a field-named 400, not a silently ignored setting. Optional `timezone` (default `America/Toronto`) and `window` (`HH:MM-HH:MM`, default `09:00-18:00`) are the recipient's sending hours, as for calls. Outside them nothing is stored or sent and the answer is 409 with `reason: outside_sending_hours`. Whatever the window, no text is sent between 9 PM and 8 AM in the recipient's local time (the zone of the number's area code, else the workspace's own time zone): the send answers `status: refused` with `error: quiet_hours`, and nothing reaches the carrier.
* `POST /v1/sms/batch`: `global` defaults and `messages`, a list of 1 to 100 objects overriding those defaults. This is bounded synchronous processing, not an uploaded-file workflow. Every row is validated before sending; the result contains a stable `batch_id` and each message's actual outcome. Each row is held to its own `timezone` and `window`; a row outside its hours is refused and not sent.
* `GET /v1/sms/conversations/{conversation_id}` returns the real thread and messages. `GET /v1/sms/conversations/{conversation_id}/messages` supports `take` and `skip`.
* `GET /v1/sms/conversations/{conversation_id}/policy` returns `thread`, `state`, `consent` (`not_opted_out`, `opted_out`, or `unreadable`), `do_not_contact`, `can_send`, a refusal `reason`, `max_body_chars`, and `sending_hours` (the default recipient timezone and window, and `human_reply_exempt: true`). Not opted out is not proof of opt-in. The send still requires a consent basis and rechecks the current guards.
* `POST /v1/sms/conversations/{conversation_id}/opt-out` with an empty object records a recipient's opt-out request against this thread's sender and returns the same policy shape. It is owner-only, scoped, audited and idempotent. It sends no confirmation text and stays available when launch eligibility disables sends. There is no owner restore action: only an inbound START, UNSTOP or YES restores texting. Suppression covers all sending numbers in that workspace; other recipients and workspaces are unchanged.
* `POST /v1/sms/conversations/{conversation_id}/handoff`, `POST /v1/sms/conversations/{conversation_id}/resume` and `POST /v1/sms/conversations/{conversation_id}/end` use the existing human/AI/ended state transitions. An ended thread cannot send or resume.
* `POST /v1/sms/conversations/{conversation_id}/messages`: `message`, `consent_basis`, optional `sender` (`human`, the default, or `agent`). A human must take over first; an agent cannot send while a human holds the thread. An `agent` reply obeys the same optional `timezone` and `window` as a send, and outside them nothing is stored or sent (409, `reason: outside_sending_hours`). A `human` reply in a live thread is exempt, and those two fields do not apply to it.

Sends obey launch eligibility, texting enablement, sender ownership, limits, write policy, do-not-contact and durable opt-outs. No public send, batch row or conversation reply goes to a verified, unreleased line on Sempera in any workspace (403), which prevents two agents from answering each other without end. An agent also answers at most 20 texts automatically per thread in 24 hours; past that, the thread is flagged for a person. A pending message is persisted before entering the send path; its outcome is recorded afterwards. An uncertain send retains its idempotency reservation without stale takeover. `sent` is provider acceptance, not proof of final delivery. Fixture mode says `simulated` and `sent: false`. A refused or withheld send is recorded honestly instead of reported as delivered. No credentials, full numbers or provider error bodies appear in the results or audit events. No migration is required.

The App's inbox and reply composer use these same public writes with browser session CSRF proof and an idempotency key. A legacy `/api/sms-handoff` reply against the customer database is refused, so it cannot bypass the consent basis, do-not-contact and send-policy checks. Its read/state controls and the offline fixture contract remain available. Successful sends clear a thread's unanswered marker only when no newer inbound message arrived during the send; failed or withheld sends do not dismiss attention.

## Opt-out and help keywords

An inbound message containing only STOP, STOPALL, UNSUBSCRIBE, CANCEL, END or QUIT, ignoring case and surrounding whitespace, opts the sender out of texts from the workspace. Sempera confirms once per inbound message: `You're unsubscribed from <business> texts. Reply START to resubscribe.` Every later agent, campaign, tool or manual send is blocked until the person texts START, UNSTOP or YES. These restore texting and send a confirmation. YES restores only a person who is opted out; from anyone else it is an ordinary message for the agent. Replayed inbound messages do not send another confirmation or undo a newer consent decision. Punctuation and extra words are not stripped when matching.

HELP and INFO return the business name and contact email or phone configured in Settings > Business, without changing consent. If no contact is configured, the business's receiving number is used. These keyword responses do not depend on whether an agent or a person holds the conversation. When the carrier reports that it already handled a keyword, Sempera does not double-reply.

## Numbers

`GET /v1/sms/numbers` (`phone_numbers:read`) lists the numbers the agents this credential can read text from: numbers bound to an agent whose phone setup turns texting on, less any the carrier reports cannot send texts.

Each row is `{"phone_number", "agent_id", "sms_config", "conversation_count"}`. `phone_number` is the bound number in E.164, `conversation_count` counts this workspace's text threads on that number, and `sms_config` is always null: Sempera keeps no per-number texting settings. `channel` defaults to `sms`; any other channel lists nothing, because Sempera offers no other messaging channel.

## Threads

`GET /v1/sms/conversations` (`calls:read`) lists text threads on the agents this credential can read.

| Parameter | Rule |
| - | - |
| `agent_id` | One agent (`agentId` is accepted too); omit for every agent in scope |
| `page` | From 1 to 100000 (default 1) |
| `pageSize` | 1 to 100 (default 50); `page_size` is accepted too |
| `sortDir` | `desc` (default) or `asc`. Threads are ordered by when each thread was created, not by its latest message |

The answer carries `pagination` beside `data`: `{"currentPage", "pageSize", "totalItems", "totalPages"}`. Each thread reads:

| Field | Meaning |
| - | - |
| `id`, `agent_id` | The thread and the agent answering it |
| `user_number`, `agent_number` | The texter's and the agent's numbers, last four digits only |
| `handoff` | `ai` (the agent is answering), `human` (a person took it over) or `ended` |
| `is_active` | False once `handoff` is `ended` |
| `message_count`, `last_message`, `last_message_at` | The thread so far |
| `created_at`, `updated_at` | When the thread began; and the time of its last message, or `created_at` when it has none |
| `effective_channel` | Always `sms` |

`variables`, `current_node_id`, `curr_pathway_id`, `pathway_tags`, `summary`, `citation_variables` and `disposition_tag` are always null: Sempera does not keep them for a text thread.

An invalid `page`, `pageSize` or `sortDir` answers `400` with `{"data": null, "errors": [{"error": "bad_request", "message"}]}`. An agent the credential cannot read answers `404`. A missing credential or scope gets the standard error envelope ([Errors and limits](https://docs.sempera.org/errors-and-limits.md)).

### Delivery states

When Sempera's servers have `SEMPERA_SMS_STATUS_CALLBACK_URL` set (the carrier-facing `/api/telephony/sms-status` address with its telephony token), every text the agent sends asks the carrier to report delivery there. The conversation drawer then shows each outbound text as `accepted` (the carrier took it), `sent` (handed to the phone network), `delivered` (the handset got it) or `not_delivered`. States only move forward, and a callback is matched to a message only inside the workspace that owns the sending number. An `undelivered` or `failed` state marks the thread not answered with a fixed sentence, never the carrier's own error text. Without the setting, texts stay `accepted`.

## Texts an agent sends on a call

Two texts leave during or after a call, both from the agent's own bound number: the in-call text tool (Send a text message, among the agent's tools), which texts the caller a confirmation, an address or a link they asked for, and the Send SMS voicemail action, which texts the message when an answering machine picks up an outbound call. Both obey the same Send text messages setting as an answer to an inbound text. With texting off, or with no phone setup yet:

* **The in-call text is refused and nothing is sent.** The agent is told that texting is off, so it can offer the details on the call instead. This holds in Test and Try it live as well as on a phone call.
* **The voicemail text is not sent.** The call's record says the text was not sent because texting is off, where it records every other reason a voicemail text did not go (no number of the agent's own, a limit reached, sending not allowed on this server). The outbound batch then treats the call as a machine that got no message.

## Who answers a text

An inbound text on a thread the agent holds (`handoff` is `ai`), on an agent whose texting is on, is answered by the agent itself: the same published version, with its own tools, knowledge and model, that answers the typed Test and the website chat. There is no separate texting prompt or model. The carrier is acknowledged at once and the answer is produced afterwards.

* **One conversation per thread.** The first text opens a conversation on the version published at that moment, and every later text on the thread is answered by that same conversation, so a publish in the middle changes nothing for it. The conversation ends after 30 minutes without a text, when the agent ends it, or when the service restarts; the next text opens a new one on whatever is published then, and it does not repeat the opening line. A service holds at most 200 conversations at once, and the least recently texted one ends first.
* **Texting off means the agent does not answer.** With texting off in the agent's phone setup, or with no phone setup yet, an inbound text is stored and shown on its thread like any other, the agent is not asked, and nothing is sent; the thread is flagged as not answered, with that reason, until a person replies or the agent answers a later text. Turned on, texting applies from the next text; a text stored while it was off is never answered later.
* **A person owns a thread they took over.** While `handoff` is `human` or `ended` nothing is asked of the agent. If a person takes a thread over while the agent is composing an answer, that answer is withheld and never sent.
* **One text.** An answer is the agent's own words with whitespace collapsed, cut to 480 characters at the end of a sentence, or at a word with an ellipsis.
* **Tools are write-protected.** An answer's tool calls run under the same protection the typed Test uses, so they do not write to a real system.
* **When the agent cannot answer, nothing is sent.** That covers an agent whose texting is off, an agent with no published version, a model that does not respond or cannot be bound, a turn that fails, a text with no words, and a number whose workspace does not own the agent. The reason is recorded on the caller's text, in one set sentence per kind of failure and never an engine's or a carrier's own error text, and the thread is flagged as not answered in the App until a person replies. There is no canned fallback reply: a fixed sentence sent from the business's own number would read like the agent answering. The API shows the thread's `handoff`; the reasons and the flag are in the App.
* **A texter left waiting needs attention.** An answer the carrier did not deliver leaves the texter waiting too. Each texter left waiting is one urgent item on the agent's Needs attention list, with the reason, however many texts they send before someone answers, and it counts toward the owner's attention alert when that is turned on. Its link opens the thread.
* **The thread in the App.** Opening a text conversation shows every message on the thread, oldest first: who sent it (the texter, the agent, or a person on the team), when, an answer that was not delivered, and why a text was not answered.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.