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

# Extension SMS

> Two-way texting from a user's voice number — an inbox-style thread list per extension DID with unread badges, compose, and mark-read, from the dashboard or the API.

# 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 the `voice:read` or `voice:write` scope.
* Writing (`POST /send`, `POST /read`) requires an owner, admin, developer, or agent role **and** the `voice:write` scope, server-side. The dashboard's role gate is a UI affordance — an API key with only `voice:read` gets a 403 on `/send` even though reads succeed.

Treat the thread list and message bodies as conversation PII, and scope API keys accordingly.

## 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:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/voice/extension-sms/threads?from=%2B13035550142&limit=50" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```json theme={null}
{
  "data": {
    "from": "+13035550142",
    "threads": [
      {
        "peer": "+12025550113",
        "messageCount": 12,
        "unreadCount": 2,
        "lastMessageAt": "2026-08-31T14:07:21.000Z",
        "lastBody": "Can you confirm the booking for Thursday?",
        "lastDirection": "inbound"
      }
    ]
  },
  "meta": { "request_id": "req_911d1c2a", "timestamp": "2026-08-31T14:08:02.113Z" }
}
```

Page with `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](#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:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/voice/extension-sms/send" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"from": "+13035550142", "to": "+12025550113", "body": "Confirmed for Thursday at 3 pm."}'
```

```json theme={null}
{
  "data": { "id": "msg_4ac1e7d9", "status": "queued" },
  "meta": { "request_id": "req_52ab00f1", "timestamp": "2026-08-31T14:10:44.003Z" }
}
```

The endpoint returns 202 — the message is accepted for delivery, not yet delivered. Poll the thread view (or a delivery webhook) for the final status. An optional `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`** — the `from` number 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.
* **`skipped` status with a `reason`** — a tenant-owned suppression policy accepted but held the send (for example, `duplicate_content` when the exact same body already reached that recipient inside your duplicate-suppression window, or `opted_out`). A `skipped` result releases any consumed monthly quota slot, matching the opt-out path.

The `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:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/voice/extension-sms/read" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"from": "+13035550142", "peer": "+12025550113"}'
```

```json theme={null}
{
  "data": { "from": "+13035550142", "peer": "+12025550113", "markedRead": 2 },
  "meta": { "request_id": "req_77c3d054", "timestamp": "2026-08-31T14:12:09.812Z" }
}
```

`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 `from` against 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 `skipped` with `reason: "duplicate_content"`.

Inbound-only reading (`GET /threads`, `GET /thread`) is unaffected by these gates.

## Extension SMS vs the Messages API

Use the [Messaging endpoints](/api-reference/endpoints/messaging) for broadcast, campaign, and programmatic fan-out sends. Use Extension SMS when the workflow is one operator conversing with one contact from their own number — the inbox metaphor, not the blast metaphor. Both surfaces read and write the same underlying message records, so a conversation started in the dashboard is fully visible to API consumers and vice versa.
