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 TwiliovoiceUrl/ Vonageanswer_urlmodel, applied to outbound. - staticVerbs — the verb list embedded in the create request itself (TwiML-Bin style), for flows that never change between calls.
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 withcapabilities=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
fromonPOST /api/v1/voice/calls, and the call enters the verb surface the moment the far end picks up.
2. Place the call
Sendto, 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
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.
A complete minimal handler — greet, play a file, bridge to a PSTN
destination, hang up:
Express
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
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 throughringing → in_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
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
Passrecord: 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
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
429is the limiter telling you it is already over. - Voicemail detection.
amd: trueat 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
recordverb have different consent gates — read the warning in the DSL reference before you turn either on. - Fallback is your seatbelt. Pick an explicit
answer_fallbackper call instead of inheritingsafe-defaultsilently — a flow with no fallback story is a flow that apologizes to your callees when your handler hiccups.
7. Failure modes
Terminal failures surface throughcall.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.
POST /api/v1/voice/calls, and the recipe
collection in API recipes.