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 requirescalls: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_messageandconsent_basisare required. The sender must be the agent’s existing enabled SMS sender. Unsupported fields get a field-named 400, not a silently ignored setting. Optionaltimezone(defaultAmerica/Toronto) andwindow(HH:MM-HH:MM, default09:00-18:00) are the recipient’s sending hours, as for calls. Outside them nothing is stored or sent and the answer is 409 withreason: 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 answersstatus: refusedwitherror: quiet_hours, and nothing reaches the carrier.POST /v1/sms/batch:globaldefaults andmessages, 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 stablebatch_idand each message’s actual outcome. Each row is held to its owntimezoneandwindow; 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}/messagessupportstakeandskip.GET /v1/sms/conversations/{conversation_id}/policyreturnsthread,state,consent(not_opted_out,opted_out, orunreadable),do_not_contact,can_send, a refusalreason,max_body_chars, andsending_hours(the default recipient timezone and window, andhuman_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-outwith 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}/resumeandPOST /v1/sms/conversations/{conversation_id}/enduse the existing human/AI/ended state transitions. An ended thread cannot send or resume.POST /v1/sms/conversations/{conversation_id}/messages:message,consent_basis, optionalsender(human, the default, oragent). A human must take over first; an agent cannot send while a human holds the thread. Anagentreply obeys the same optionaltimezoneandwindowas a send, and outside them nothing is stored or sent (409,reason: outside_sending_hours). Ahumanreply in a live thread is exempt, and those two fields do not apply to it.
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.
The answer carries
pagination beside data: {"currentPage", "pageSize", "totalItems", "totalPages"}. Each thread reads:
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).
Delivery states
When Sempera’s servers haveSEMPERA_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
handoffishumanorendednothing 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.