Skip to main content

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 Twilio voiceUrl / 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.

The request Orbit POSTs to your answer_url

On each inbound call, Orbit sends an HTTP request to your configured URL. The default method is POST 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 Twilio X-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 ?).
To verify:
  1. Read X-Devotel-Timestamp and reject the request if it is older than a few minutes (for example, 5 minutes) to bound replay.
  2. Compute expected = "sha256=" + HMAC_SHA256(secret, "<timestamp>.<payload>") as hex, using the raw body (POST) or raw query string (GET) as payload.
  3. Compare expected against X-Devotel-Signature with 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 with Content-Type: application/json and either an envelope or a bare array of verbs:
A bare array is also accepted:
The verbs run top-to-bottom. If your URL times out (default 5s, hard cap 10s), returns a non-2xx status, returns non-JSON, or returns an empty / malformed array, Orbit emits the route’s configured fallback verbs so the caller never lands on dead air.

Verbs

Each verb object is discriminated by its verb 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 (invariant #45) — 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.
Consent for the record verb is your responsibility. A record verb returned from your answer_url is passed through to the call as-is — Orbit does not apply its recording-consent gate to it. That gate governs only Orbit-managed capture: route-level recording (recording_mode) and the platform voicemail flow. When your answer_url drives the call, you own the flow and the compliance obligation. Play any disclosure your jurisdiction requires — for example, US all-party-consent states or EU two-party consent regimes — before you return the record verb.
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.

Reserved verbs

Two verbs are managed by Orbit and cannot be returned from your answer_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.
If your 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 your answer_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.
The caller is never left on dead air, and the categorical failure reason is logged for your route analytics without leaking operator topology.