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

# SMS integration loop: send, deliver, reply, and recover

> Build and test the complete SMS integration loop with POST /messages/sms, delivery webhooks, inbound replies, and a compensating rollback when delivery fails.

# SMS integration loop: send, deliver, reply, and recover

An SMS integration is more than a successful `POST`. Your application must retain the message id, consume delivery receipts, accept an inbound reply, and recover when the carrier rejects or never confirms delivery. This walkthrough puts those steps in one runnable loop.

Use a sandbox key first. The examples use the API base URL below and an SMS-capable number you own as `from`.

## 1. Map the endpoints

Keep these routes together in your integration:

| Job | Method and path | Your application owns |
| - | - | - |
| Send an SMS | `POST /api/v1/messages/sms` | Request body, idempotency key, and `data.id` |
| Inspect a message | `GET /api/v1/messages/:message_id` | Correlation and display of the latest stored status |
| Receive lifecycle events and replies | Your HTTPS webhook URL | Signature verification, deduplication, and fast acknowledgement |
| Register the webhook | `POST /api/v1/webhooks` | URL and event subscriptions |
| Send a reply | `POST /api/v1/messages/sms` | Swap the conversation endpoints and keep the same sender |

All API calls use one base URL. The key selects sandbox or live mode; there is no separate sandbox host.

```text theme={null}
https://api.orbit.devotel.io/api/v1
```

The canonical loop is:

```text theme={null}
POST /messages/sms
  → 202 { data.id, data.status, data.segments }
  → message.sent / message.delivered / message.failed
  → message.received (the recipient replies)
  → POST /messages/sms (your reply)
  → compensate your business action if delivery fails
```

For the full cross-channel comparison, see [Send & Receive Messages](/guides/send-receive-messages). This page narrows that pattern to SMS.

## 2. Start in the sandbox

Create a `dv_test_sk_...` key and register your webhook with it. Sandbox traffic is simulated, free, and deterministic. Use the same request body and webhook code in production; when the loop passes, replace the key with a `dv_live_sk_...` key and use a live SMS-capable number.

A public HTTPS receiver is required. Subscribe to all four lifecycle names used by the message event family, plus inbound replies:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/webhooks \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.example/webhooks/orbit",
    "events": [
      "message.sent",
      "message.delivered",
      "message.failed",
      "message.received"
    ]
  }'
```

Store the signing secret returned by registration. Verify `X-Orbit-Signature` against the raw request body, deduplicate on the event `id`, and return `200` quickly before doing application work. Delivery is at-least-once, so a retry must not create a second reply or a second rollback.

Before switching to live, run the four sandbox cases in [The test](#5-the-test). A live key does not make the magic-number recipients safe: in live mode they are real destinations, not simulations.

## 3. Send the message

Send with `POST /messages/sms` and keep the response's `data.id` as your correlation key. The API returns `202 Accepted` when Orbit has persisted and queued the message; it is not a delivery confirmation.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/sms \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1234-sms-v1" \
  -d '{
    "to": "+15005550002",
    "from": "+18005551234",
    "body": "Your order 1234 has shipped. Reply STATUS for tracking."
  }'
```

A successful response resembles:

```json theme={null}
{
  "data": {
    "id": "msg_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
    "status": "queued",
    "channel": "sms",
    "direction": "outbound",
    "segments": 1
  },
  "meta": {
    "request_id": "req_xyz789",
    "test_mode": true
  }
}
```

### Body fields

* **`to`** is the recipient in E.164 format, such as `+14155552671`. Normalize user-entered numbers with the [E.164 formatter tool](https://orbit.devotel.io/en/tools#e164-formatter) before sending.
* **`from`** is your owned, SMS-capable number. It is optional when Orbit can choose an eligible sender, but pass it explicitly when you need stable two-way threading.
* **`body`** is the message text. It must be within the API payload-character limit. Its encoding determines how many carrier segments the message uses.
* **`Idempotency-Key`** is a caller-generated key for safe retries. Reusing the key with the same body returns the original result; changing the body with the same key is rejected. Never use a new key to blindly retry a send after an uncertain timeout until you have reconciled the original request.

The response's **`segments`** is the number of billable SMS segments calculated for this body. GSM-7 fits 160 characters in one segment and 153 characters per segment after concatenation. UCS-2 fits 70 characters in one segment and 67 per segment after concatenation. One emoji, smart quote, or other character outside GSM-7 can pivot the entire message to UCS-2. Preflight the rendered text with the [SMS segment and cost calculator](/guides/sms-calculator-tool), including substituted names and footers.

For the complete error envelope, field-level fixes, and retry decisions, see [API error handling by example](/guides/error-handling-examples). A `422` is a caller correction, not a delivery failure; an accepted send waits for its DLR.

## 4. Wire delivery and reply webhooks

### Delivery receipts

Subscribe to the per-channel DLR events in [Wire delivery-report webhooks per channel](/guides/wire-dlr-webhooks-per-channel):

* `message.sent` means the send was accepted downstream. It is not delivery.
* `message.delivered` means the carrier confirmed delivery to the handset.
* `message.failed` means a terminal failure such as a handset rejection, block, expiry, or no-receipt outcome. Read `data.status`, `error_code`, and `error_message` when present.

Correlate each event on `data.message_id`, not arrival order. SMS does not produce a read receipt, so do not wait for `message.read` to declare an SMS delivered. The DLR guide explains channel-specific windows and late correcting receipts.

A delivery event has this shape:

```json theme={null}
{
  "id": "evt_abc123",
  "type": "message.delivered",
  "data": {
    "message_id": "msg_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
    "channel": "sms",
    "status": "delivered",
    "is_terminal": true
  }
}
```

### Inbound replies

The [Send & Receive Messages](/guides/send-receive-messages) guide is the canonical inbound route. A reply arrives as `message.received` on the same webhook; it is a new inbound message, not a response to the original HTTP request. Match the direction-aware `from`/`to` pair, normalize both numbers to E.164, and deduplicate on the event id or inbound message id.

A minimal handler should do this in order:

1. Verify the signature over the raw body.
2. Deduplicate the event id.
3. Persist the inbound message and associate it with the conversation pair.
4. Return `200`.
5. Queue any business action or reply.

Reply by calling `POST /messages/sms` again with the endpoints swapped. Keep the same `from` number so the handset keeps one conversation thread. Never send outbound SMS through a different provider or bypass the Devotel softswitch.

## 5. The test

Run these four cases with a sandbox key before you flip to live. The trailing digit selects the simulation, as documented in the [sandbox magic numbers playbook](/guides/sandbox-magic-numbers-playbook).

| Recipient | Expected coverage | What to assert |
| - | - | - |
| `+15005550001` | `sent` only | Your consumer records acceptance and does not invent `delivered` after the DLR window expires. |
| `+15005550004` | `failed` | Your failure branch runs even though no `sent` event precedes a pre-submit failure. |
| `+15005550002` | `delivered` | Your terminal-success branch correlates the DLR to the returned `data.id`. |
| `+15005550006` | `read` negative cover | SMS must not emit `message.read`; your handler treats the missing read event as expected, not as a delivery error. |

Use a separate test to exercise `message.received`: have a sandbox handset or the sandbox inbound recipe send a reply to your number, then assert signature verification, deduplication, thread attribution, and your reply send. The [first SMS, end to end](/guides/first-sms-end-to-end) guide is the longer setup path if you still need a number or API key.

### Roll back a failed business action

SMS cannot be recalled after Orbit accepts it, and there is no API call that turns a delivered message back into an undelivered one. Roll back the **business action attached to the message**, not the carrier event:

* Store your order or notification state as `pending_sms` with the `message_id`.
* On `message.delivered`, mark the notification complete.
* On `message.failed` or an expired no-receipt outcome, mark the action for retry or compensation according to your own tenant policy.
* Retry only with a new idempotency key after correcting the cause or choosing a new recipient. Do not send a duplicate merely because a webhook was retried.
* If a late delivered receipt corrects a prior terminal event, accept the correction and reconcile your local state by `message_id`.

This rollback is a tenant-owned control: your application decides whether to cancel an order, release a reservation, notify an operator, or queue a retry. Keep the provider status immutable in your audit trail.

## Next steps

* [Your first SMS, end to end](/guides/first-sms-end-to-end) — provision a number and build the longer first-send path.
* [Send & Receive Messages](/guides/send-receive-messages) — canonical inbound webhook and reply flow.
* [Wire delivery-report webhooks per channel](/guides/wire-dlr-webhooks-per-channel) — event semantics, signatures, sandbox rehearsal, and debugging.
* [API error handling by example](/guides/error-handling-examples) — validation, rate limits, and retryable failures.
* [SMS segment and cost calculator](/guides/sms-calculator-tool) — preflight encoding, segments, and cost.
* [E.164 formatter tool](https://orbit.devotel.io/en/tools#e164-formatter) — normalize numbers before the send.


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