Extension SMS
Extension SMS is two-way texting from a user’s voice number. Every SMS where an extension’s DID is sender or recipient is rolled up into an inbox-style conversation list, threaded by the other party — the softphone-texting surface desk-phone platforms ship as table stakes. It is deliberately separate from the bulk Messages API:POST /api/v1/messages is a fan-out send surface with no per-extension inbox, while Extension SMS is the same-person conversation view an operator lives in.
Typical use: a support agent whose softphone rings on +13035550142 also answers customer texts from that same number, without switching tools.
Four endpoints back the surface, under /api/v1/voice/extension-sms:
GET /threads— the inbox: per-peer conversation summaries with unread counts, newest first.GET /thread— one conversation: every SMS exchanged with a single peer, newest first.POST /send— send an SMS from the extension’s DID.POST /read— mark a peer’s inbound messages read.
Access and roles
The dashboard page at Voice → Extension SMS is visible to owner, admin, and developer roles; other roles see an access-restricted notice. The API enforces two layers independent of the page:- Reading (
GET /threads,GET /thread) requires thevoice:readorvoice:writescope. - Writing (
POST /send,POST /read) requires an owner, admin, developer, or agent role and thevoice:writescope, server-side. The dashboard’s role gate is a UI affordance — an API key with onlyvoice:readgets a 403 on/sendeven though reads succeed.
Read the thread list
Open Voice → Extension SMS. The number picker at the top lists the organization’s active phone numbers (SMS-capable numbers sort first) — pick the DID whose inbox you want. The left column is the thread list. Each row is one contact: the peer number, a preview of the most recent message, its relative timestamp, and an unread badge when unread inbound SMS exist for that peer. Rows with newest activity float to the top; the list refreshes roughly every 30 seconds while the page is open. A thread’s unread count covers inbound messages only — your own outbound texts never badge you. Opening a conversation marks it read automatically (see below), and the badge clears on the next refresh. The same roll-up is available over the API:limit (1–100, default 50) and offset. GET /thread then loads one conversation, newest first, up to 200 messages per page. Both endpoints reject a from number the organization does not own as an active DID with a 403 — see Operating limits.
Send an SMS
In the dashboard, open a conversation — or start a new one with New message — and type into the compose box. The composer enforces the API’s 1,600-character ceiling and rejects a blank body or an invalid recipient format. Delivery status appears on the message, and a failed outbound carries a human-readable failure reason (carrier rejection, invalid destination, sender restriction) rather than a bare failed badge. Over the API:media_url attaches one MMS asset (HTTPS URL); blank values are ignored, so serializing an empty field on a plain-text send is safe.
Common send rejections
- 403
FORBIDDEN— thefromnumber is not an active DID the organization owns. Check the number picker; this check runs on all four endpoints. - 422
RECIPIENT_OPTED_OUT— the recipient opted out of SMS; no further sends to them succeed until they opt back in per your organization’s opt-out policy. skippedstatus with areason— a tenant-owned suppression policy accepted but held the send (for example,duplicate_contentwhen the exact same body already reached that recipient inside your duplicate-suppression window, oropted_out). Askippedresult releases any consumed monthly quota slot, matching the opt-out path.
from number is written on every send and the action lands in the audit log, so outbound texting is attributable to the user who sent.
Mark a thread read
Unread counts follow the inbound messages that still have no read marker on them. When you open a conversation in the dashboard, its inbound leg is marked read automatically; the call is idempotent and records which user cleared it, so a re-mark never overwrites the original reader or timestamp. Over the API:markedRead is the count of inbound messages newly marked — 0 if the thread was already read. Like the threads roll-up, only the inbound direction counts; outbound messages are never part of an unread badge.
Operating limits
- Owned, active numbers only. Every endpoint validates
fromagainst your organization’s active phone numbers before reading or sending. Anything else is refused (403), and text-spoofing a released or foreign number is not possible. - Body size. 1–1,600 characters per SMS; longer texts must be split client-side or sent as an MMS attachment via
media_url. - Rate limits. Reads and writes use the standard authenticated read/write rate limits, same as the rest of the voice surface.
- Delivery attribution. Every send writes an audit-log entry with the from number, destination, and acting user.
Compliance and quiet hours
Extension SMS sends go through the same delivery pipeline as every other SMS in your organization, so the tenant-owned gates you have configured apply here exactly as they do for campaign or API sends:- Opt-out lists — a recipient who opted out gets a 422 (
RECIPIENT_OPTED_OUT) instead of a delivery. - Quiet hours — if your organization has opted into per-channel quiet hours (organization settings), an Extension SMS send is evaluated against them like any other send; transactional traffic resolves as transactional and flows 24/7, while sends tagged as marketing are blocked during the restricted window. If you have not enabled quiet hours, sends are unrestricted. Schedule-sensitive texting (for example, appointment reminders an agent sends at night) should be understood against your organization’s quiet-hours settings, not assumed outside them.
- Duplicate-content suppression — if your organization enables the duplicate-message window, a repeated identical body returns
skippedwithreason: "duplicate_content".
GET /threads, GET /thread) is unaffected by these gates.