Programmable Voice DSL Reference
When you point one of your Devotel phone numbers (DIDs) at an HTTPS answer URL, Orbit turns that DID into a fully programmable inbound call — the TwiliovoiceUrl / Vonage answer_url model. On
every inbound call Orbit performs a server-side HTTP request to your
URL with the call metadata and expects a JSON response describing what
to do with the call. That JSON is a list of verbs — the
programmable-voice DSL documented here.
The verbs your answer_url returns are parsed, validated, and sanitised by
sanitizeDslVerbs (apps/api/src/routes/jambonz/voice-dsl-executor.ts) —
that function is the source of truth for this contract. The webhook
inbound-route handler (buildWebhookVerbs in
apps/api/src/routes/jambonz/verb-builder.ts) performs the server-side fetch
to your URL and then hands the response body to sanitizeDslVerbs; the other
build*Verbs handlers in that file build the platform-managed verb sequences
for the non-webhook route types (agent, queue, voicemail, …). The verb shape
is pinned as the VoiceDslVerb OpenAPI component
(apps/api/src/lib/openapi-schemas.ts).
Outbound calls always terminate through the Devotel softswitch.
Every
dial verb is routed through Devotel’s wholesale softswitch trunk.
You cannot select an arbitrary carrier, Telnyx Call Control, or DIDWW for
the outbound leg — any carrier-selection field you return is ignored.
Quality, STIR/SHAKEN signing, and PSAP routing are owned by Devotel.Architecture: JSON verbs, not a VXML server
If you are coming from Twilio TwiML or raw VoiceXML, the mental model carries over with one difference: Orbit does not serve a VXML document to the voice engine. jambonz, the engine under Orbit, consumes the verbs as structured JSON, so your answer_url is the VXML authoring surface: it decides what happens next on every call, and the JSON verb list you return is the call-control document. Orbit validates and sanitises that document, then hands it to the engine exactly as a VXML interpreter would hand a parsed document to a browser. Scripting, branching, and loops live in your answer_url code (any language that can serve HTTPS JSON), not in a document grammar. The mapping is close to one-to-one. A classic VXML menu:<prompt> maps to say (or play for a hosted audio file), the DTMF
grammar to gather with input: ["digits"] and numDigits, and
<submit next=…> to actionHook, the URL that receives the collected
digit and returns the next verb list. There is no XML to maintain, host,
or version: the same serverless function or controller action that would
render a VXML document returns JSON instead.
The field-by-field correspondence for every verb is in the
VoiceXML mappings table below.
The request Orbit POSTs to your answer_url
On each inbound call, Orbit sends an HTTP request to your configured URL. The default method isPOST with a JSON body; if you configured GET,
the same fields arrive as query-string parameters.
Verifying the request came from Orbit
When your inbound route has a signing secret configured, Orbit signs every request to your answer_url with HMAC-SHA256 so you can confirm it genuinely came from Orbit and was not forged by someone who learned your URL — the TwilioX-Twilio-Signature / Vonage answer_url JWT parity.
Two headers carry the signature:
Routes provisioned without a signing secret receive unsigned requests
(neither header is present), so signing is opt-in and adding a secret never
breaks an existing integration.
The signed content is the timestamp, a literal
., and the exact request
payload Orbit put on the wire:
- POST —
<X-Devotel-Timestamp>.<raw_request_body>, where the raw body is the exact JSON byte-for-byte as received. Hash the raw bytes; do not re-serialize the parsed JSON, or key-order and whitespace differences will make the signature mismatch. - GET —
<X-Devotel-Timestamp>.<raw_query_string>, where the query string is exactly what Orbit appended to your URL (everything after?, without the leading?).
- Read
X-Devotel-Timestampand reject the request if it is older than a few minutes (for example, 5 minutes) to bound replay. - Compute
expected = "sha256=" + HMAC_SHA256(secret, "<timestamp>.<payload>")as hex, using the raw body (POST) or raw query string (GET) aspayload. - Compare
expectedagainstX-Devotel-Signaturewith a timing-safe comparison and reject on mismatch.
This answer_url signature is a separate scheme from the customer-webhook
HMAC documented in Webhook Security. Both use
HMAC-SHA256 keyed on a per-resource secret, but the answer_url request
carries the timestamp in its own
X-Devotel-Timestamp header and a
sha256=<hex> value in X-Devotel-Signature, rather than the combined
t=<unix>,v1=<hex> encoding used for webhook deliveries. Verify each with
the scheme documented for it.The response your answer_url returns
Respond withContent-Type: application/json and either an envelope or a
bare array of verbs:
Verbs
Each verb object is discriminated by itsverb field.
say
Text-to-speech playback to the caller.
play
Play a pre-recorded audio file. The URL must be https:// and is
SSRF-validated at configuration time.
gather
Collect DTMF and/or speech input from the caller. This is also the IVR-menu
primitive: play your prompt with a preceding say (or play), then branch on
the collected digit in the verb list you return from actionHook.
dial
Bridge the caller to an outbound leg. Routed exclusively through the
Devotel softswitch trunk — you cannot select a carrier.
record
Capture the caller’s audio to a single recording file — the file-capture
primitive (e.g. for a voicemail, a spoken message, or a survey answer). It
records the current leg until the caller presses finishOnKey or
maxLength seconds elapse, then POSTs the recording metadata to your
actionHook. You are responsible for the consent this capture requires —
see the consent note below.
Recording the whole call is a route setting, not a DSL toggle. To record
an entire bridged call, set the inbound route’s
recording_mode (all or
inbound) — Orbit then records both/inbound legs through the Devotel SIPREC
server for the life of the call. There is no boolean record start/stop
switch in the DSL: a record field set to true or false is not consumed
by the engine and is ignored. Use the record verb above only to capture a
discrete clip to a file.enqueue
Park the caller in a named queue (e.g. for ACD / contact-center routing).
transfer
Hand the call off to another inbound route or destination.
Like
dial, a transfer to a PSTN number terminates through the Devotel
softswitch trunk — emergency short codes and blocked-prefix destinations are
rejected, and the caller hears your fallback instead.
hangup
End the call. Optionally supply a SIP reason surfaced in carrier CDRs.
listen
Fork the live, bidirectional call audio to your own WebSocket endpoint for
custom transcription, voice-AI, or analytics — the Twilio Media Streams /
Telnyx fork model. Orbit opens a secure WebSocket (wss://) to your url
and streams raw audio frames for the duration of the call. The endpoint must
be a public host: it is SSRF-validated at admission, so a private or
internal address is rejected and the verb is dropped.
listen forks media on the existing call leg — it never originates or
terminates a call. Bidirectional audio is injected back into the same leg,
so it does not create an outbound carrier leg and is unaffected by the
softswitch-termination rule that governs dial.VoiceXML mappings
For every construct, the verb you return and the VoiceXML element it replaces. Use this table when you port an existing VXML document.
The constructs Orbit manages for you, and that you therefore never emit,
are listed in Reserved verbs below.
End-to-end example: IVR menu, VXML translated to the DSL
The canonical IVR: “press 1 for sales, press 2 for support.” First the classic two-document VXML form, then the same flow in Orbit. Entry document (what a VXML server would serve on answer):
Branch document (what the VXML server would serve after
<submit>; in
Orbit, what https://app.example.com/voice/menu returns). Your handler
reads the POSTed digits field and returns the matching verb list:
1, and the equivalent list pointing at your support line for
digit 2. The dial verb terminates through the Devotel softswitch trunk;
no carrier selection is possible or needed.
The complete handler, including signature verification, is in
Verifying the request came from Orbit;
the response envelope is in
The response your answer_url returns.
Copy-paste snippets
Both snippets below are the wire contract verbatim: Orbit sanitises the list and hands it to the engine, so the JSON is the executed call-control document — there is no intermediate VXML render step to inspect. Each snippet also shows the classic VoiceXML document it replaces, for readers porting an existing VXML app; the construct-by-construct correspondence is in VoiceXML mappings.Outbound appointment reminder
Originate the call withPOST /calls, passing your answer_url exactly as
you would for inbound. This is the same contract as Twilio’s
POST /Calls with a Url.
actionHook handler records the digit (1 = confirmed, 2 =
cancelled, absent = no answer) and returns a closing say plus hangup.
In sandbox mode the call resolves completed without touching a carrier,
so you can exercise the full round-trip for free; see
Test your flow without live minutes.
Record then transcribe a voicemail
Capture the message withrecord, then fork live audio to your
transcription worker with listen. The recording metadata POSTed to your
actionHook carries the recording URL; the wss:// stream carries the
audio frames your worker transcribes.
actionHook must be
https:// on a public host, and listen must be wss:// on a public
host. Anything else (an internal address, plaintext ws://) is dropped so
the call degrades safely instead of failing. Remember that consent for
record is yours to provide; see the consent warning under
record above.
Test your flow without live minutes
The Sandbox and magic numbers simulator keys every outcome on the trailing digit of the destination number, so you can walk a whole flow with no carrier and no charge.- Create an inbound route that points one of your sandbox DIDs at your
answer_url, and use a sandbox API key (prefix
dv_test_sk_*). - Place a sandbox call to the DID. The call never reaches a carrier and deducts no balance.
- Exercise the failure paths by calling the magic numbers instead:
trailing
9maps to no-answer and8to busy in the simulator’s catalog, so your no-input and no-answer branch logic gets real traffic. - Assert on your side: your answer_url log shows the POST from Orbit with
callSid,from, andto, and your actionHook receives the DTMF digits you sent.
Limits and pitfalls
- Answer-URL budget: default timeout 5 seconds, hard cap 10 seconds,
on every fetch (first answer and each
actionHookround-trip). A slow endpoint means the fallback verbs run instead of your flow; keep prompts pre-rendered and databases warm. - No loops in the document: there is no loop verb. Re-prompts and
retries are your code’s job: return the same verb list from your
actionHook, and cap the attempts yourself (for example, three
gatherround-trips, thenhangup). Also cap chainedactionHookhops: each hop is a network round-trip billed against the caller’s patience. - Asynchronous verbs:
gatherandrecordpause execution until input or timeout; nothing after them runs until they complete. Whatever follows agatheris the no-input path, as in the IVR example above. - An empty or malformed response runs the fallback: returning
[], non-JSON, or a non-2xx never produces dead air; the route’s fallback (safe-default apology, voicemail, or decline) runs instead. - Reserved verbs are dropped:
conferenceandconfigreturned from your answer_url are silently removed; see Reserved verbs. - Caller ID on
dial: acallerIdthat is not a syntactically valid E.164 number is dropped and the dialed DID is presented instead; ownership and STIR/SHAKEN attestation happen on the softswitch leg. - Hard rejections replace the whole sequence: a
dialortransferto an emergency short code or a blocked prefix does not drop only that verb; the caller hears a polite refusal and the call ends, so a bad number cannot slip past a good prompt. dialexits through the Devotel softswitch only (see the note at the top of this page): carrier-selection fields you return are ignored, SIP-URI targets are rejected, and emergency or blocked-prefix destinations are refused.listenneeds a publicwss://host and forks the existing leg only; it never originates or terminates a call.
Reserved verbs
Two verbs are managed by Orbit and cannot be returned from youranswer_url:
conference— joining a caller into a named conference room. Rooms are created and named by the platform (conference routes, call park, supervisor monitoring), so accepting a room name from a webhook would let one call bridge into another tenant’s or a supervisor’s room. Configure conference routes in the dashboard instead.config— changing in-call engine settings such as pausing or resuming recording. Recording is governed by your route’s recording and consent settings, not by the verb list.
answer_url returns either verb, Orbit drops it and continues with the
rest of your verb list. Everything you need for a normal call flow is covered by
the verbs above.
Failure handling
Orbit fails soft. If youranswer_url is slow, unreachable, returns a
non-2xx, or returns a malformed body, the configured fallback runs
instead — one of:
- safe-default — a short apology prompt then hangup.
- voicemail — defer to the route’s voicemail configuration.
- decline — reject the call with a SIP
603 Decline.