SMS task recipes: batch, webhook, or dashboard
Most SMS work in Orbit falls into one of three shapes: a one-off batch to a finite list, a durable webhook consumer that reacts to receipts and replies, or a dashboard campaign marketing iterates on. Each shape has a recipe below — the exact recipient format the batch surface accepts, the webhook delivery contract your consumer must survive, and the gates to clear before any of them send. The recipe links out to the deep-dive guide rather than restating it.1. Decision matrix — which path wins
The paths are not exclusive: a campaign launch and a batch both produce per-recipient delivery receipts, and the webhook consumer is how your own system reads them. What changes per path is who drives the send (wizard, API, campaign lifecycle) and who consumes the events (a dashboard, or your server).
2. Batch path — recipient acceptance and the API contract
The Messages → Batch SMS wizard accepts recipients two ways, and the acceptance rules decide the mode the rest of the wizard runs in:- Paste E.164 numbers separated by newlines, commas, or semicolons. Whitespace, dashes, and parentheses are stripped before validation.
- Upload a CSV (≤25 MB, one phone column). The phone column is located by header name —
phone,phone_number,to,number,mobile,telephone, ortelare recognized; a header row that looks like a literal label is skipped, and E.164-shaped first values are treated as data rows. - Per-row messages. If the CSV carries a
message,body,text,content,sms, orsms_bodyheader, the wizard switches to per-row mode: every row brings its own body and the compose step is skipped. Without such a header, a shared template with{{variable}}placeholders applies to every recipient.
+123 is flagged and excluded, never priced), deduplicates the list, and reports valid/invalid counts before you proceed. Before the final click the review step shows, per recipient, the segment count (GSM-7 at 160 characters per segment, 153 multipart; UCS-2 — any emoji or non-Latin character — at 70, 67 multipart) and the per-country cost breakdown priced from the same rate sheet Billing → Pricing shows. The confirm dialog restates sender, valid recipients, and total estimated cost, and sending is gated on a wallet check plus a compliance attestation.
Programmatic sends use the same contract:
- Response shape. HTTP 200 when every recipient is accepted; HTTP 207 (Multi-Status) when any recipient failed. The body carries
batch_id, a per-recipientresults[]withmessage_id,to,status(sent/queued/scheduled/delivered/failed),error_code, anderror_message, plus a summary with succeeded / failed / scheduled totals. Treat 207 as a partial success — readresults[]row by row rather than retrying the whole payload. - Limits. Up to 10,000 recipients per call (per-tenant cap). A per-request
webhook_urlreceives this batch’s DLRs only; for a standing stream, register an endpoint as in the next section. - Semantics. Success rows read submitted (carrier accepted), not delivered — DLRs arrive asynchronously and move each row to Delivered / Submitted (no receipt) / Failed in the SMS list.
Errors — partial success and the wallet gate
- 207 is the per-recipient verdict. Read
results[]row by row and only retry the failed rows — the succeeded rows already have message IDs, so re-sending the whole batch double-sends them. - Wallet gate (402). Before any recipient row is attempted, Orbit checks the prepaid balance covers the channel floor price × recipient count. If the wallet is short, the request returns
402withINSUFFICIENT_BALANCEand no batch is persisted — top up from Billing → Top up and re-send the same payload; no partial debit occurs because the check runs before any message row is inserted.
scheduled_at.
3. Webhook-consumer path — the delivery contract your server must survive
Register a webhook endpoint once and every subscribed event —message.delivered, message.failed, inbound replies — reaches your server. The contract to implement:
- Verify HMAC before anything else. Read the raw request body, check the signature timestamp (reject anything older than 5 minutes), and compare
X-Orbit-Signature(fall back to the legacyX-Devotel-Signature) with a timing-safe HMAC-SHA256 overt.body. The endpoint’s cleartextwhsec_...secret is returned exactly once at registration — copy it then. - Dedupe before work. Delivery is at-least-once; retries re-deliver the same event
id. Persist seen IDs (a table withINSERT ... ON CONFLICT DO NOTHING) for at least 7 days so replayed dead letters don’t double-process. - Ack fast. Return
2xxwithin the 30-second window and hand the payload to your own queue. A slow or failing handler burns retry budget on work your queue would survive. - Survive retries and dead letters. A non-2xx or timeout triggers exponential-backoff retries for roughly 4–5 hours, then the event lands in the dead letter queue, replayable from Webhooks > Failed Deliveries for 7 days.
- Rotate secrets without downtime. During rotation Orbit dual-signs —
X-Orbit-Signaturewith the new secret,X-Orbit-Signature-Nextwith the previous — so a verifier that checks both headers accepts either key through the grace window.
rotation/initiate, rotation/status, rotation/complete), and a signed local test one-liner are all in Build a durable webhook consumer. If only one batch needs receipts, a per-request webhook_url on POST /messages/batch avoids standing up an endpoint at all.
4. Dashboard campaign path — when to invest
A campaign earns its machinery when any of these are true: the audience is a live segment evaluated at send time; you need a draft lifecycle with review before launch; you want a dry-run, holdouts, or A/B variants; or delivery stats and ROI attribution must roll up at campaign level. One payload, one send, no tracking need — that stays a batch. Walk the full draft → audience → launch → analytics flow in Send a campaign end-to-end; both surfaces share the same send path, so the gates below apply identically.5. Data-hygiene chain — before any path
These four are path-independent; run them before sending and revisit them after every import:- Import and migrate contacts — pick the import surface (CSV wizard, async jobs, CDP ingest, or platform connectors), dry-run, map consent, dedupe.
- Opt-out lists — honor STOP keywords and maintain opt-out entries at the right scope.
- Message suppression — keep a tenant-owned suppression list so blocked numbers are filtered at send time.
- DNC pre-flight scrub — scrub recipient lists against do-not-contact entries before launch.
6. Pre-flight gates — why this step is non-optional
Every send — wizard, API batch, or campaign launch — passes tenant-owned admission gates before a single segment reaches a carrier: wallet posture, opt-out and suppression sync health, quiet hours, frequency caps, and the TCPA voice window. A skipped pre-flight pass reads as “admitted, then blocked”: the dashboard accepted the send, the gates rejected rows downstream, and the budget is spent on failures the checklist would have caught. Run Send gates: pre-flight checklist before production traffic and count blocked rows after launch from the same guide.7. Local compliance — sender identity per destination country
- Sender ID country matrix — which sender types (alphanumeric, numeric, short code, long number) each destination supports and what registration each requires.
- US traffic: complete 10DLC registration before sending from a US long number; keep 10DLC rejections and re-vet open for campaign-review rejections. Toll-free verification covers US toll-free numbers instead.
8. After the first send — monitor and inspect
- Delivery log — search every delivery by message ID, provider reference, recipient, or sender and work a result row to its answer.
- Insights: deliverability — read the consolidated dashboard across channels to spot degradation before it lands in complaint tickets.
- Smart route preview — call
POST /messages/route-previewto see which channel the router picks, why, and what it costs when the selected channel underperforms or the destination is better served by WhatsApp or RCS.
9. Troubleshooting — the common misreads
- “Delivered batch shows zero events.” The batch wizard and
POST /messages/batchsend but do not register an endpoint for you. Register one (Build a durable webhook consumer) or pass a per-batchwebhook_url, and confirm it acks within its timeout before re-sending test traffic. - “Blocked at a gate” reads as “provider rejected.” A gate is a tenant-owned admission check (wallet, quiet hours, suppression, frequency cap). The pre-flight checklist shows the admission order; count blocked rows after launch in the same guide.
- “Deliverability dashboard is clean but recipients say nothing arrived.” DLRs are per-recipient; check the Delivery log for the specific recipient’s status rather than the aggregate window.
- “The legislation oddly prohibits alphanumeric senders.” That is the destination, not a platform bug — confirm from the Sender ID country matrix and pick a compliant sender type per country.
- “US recipients flagged as blocked despite clean registration.” Check brand/campaign review status in 10DLC rejections and re-vet, not the aggregate dashboards.
- “CSV upload rejects the whole file.” The phone column must use a recognized header (
phone,phone_number,to,number,mobile,telephone,tel) and each row must be a complete E.164 number — amessage/body/textcolumn switches the wizard into per-row mode, where a shared compose body no longer applies.
See also
- Batch SMS — the full wizard walkthrough and
POST /messages/batchreference. - Build a durable webhook consumer — the working receiver, dedupe store, and rotation flow.
- Send a campaign end-to-end — the full campaign lifecycle.
- Messaging services console — bundle sender pool, opt-out list, and inbound webhook under one id when compliance or routing needs a dedicated pool per region or use case.
- SMS sending services — the service-attachment model behind that console.