Skip to main content

REST API recipes

This is the cookbook the SDK index points at: since no SDK is published to a package registry yet, every recipe here is REST-first. The first ten recipes pair curl with the Node SDK (source-only, unpublished); the eight added below (voice, numbers, WhatsApp templates, AI agents, CDP segments, flows, knowledge bases, custom fields) pair curl with raw Python — the SDK modules (client.voice, client.numbers) where one exists, requests against the REST path where scope has not caught up. Between the two languages you can lean on whichever you read faster. Each task is self-contained; run it against the sandbox with a dv_test_sk_… key, then swap in your live key. Authenticate every request with X-API-Key. Base URL is https://api.orbit.devotel.io/api/v1 — sandbox is the same URL with a test key, not a separate host. Full primer: API integration.

Task index

1. Send a message, poll status, receive the webhook

Send one SMS, learn its id, then track delivery two ways: a direct status poll and a delivery webhook. Webhooks are the production answer — poll once to confirm the send works, then move to events. Send:
cURL
Node SDK
A 202 Accepted returns the persisted send in the standard envelope. data.id (msg_…) is your handle; data.status starts as sent (or test_sent in sandbox) and moves as the carrier reports. Poll status once:
cURL
Node SDK
Receive the webhook instead. Register one endpoint in Settings → Webhooks subscribed to message.delivered and message.failed (or * for everything). Orbit POSTs each status change to you; verify the X-Orbit-Signature header before trusting the body — verification is one function call per API integration → Webhooks. Polling is for the first integration smoke test; production consumers subscribe. Canonical pages: Messaging API, Webhook events.

2. Handle a 429 with retry_after

When your send rate passes the per-key limit, the API answers 429 with a body that names the wait inside the envelope (error.retry_after) and again as a Retry-After header. Read whichever your HTTP client exposes; honor it before any fallback backoff — doubling on your own can retry inside the window and re-429.
cURL
Node SDK
The full retry-wrapper pattern (capped exponential fallback when the server didn’t name a wait) lives in API error handling by example; per-channel rates are in API integration → Rate limits.

3. Verify an OTP, end to end

Two requests: send the code, then check what the user typed. The platform generates, delivers, and expires the code; your backend never stores or compares it. Send — capture data.verification_id from the response:
cURL
Node SDK
Check — the user’s answer plus the id from send:
cURL
Node SDK
status: "approved" is the only pass. failed means the code didn’t match; re-check with the same verification_id until attempts_remaining hits 0, then send a fresh one. expired means the send’s TTL lapsed — send again. In sandbox, a test key simulates delivery; find the expected code in the verification’s dashboard row. Canonical page: Verify API. A longer REST-only walkthrough (the same two calls plus webhook fallback): Verify integration without our SDK.

4. List and cursor-paginate

List endpoints return stable cursors — no offsets, so concurrent inserts can’t shift your page. Request the first page without a cursor, then pass meta.pagination.cursor until meta.pagination.has_more is false.
cURL
Node SDK
A wrong or expired cursor is a 422 INVALID_CURSOR — restart the iteration without a cursor; never reuse cursors across requests changed in filters or across retries older than the same-minute window. Full table of cursor-vs-offset endpoints: Pagination.

5. Batch-import contacts, poll the import job

For anything past a few hundred rows, skip per-row POST /contacts calls and enqueue one async import. The endpoint accepts the parsed CSV as a rows array, returns 202 with a job_id, and runs the insert in the background. Poll the job id for progress.
cURL
Node SDK
Poll the job until status is no longer pending/running:
cURL
Node SDK
merge_strategy: "skip" (the default) leaves existing contacts untouched on duplicate keys; "merge" updates them with your new values. A completed row besides the failed ones reports per-row outcomes; rows that failed validation are listed as skipped, not imported as empty records. A 503 IMPORT_QUEUE_UNAVAILABLE means the background queue is briefly down — retry after a few seconds; don’t fall back to hundreds of single creates under load. Canonical pages: Contacts API, Import contacts guide.

6. Score a destination before you send

POST /risk/score fuses the platform’s fraud detectors into one 0–100 verdict you can query before committing an SMS send or verify. It’s read-only and advisory — it never dispatches, so it’s safe to burn before every high-cost send.
cURL
Node SDK
Branch on recommendation: allow — proceed to the send; review — queue for a human only if the send is high-value; block — suppress the send and log the decision. band is the same verdict bucketed; score (0–100) is the worst-signal composite — the per-channel breakdown names which detector fired. This is a gate on your send decision, not a platform block: enforcement stays in your code, and outbound SMS still exits only through the platform. Canonical page: Risk API. The pumping-attack context this protects against: SMS pumping protection.

7. Run a loyalty earn → preview → redeem round trip

Points come from events you already send to the CDP — loyalty computes a member’s balance from your event history when you check it, so there is no separate loyalty ledger to sync. The round trip: dry-run the program with preview, read a member’s balance, then burn points. Preview — evaluate your active program (or an override in the request body) against sample events. Nothing writes to the database, so burn this freely while you tune earn rules:
cURL
Node SDK
Read a member’s balance — points accrue from CDP events your app already emits (track on the CDP API); the read endpoint projects those events into the member’s standing on demand. Two compensation events join order-completed and loyalty.points_redeemed in the ledger: loyalty.points_adjusted (an operator’s manual credit or debit) and loyalty.program_configured (a revision under a config sentinel contact that members never write):
cURL
Node SDK
balance is the spendable remainder; use GET /loyalty/members (paginated) to list every member or a specific id for one. The traits block also exposes loyalty_points_balance, loyalty_tier, and friends exactly as segments and journeys see them. Redeem — burn points atomically. Concurrent redemptions against the same contact serialize, so a double-click can’t overspend:
cURL
Node SDK
Success is 201 Created with the redemption event id and the post-burn balance. An overspend returns 409 INSUFFICIENT_POINTS with the available remainder — burden the retry on a smaller amount or abort. Manual operator adjustments (POST /loyalty/members/:contactId/adjust) share the same overspend-safe path. Canonical page: Loyalty API. The program-design walkthrough this loop plugs into: Loyalty program setup.

8. Open an OAuth connection and pull synced records

The integrations loop is connect → status → data: start the OAuth flow, confirm the connection formed, then read the synced records. Connect and sync trigger are owner/admin-gated; status and data are reads any scoped key can run. Start the connect flow — get the auth_url you redirect the operator to:
cURL
Node SDK
A provider with no OAuth credentials registered fails 404; an integration server misconfigured answers 503. Either is terminal — fix in Settings → Integrations, not in your retry loop. Confirm status — the OAuth popup closing is not proof the connection formed; ask before you read data:
cURL
Node SDK
connected: false resolves cleanly (with an empty sync list) rather than an error, so branch on it without a try/catch. Once connected, the same payload carries the connection’s metadata and per-sync status. Pull synced records — name the model (contacts, deals, …) in the query:
cURL
Node SDK
An empty array is a valid answer — it means the connection is live but the first sync hasn’t landed records for that model yet. An owner/admin kicks an immediate run with POST /integrations/:id/sync { "sync_name": "contacts" } instead of waiting for the scheduled cadence; a 502 on data reads means the upstream fetch failed, retry after a beat. Canonical page: Integrations API. The full CRM loop (webhooks, writeback, debugging): Connect HubSpot & Salesforce end to end.

9. Coordinate the team in a team-chat channel

Team chat is the internal surface — channels, DMs, and huddles for the operators running your workspace, membership-gated so nothing ever reaches a customer. Useful when a back-office bot or dashboard posts handoff notes: list your channels, post to one, read it back. List your channels — every read is membership-scoped, so you see only channels your API key’s principal belongs to:
cURL
Node SDK
Post to a channel:
cURL
Node SDK
A 403 here is a membership gate, not a key problem — the same principal must be on the channel from the same identity; have the owner add you with POST /team-chat/channels/:id/members. Reactions, threads, DMs, and huddles follow the same shape off the message id. Canonical page: TeamChat API. The operator workflows behind it: Team Chat guide.

10. Errors as recipes

Errors are recipes too — the failure classes in API error handling by example map one-to-one onto the tasks above. Branch on error.code, never on message text. Anything not in this table: read meta.docs_url in the error envelope — it links to the code’s remedy in the Error Code Reference. Branch on the retriable-vs-terminal table in API error handling by example.

11. Place an outbound voice call

Originate a call, then track it to completion either by polling or — in production — by subscribing to call.completed in your webhook endpoint. The to/from are enough for a ring-only call; pass answer_url (HTTPS, on your server) returned-verb say/gather/dial instructions when the call answers. The second snippet uses the Python SDK’s typed voice module. Place:
cURL
Python SDK
A 201 Created returns the created call in the standard envelope — data.id (call_…) is your handle. Full-body reference (record/amd/metadata, the answer_url vs inline-verb tradeoff): Voice API. Poll once:
cURL
Python SDK
Receive the webhook instead. Subscribe your endpoint to call.completed (and call.failed) in Settings → Webhooks; verify X-Orbit-Signature before trusting the body, same contract as message DLRs. The full event list: Webhook events. Canonical page: Voice API.

12. Rent a number and start a port-in

Search the inventory for a contiguous block, purchase one into your tenant, then — when the digits you actually want live at another carrier — pre-check portability and open a port-in request. Search available numbers:
cURL
Python SDK
Purchase one:
cURL
Python SDK
Port an existing number from another carrier — pre-check eligibility, then open the request:
cURL
Python SDK
Track the port’s state on GET /numbers/porting (list of your open requests) — the carrier completes it asynchronously; don’t poll the check endpoint. Full catalogue — rent/release/webhook binding and port lifecycle: Numbers API.

13. Publish a WhatsApp template and send it

Outside Meta’s 24-hour customer-service window, every WhatsApp send has to ride an approved template. The loop: list your templates to find the approved name, then send it with positional variables. Find the approved template (the data array carries each template’s Meta status — APPROVED/PENDING/REJECTED — so you never copy a name that hasn’t cleared review):
cURL
Python
Send the template with template_params filling positional {{1}}…{{N}} placeholders (or Meta-native components):
cURL
Python
The language.code must be the exact locale the template was approved under in WhatsApp Manager, or the send is rejected with 422 WHATSAPP_TEMPLATE_NOT_FOUND (Meta 132001). Poll GET /messages/:id for the DLR. Creating new templates flows through the same /templates surface — full parameter reference: Messaging API.

14. Converse through the agent copilot

When a conversation reaches the unified inbox, an operator can ask the AI copilot for a suggested reply instead of drafting one. The loop: a live conversation id (from the inbox feed or a webhook), then one request per suggestion. Ask the copilot on a conversation:
cURL
Python
Each suggestion comes back scored for grounding — paste it, edit it, or discard it operator-side. Pick the conversation id off GET /conversations (the unified-inbox feed) or a conversation.created webhook. Supporting knowledge for the copilot’s answers lives in the Knowledge bases API; the conversation family: Conversations API.

15. Build a CDP segment and read its members

Segments are the CDP’s saved audiences — an AND/OR filter over traits, events, and scores that auto-refreshes as events land. Create one, then page its membership when you need the actual contacts behind it. Create a segment:
cURL
Python
A 201 Created returns the created segment already materialised — data.contact_count is live immediately, not a scheduler estimate. Read members:
cURL
Python
Cursor-page the members list exactly as you would any list endpoint (Task 4). Segments refresh automatically — auto_refresh (default true) keeps membership current as new CDP events update the fields the rules read. Programmatic POST /contacts/segments is an alternative to the dashboard builder; the full filter shape and exports: Segments API.

16. Run a Flow and inspect the execution

Flows are the visual automation graph a tenant builds in the dashboard — a trigger into a directed set of steps (delays, branches, channel sends). Create or fetch a flow id, trigger a run, then read the execution back step-by-step. Create a flow (the shape lives in your dashboard — POST /flows saves the graph; subsequent POST /flows/:id/publish versions it live):
cURL
Python
Trigger a run — a trigger_data payload optional, available to nodes as {{…}} template references:
cURL
Python
Inspect the execution — one call returns the full per-node trace:
cURL
Python
The flow list/analytics reads (GET /flows, GET /flows/executions/summary) give the same history tail the dashboard renders. Create/trigger/history detail: Flows API. A knowledge base is the retrieval store an AI agent grounds its answers on. The loop: create the base, push a document into it (multipart for binary files, JSON for text), poll the document list until indexing finishes, then run the same semantic search the agent uses — so you can preview exactly what would ground its answer. Create a knowledge base — 201 Created returns the created base; data.id (kb_…) is the handle every subsequent call needs:
cURL
Python
Writes need an admin-class role plus the knowledge:write scope; reads (list/poll/search) answer to knowledge:read. A 403 here is a scope/role gap on the key, not a malformed body — a 422 carries a field problem. Upload a document — send binary files (PDF/DOCX, images for OCR) as multipart:
cURL
For inline text (markdown, CSV, plain text), a JSON body works equally well:
cURL
Python
The response lands at data.status: "processing" — the upload is accepted but the chunks still have to embed. Wait on GET /knowledge-bases/:id/documents until the row flips:
cURL
Python
ready means retrievable; failed means extraction broke — re-queue just that doc with POST /knowledge-bases/:id/documents/:docId/retry (a 202, then poll again). partial means some chunks embedded; treat it as searchable but incomplete. Polling the base GET /knowledge-bases/:id instead of the documents list gives you only counts, not per-doc status. Search — run the same semantic retrieval an agent call would get. This read needs only knowledge:read, and the per-base search config (recency boost, source weights, min score) applies here too:
cURL
Python
A 200 with the ranked chunk list in data.results is the expected response; limit caps at 20. Attach the base to an agent and its calls resolve through this same search. Canonical page: Knowledge Bases API. The agent this grounds: Agent copilot.

18. Define a custom field and set it on a contact

Custom fields extend the contact record with tenant-defined keys — a tier, a region, a support PIN — that segments and exports then filter on. The loop: define the field once (key + type are immutable), write a value onto a contact, then read the contact back and see the value inline. Define the field — for select / multiselect the options list is required; pick one of text, number, date, boolean, url, select, multiselect. 201 Created returns the definition; the key (customer_tier here) is the handle every value writes against:
cURL
Python
Writes are owner/admin/developer-gated, and a PATCH /custom-fields/:id later can rename or re-order but never re-type the key — choose the type the first time; migrations between types are a new field plus a re-write. Set a value on a contact — PUT /custom-fields/values validates the value against the definition (type, regex, enum, length) before it lands, so a bad value answers 422 instead of dirtying the record:
cURL
Python
Read it back — the definition-backed values ride the contact’s custom_attributes object, returned inline by GET /contacts/:id right after the write (the write invalidates the contact cache):
cURL
Python
Every value you set this way is filterable in segments as custom_attributes.customer_tier and shows up in exports. List the definitions back with GET /custom-fields to drive a settings table or a field picker. Canonical pages: Custom Fields API, Contacts API.

See also