Skip to main content

Program an outbound voice call

POST /api/v1/voice/calls originates a call, and the programmable voice surface decides what that call does once the far end answers. You have two options, picked per call:
  • answer_url — an HTTPS endpoint on your server. When the call answers, Orbit POSTs the call metadata to it and expects a JSON list of verbs (say, play, gather, dial, hangup, …) that runs the call — the Twilio voiceUrl / Vonage answer_url model, applied to outbound.
  • staticVerbs — the verb list embedded in the create request itself (TwiML-Bin style), for flows that never change between calls.
Both run through the same verb engine documented in the Programmable Voice DSL reference; this guide walks the whole loop — provision a number, place the call, serve the verbs, track progress, then fetch the recording and the transcript.
Every outbound leg terminates through the Devotel softswitch. You cannot route outbound voice through Telnyx, DIDWW, or an arbitrary carrier — a carrier-selection field a returned verb carries is ignored.

1. Provision a voice-capable number

You need one owned DID for the caller ID. If you do not already have one, work through Buy and provision numbers — search inventory with capabilities=sms,voice, purchase, and wait until the order reports the line provisioned. Two distinct models sit on that number:
  • Inbound — a caller dials your DID and the number’s inbound route decides what happens. Set it to an answer_url route if you want the same verb-driven treatment inbound.
  • Outbound — nothing to configure on the number. You pass it as from on POST /api/v1/voice/calls, and the call enters the verb surface the moment the far end picks up.

2. Place the call

Send to, optionally from (falls back to a tenant default when omitted), and one of the two control modes. Also accepted here: record (capture the call audio), amd (carrier-side answering-machine detection — judge the result server-side before you deliver a message meant for a human), metadata (an arbitrary JSON object persisted on the call row and echoed back in every webhook), plus campaignId and orgTimezone for spend-cap and quiet-hours bookkeeping.
cURL
Node SDK — @devotel/orbit
Python SDK — orbit-sdk
A 2xx returns the created call in the standard envelope; data.id is the call_… handle every later step uses. answer_url or staticVerbs? They are mutually exclusive — the API rejects a request carrying both, and a request carrying neither places a plain call with no programmable control. Reach for staticVerbs when the flow is fixed; reach for answer_url the moment the flow depends on the callee, your data, or input collected mid-call:
cURL — staticVerbs variant
answer_fallback governs what the call does when your answer_url cannot be served — safe-default (a short apology then hangup), voicemail, or decline. Between this and the production guardrails below, that is the whole reliability contract of the surface.

3. Implement the answer_url handler

The full handshake — the request body Orbit sends, the response shapes, and every verb — is in the Programmable Voice DSL reference. In short: Orbit POSTs { callSid, from, to, dialedE164, routeId, organizationId } to your URL, and your handler must reply with JSON — either { "verbs": [...] } or a bare verb array — within the fetch budget (5s default, 10s hard cap). On timeout, non-2xx, non-JSON, or an empty array, the answer_fallback you set on the route or the call runs instead, so a callee never lands on dead air.
Verify the signature before you serve verbs. Configure a signing secret on the route, and Orbit signs every request to your answer_url with HMAC-SHA256 in X-Devotel-Timestamp / X-Devotel-Signature. Verify against the raw request body — re-serializing parsed JSON breaks the comparison. Hash the raw bytes, reject timestamps older than a few minutes, and answer 401 on mismatch. This scheme is separate from customer-webhook signatures; the exact contract is in Verifying the request came from Orbit.
A complete minimal handler — greet, play a file, bridge to a PSTN destination, hang up:
Express
Branch on collected DTMF. gather collects input mid-call and POSTs the result to its actionHook, which returns the next verb list — the IVR-menu primitive over the same channel:
Express — DTMF branch
The full surface — gather with speech input, record (single-file capture with its own consent obligations), enqueue, transfer, listen (fork the live audio to a WebSocket for transcription or a voice agent), and the reserved conference / config verbs — is documented verb by verb in the DSL reference.

4. Track progress

Every call moves through ringingin_progress → a terminal completed or failed (a ring that never answers ends as no-answer, reported in the same terminal stream). Poll the call record…
cURL
Node SDK
…or — in production — subscribe to the terminal events. Create a webhook endpoint in Settings → Webhooks, select call.completed and call.failed, and verify the delivery signature before trusting the body. Each event carries the call handle, direction, final status, duration, and the machine-readable cause (hangup_reason + the SIP code); your metadata from step 2 is echoed back so you can correlate straight to your own record:

5. Fetch the recording and the transcript

Pass record: true at create time and the call’s media lands as a recording once the call is terminal. Subscribe to recording.completed instead of polling; when the event lands the download URL on the call row is ready:
cURL — recording metadata + signed download URL
Node SDK — finalized transcript
Python SDK
For a call you want to read as it happens, GET /api/v1/voice/calls/{callId}/transcript/stream streams partial and final transcript frames over SSE for the life of the call — one SDK call in Node:
Node SDK — live transcript stream

6. Production guardrails

  • Concurrency. Pace outbound originations per number and per API key — watch live usage and headroom in the API rate limits & quotas console and hold your originator below it; a 429 is the limiter telling you it is already over.
  • Voicemail detection. amd: true at create time asks the carrier to flag machine pickups — treat an AMD-flagged answer as a distinct branch (different prompt or a scheduled retry), never as a human-answered call.
  • STIR/SHAKEN. Devotel signs every outbound leg; what you own is the attestation policy, branded calling, and CNAM — see STIR/SHAKEN attestation.
  • Recording consent. Route-level capture and the DSL record verb have different consent gates — read the warning in the DSL reference before you turn either on.
  • Fallback is your seatbelt. Pick an explicit answer_fallback per call instead of inheriting safe-default silently — a flow with no fallback story is a flow that apologizes to your callees when your handler hiccups.

7. Failure modes

Terminal failures surface through call.failed and on the call record. Map the common ones: Consume these from the webhook events and let them feed disposition codes back into your list — the same signal the outbound dialer campaign surface paces with.

8. Next steps

  • Visual IVR for the inbound side — when the same verb logic should be authored, validated, and versioned without JSON, rebuild it in the IVR builder.
  • Outbound at scale — when you graduate from one-off calls to a paced dial list with disposition codes, move to the outbound dialer campaign surface.
The reference material this guide stood on: the Programmable Voice DSL reference, the OpenAPI spec for POST /api/v1/voice/calls, and the recipe collection in API recipes.