Skip to main content

Per-language recipes

The Quickstart and each SDK page show you a first send. This guide collects the four ways that loop completes on a server integration — send a message and consume its delivery webhook, batch-send with an idempotent backoff loop for 429s, run the OTP send/check round trip, and read paginated usage/deliverability — each with a cURL tab plus Python, Go, Ruby, PHP, Java, and C#. Each snippet ends by accessing the response fields idiomatically (dict access in Python, map access in Go, hash dig in Ruby, array access in PHP, result.get(...) in Java, TryGetProperty in C#). A second section below — Client-side loops — covers the mobile and browser SDKs (push device tokens, in-app chat, web personalization). Where the SDK’s typed scope stops, the language’s escape hatch (client.request / client.Request / $client->request / client.RequestAsync) takes over, and the per-language round trips on batch sends carry a pinned Idempotency-Key, so a retry reads as the sender’s own loop rather than an outright retry — the same provision the rest of the coding surface accepts.
Typed helpers exist on every SDK for messaging (send), voice, and verify send/check. The batch send, the analytics read, and the DLR webhook ingest go through each SDK’s generic request helper — client.request / client.Request / $client->request / client.RequestAsync — against the canonical POST /api/v1/messages/batch and GET /api/v1/analytics/messages routes. Scope per language is on the SDK index.

1. Messages — send, then consume the delivery webhook

POST /api/v1/messages/sms returns the message id immediately; delivery then reports back as a webhook event (message.delivered / message.failed on the endpoint you register in Settings → Webhooks). The first snippet of each tab sends and keeps the id; the second is the raw ingest loop the events arrive on — subscription list message.delivered, message.failed, and the per-language signed-body check (verify the X-Orbit-Signature header with the raw body before parsing, same contract as every event; a full handler per language is at Verify webhook signatures). Response fields: data.id (the msg_... handle) and data.status.
UI wiring aside, the DLR itself is a webhook on your registered endpoint — nothing about the send changes; subscribe the endpoint, then teach your handler to keep the message id (from send) through the event’s data.id so retries thread back to the same record. The full event list is Webhook events; the send contract (routing, from overrides, MMS) is on the Messaging API.

2. Batch — bulk-send with a pinned-key 429 loop

POST /api/v1/messages/batch bulk-sends one channel (SMS, WhatsApp, email, RCS, …) to up to 10,000 recipients per call with per-row variables, returning a per-recipient outcome map (HTTP 207 when any row fails) with Idempotency-Key dedup for retries. The snippets below fold the backoff in: an explicit Idempotency-Key pinned on the entire round trip, then on 429 honor the server-named wait (retry_after in the envelope, Retry-After header) before sleeping and re-packing the same key — so the sender’s own loop, not a second-op retry, is what runs on a throttle. Every language below reaches the same canonical route through its escape hatch — client.request in Python/Ruby/PHP, client.Request in Go, client.request in Java, client.RequestAsync in C# — passing /messages/batch relative to the /api/v1 base the SDK prefixes. Response shape: per-recipient data.results[] array with individual statuses and a data.summary counter.
The pinned Idempotency-Key is what makes the loop safe — a retry replays your original 207/200 envelope for 24 h on success codes instead of enqueueing a second batch, so a throttle inside your own loop never double-charges a row. The other failure posture lives below — SDK wide, the same pattern awaits any 429 (send, verify, batch); the full retry-wrapper shape (capped exponential fallback where the server named no wait) is at API error handling by example. The batch contract — per-row variables, the 207 map, RCS/WhatsApp channels — is on the Messaging API.

3. Verify — OTP round trip

Send the code with POST /api/v1/verify/send, keep the returned id, and check what the user typed with POST /api/v1/verify/check. Every SDK wraps both calls typed — the loop closes in two taps either way, and a wrongly-typed or expired code comes back valid: false on the second, never raising for a bad guess (that posture only holds on transport/auth failures). Response fields: data.valid, data.status (approved when the code matches).
Re-send and detail posture are on the same endpoint — approved is the only pass, failed re-checks against the same id until attempts_remaining bottoms out, expired re-sends. The full resend/expire contract is at the Verify API reference; the OTP step-by-step for a fresh install (without an SDK) is at Verify integration without our SDK.

4. Usage — paginated usage/deliverability read

GET /api/v1/analytics/messages returns aggregate delivery metrics (totals + time-series) your invoice and carrier-degradation checks hang off — bucket the series with group_by (hour, day, week, month) and page large ranges by splitting the date window. It has no typed helper in any SDK, so each language reads it through its escape hatch — the same low-level call the languages already use for uncovered routes. Response fields: data.totals.delivery_rate, data.totals.total_sent, data.time_series. Aggregation runs against a read replica, so reporting traffic never contends with the live send path.
All analytics calls share a 60-request/minute budget per tenant and require the analytics:read scope; cursor pagination (meta.pagination) only applies to list endpoints — bucket the rollup time-series with group_by and page by splitting the date window. The per-surface analytics read (the dashboard’s delivery panel pulls the same) and the full parameter matrix are on the Analytics page.

Client-side loops

The four sections above cover the server-language SDKs. The recipes below are the client-side counterpart — the parts of the loop a mobile or browser app runs against Orbit’s end-user surfaces with a publishable key (dv_live_pk_…): register and track a push device token, connect an in-app chat client with a 429-aware send loop, and resolve a personalization slot in the web SDK. Each recipe closes with the escape-hatch provision — for the client SDKs that is the per-call idempotencyKey parameter every send method accepts — and ends by accessing the response or event fields idiomatically (struct fields in Kotlin and Swift, Dart strong types, JS property access in React Native and the browser).
Device-token registration runs in your backend with a secret key — client SDKs never hold one. Each client SDK page — Flutter, Android, iOS, React Native — carries a pre-publish, source-only note; vendor from the monorepo until the first public package publishes.

5. Push — register a device token, then check delivery

Register the device’s FCM/APNs token from your backend with the secret key (POST /api/v1/push/device-tokens on the REST surface; the Node SDK wraps it as orbit.push.tokens.register — an upsert which refreshes last-seen on every token rollover), then list the contact’s tokens (GET /api/v1/push/tokens, or the SDK’s push.tokens.iter iterator) to confirm which platforms became reachable. The mobile tabs show the server-side call the app triggers below — fifth tab = the browser path. The full request/response contract, including DELETE on sign-out, is in the Push reference.

6. Chat — connect, send with optimistic echo, handle 429

Construct the client with a publishable key, subscribe handlers before connect(), and branch on the typed error. A 429 carries the server-parsed Retry-After on a named field — wait it out instead of hammering the send loop; widened slightly per language below, then deduplicated with a pinned key.

7. Web — fetch a personalization slot and inject markup

GET /sdk/personalize resolves one slot for the current visitor: it reads the visitor’s segment off their contact-score record, picks the highest-priority active variant (segment-specific first, slot default as fallback), and returns content / cta_url / cta_text / variant. The web SDK’s getPersonalization(slot) returns a generic empty default on non-OK or timeout — and render(slot, element, { html: true }) only opts into HTML injection when the server-side variant markup passed the markup validation (scripts and inline handlers are rejected with 422 at variant write time, so check the variant save response rather than assuming the slot renders). The end-to-end setup — slots, segments, lift — is in the Personalize website content guide.
Response fields: data.slot, data.segment (null for unknown visitors), data.content, data.cta_url, data.cta_text, data.variant (null when the generic default resolved). Cache guidance: the endpoint sends Cache-Control: private, max-age=60, stale-while-revalidate=120, so browser fetchers may serve a minute-old variant while refreshing in the background.