Skip to main content

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, or tel are 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, or sms_body header, 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.
After either input the wizard validates each row as a complete, routable E.164 number (a partial like +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-recipient results[] with message_id, to, status (sent / queued / scheduled / delivered / failed), error_code, and error_message, plus a summary with succeeded / failed / scheduled totals. Treat 207 as a partial success — read results[] row by row rather than retrying the whole payload.
  • Limits. Up to 10,000 recipients per call (per-tenant cap). A per-request webhook_url receives 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 402 with INSUFFICIENT_BALANCE and 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.
The full wizard walkthrough — drafts and resume, scheduling, the destination resolver, post-send failure tables — lives in Batch SMS, with scheduling semantics in Schedule one-off sends with 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:
  1. 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 legacy X-Devotel-Signature) with a timing-safe HMAC-SHA256 over t.body. The endpoint’s cleartext whsec_... secret is returned exactly once at registration — copy it then.
  2. Dedupe before work. Delivery is at-least-once; retries re-deliver the same event id. Persist seen IDs (a table with INSERT ... ON CONFLICT DO NOTHING) for at least 7 days so replayed dead letters don’t double-process.
  3. Ack fast. Return 2xx within 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.
  4. 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.
  5. Rotate secrets without downtime. During rotation Orbit dual-signs — X-Orbit-Signature with the new secret, X-Orbit-Signature-Next with the previous — so a verifier that checks both headers accepts either key through the grace window.
Working receivers in Node.js (Express) and Python (Flask), the dedupe table, rotation calls (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

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-preview to 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/batch send but do not register an endpoint for you. Register one (Build a durable webhook consumer) or pass a per-batch webhook_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 — a message/body/text column switches the wizard into per-row mode, where a shared compose body no longer applies.

See also