Skip to main content

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: All API calls use one base URL. The key selects sandbox or live mode; there is no separate sandbox host.
The canonical loop is:
For the full cross-channel comparison, see 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:
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. 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.
A successful response resembles:

Body fields

  • to is the recipient in E.164 format, such as +14155552671. Normalize user-entered numbers with the E.164 formatter tool 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, including substituted names and footers. For the complete error envelope, field-level fixes, and retry decisions, see API error handling by example. 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:
  • 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:

Inbound replies

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