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:- Get an API key
- Buy a number
- Send the SMS
- Track the delivery receipt
- Receive the reply
- Answer in the same thread
- Handle opt-outs
- 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 thesms capability in the request:
- cURL
- Python
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.
- cURL
- Node.js
- Python
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 walksqueued → 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:
5. Receive the reply
Replies to your number arrive asmessage.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:
- Node.js (Express)
- Python (Flask)
6. Answer in the same thread
Replying is anotherPOST /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.
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-mandatedSTOP / 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 onerror.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
- Buy and provision numbers — bulk orders, polling, regulatory holds, post-purchase wiring
- Send & Receive Messages — the same loop generalized to WhatsApp, RCS, Viber, and Email
- Two-way SMS conversations — thread maps, keyword auto-reply, Inbox upgrade
- First webhook quickstart — tunnel, register, verify, replay
- Webhook tester — replay payloads against your receiver before live traffic
- Opt-out lists — per-brand STOP/HELP/START keywords and copy
- Error codes — every
error.codethe send path can return