Skip to main content

Your first SMS, end to end

The SMS building blocks each have their own page — buying numbers, sending, delivery receipts, two-way threading, opt-outs. This one walks the whole arc in order, first message through first reply, so you leave with a working two-way loop instead of five disconnected checkboxes. Build on a sandbox key the whole way through, then swap a live key in for production. You will:
  1. Get an API key
  2. Buy a number
  3. Send the SMS
  4. Track the delivery receipt
  5. Receive the reply
  6. Answer in the same thread
  7. Handle opt-outs
  8. Fix the common first-send failures

1. Get an API key

Create a key in Settings → API Keys and scope it down to messaging: messages:read and messages:write cover every call in this walkthrough. The send step exercises both halves — everything outside the scope 403s — so a narrow key proofs your least-privilege setup from the start. Scope families per product surface are tabulated in Choose and scope API keys. Carry a sandbox key (dv_test_sk_…) while you build; swap a live key (dv_live_sk_…) for production. Sandbox sends are free and simulated, and the delivery receipts are deterministic, so you can exercise the delivered and the failure paths against the same walkthrough. Every request posts to one base URL and carries the key in the X-API-Key header — sandbox is the same host, selected by the key type, not a different domain:

2. Buy a number

SMS needs an SMS-capable sender you own. Search live inventory, then buy with the sms capability in the request:
Passing an explicit capabilities list matters: omit it and Orbit provisions what the carrier advertises, which can leave a voice-capable DID without the SMS flag your send needs. The full buy loop — single purchase, bulk orders, polling, regulatory holds — is in Buy and provision numbers. Numbers reads need numbers:read, purchases numbers:write; mint those on the same key or a second one. If you would rather exercise the path before spending, claim the one free trial number instead — a 24-hour lease from the shared pool that converts to a permanent purchase with POST /numbers/purchase-trial before it lapses. Trial lifecycle is under Number Lifecycle.

3. Send the SMS

POST /messages/sms takes to and body; from is optional (omit it and Orbit picks an eligible sender — pass yours explicitly once you own one). Always send an Idempotency-Key: a retry of the same key and body returns the original response instead of a duplicate.
The response is 202 Accepted — persisted and queued, not yet handed to the carrier. Hold the data.id (a msg_ + 32 hex characters): every follow-up, including the webhooks below, keys off it.

4. Track the delivery receipt

A queued message walks queued → sending → sent → delivered (or terminates in failed / undelivered). Do not poll — register one webhook endpoint subscribed to the lifecycle events, and Orbit POSTs each transition to you:
The created endpoint returns a generated signing secret — store it; you verify every inbound POST against it. A delivered receipt reads:
On a sandbox key those receipts are deterministic, so you can force both the delivered and the failed path without spending. Before you wire a live endpoint, push a test event through the Webhook tester — it replays payloads against your URL so the receiver logic is debugged before real traffic arrives. A full receiver loop (tunnel → register → verify → replay) is in the first webhook quickstart; the receiver below is the same shape pulled in here.

5. Receive the reply

Replies to your number arrive as message.received events on the same endpoint. Inbound routing for numbers you own is automatic — no per-number URL wiring. Here is a minimal receiver that verifies the signature, deduplicates on the event id, and returns 200 before doing any work:
Verify the signature before reading the body, and treat a retried event as a duplicate — that pair is the whole honesty rule for a receiver. Signature verification shapes per language are in Webhooks security.

6. Answer in the same thread

Replying is another POST /messages/sms with the endpoints swapped — there is no reply endpoint and no header to set. Reply on the same from the conversation started with; swapping senders mid-thread splits the conversation into two threads on the customer’s handset.
SMS carries no reply metadata — thread identity comes from the direction-aware endpoint pair (customer → your-number, ordered the same on inbound and outbound). When you outgrow a Map in your receiver, the Two-way SMS conversations guide keys the thread map, covers keyword auto-reply, and plans the Inbox upgrade; both sides of a conversation also land on one conversation_id you can pull via GET /messages.

7. Handle opt-outs

Carrier-mandated STOP / HELP / START handling is on for every tenant by default: a STOP inbound writes a contact opt-out, auto-replies, and every later send to that recipient is rejected at dispatch instead of leaking through. Your receiver’s job is bookkeeping — treat STOP as a terminal thread state — not filtering. Once you send under a second brand or language, add a custom opt-out list: tenant-owned keyword aliases and branded auto-response copy, attached to a messaging service. The aliases widen the trigger surface; the mandatory defaults can never be removed. Register aliases ahead of your first campaign, not after the first unsubscribe.

8. Fix the common first-send failures

Match on error.code; log meta.request_id for support. The first-send failures almost everyone hits once: The remaining send-path codes (INVALID_API_KEY, INVALID_PHONE_NUMBER, RATE_LIMITED, VALIDATION_ERROR) are tabulated with every other code in Error codes.

Next steps