Skip to main content

Programmable Voice — Answer Calls From Your Own Server

Customer AdminDeveloper

Most numbers in Orbit are answered by a call flow you draw in the editor. Programmable Voice is the other option: Orbit posts the incoming call to your HTTPS endpoint and runs the instructions your server returns.

Reach for it when the decision needs data or logic that lives in your systems — routing by account balance, a rota held in your own database, a queue chosen by an order's status. Everything else — recordings, transcripts, analytics — keeps working exactly as it does for any other call.

In the app: Telephony → Phone Numbers → open a number → How inbound calls are answered. It is a licensed add-on (programmable-voice); on an account without it the screen simply offers the call-flow field.


1. Point a number at your server

  1. Open Telephony → Phone Numbers and open the number. (Add the number first — the answering mode is set on a saved number.)

    The number's answering mode set to Call flow, with the application field below it

    The number's answering mode set to Call flow, with the application field below it

  2. Under How inbound calls are answered, choose Your server (webhook).

    Webhook mode with the Webhook URL, Method and Signing secret fields

    Webhook mode with the Webhook URL, Method and Signing secret fields

  3. Enter the Webhook URLhttps://, publicly reachable. Orbit refuses plain HTTP and addresses on private networks.

  4. Leave Method on POST — that is what carries the call payload.

  5. Set a Signing secret — see §4.

  6. Save.

The next call to that number goes to your endpoint.

A number answers one way at a time

Choosing a webhook replaces the call flow on that number; switching back restores it. If the number shared its application with other numbers, Orbit gives it one of its own (named Webhook — <number>) so the change cannot leak onto its neighbours.


2. What Orbit sends you

As the call arrives — before the caller hears anything — Orbit posts:

POST https://api.example.com/orbit/incoming
Content-Type: application/json
User-Agent: SaaS-SBC/1.0
X-Signature: 9f2c8a1e…

{
"callId": "b1e7c0a4-…",
"traceId": "3a02f19d-…",
"accountSid": "d5aa32ea-…",
"applicationSid": "5e7dff08-…",
"from": "15551001001",
"to": "902161234567",
"direction": "inbound",
"callStatus": "ringing"
}
FieldMeaning
callIdThis call leg. It can change if the call is transferred.
traceIdStable for the whole conversation — the id you will find on the call record, the recording and the transcript. Store this one.
accountSidYour Orbit account.
applicationSidThe application behind this number.
from / toCaller number and the dialled number, digits only.
directioninbound.
callStatusringing — the call is not answered yet.

Later requests — a gather's actionHook, or a redirect — are posted the same way, and carry callId, accountSid, applicationSid, from, to and your call tags, plus the digits or speech that were collected.


3. What you return

A JSON array of instructions, run in order. Each item is an object with a verb field:

[
{ "verb": "say", "text": "Welcome to Acme Support." },
{
"verb": "gather",
"input": ["dtmf"],
"numDigits": 1,
"say": { "text": "Press 1 for sales, 2 for support." }
},
{ "verb": "queue", "name": "sales" }
]
The whole list is accepted or rejected together

One unknown verb invalidates the entire response — Orbit does not run the good instructions and skip the bad one. Validate what you emit.

Five verbs hand the call over and end the list: dial, queue, conference, record and listen pass control on, so anything after them in the array never runs. Put them last.

Available verbs

say — speak text

FieldTypeNotes
textstringRequired.
languagestringe.g. en-US. Defaults to the application's language.
voicestringDefaults to the application's voice.
loopnumberRepeat count. Default 1.

play — play an audio file

FieldTypeNotes
urlstringRequired. WAV or MP3, reachable over HTTPS.
loopnumberDefault 1.

gather — collect keypresses or speech

FieldTypeNotes
inputstring[]["dtmf"], ["speech"], or both. Default ["dtmf"].
numDigitsnumberExpected digit count. Omit to collect until finishOnKey.
timeoutnumberSeconds to wait. Default 5.
finishOnKeystringDefault #.
say / playobjectPrompt to play while listening.
actionHookstringURL that receives { digits, speech } and returns the next instructions.

dial — connect the caller to someone

FieldTypeNotes
targetobject[]Required, tried in order. Each { type, name }, where type is user, phone, sip or queue.
timeoutnumberRing timeout in seconds. Default 60.
callerIdstringOverrides the presented number. Must be a number your account owns.
recordbooleanRecord this leg. Default false.

queue — place the caller in a queue

FieldTypeNotes
namestringRequired. Queue name as configured in Orbit.
mohstringMusic-on-hold stream. Default default.
timeoutnumberMaximum wait in seconds. Default 300.
timeoutHookstringURL called if the wait runs out.
announcePositionbooleanDefault true.
announceIntervalnumberSeconds between announcements. Default 30.

conference — join a conference room

FieldTypeNotes
namestringRequired.
mutedbooleanJoin muted. Default false.
startOnEnterbooleanDefault true.
endOnExitbooleanDefault false.
maxParticipantsnumberDefault unlimited.
recordbooleanDefault false.

record — start recording

FieldTypeNotes
formatstringmp3 or wav. Default mp3.
stereobooleanSeparate channels per party. Default true.
statusHookstringURL notified when the recording is ready.

listen — stream the audio to your service

FieldTypeNotes
urlstringRequired. wss:// endpoint.
mixTypestringmono, stereo or mixed. Default mixed.
sampleRatenumber8000 or 16000. Default 16000.
metadataobjectSent with the first WebSocket message.

pause, hangup, redirect, tag

VerbFieldNotes
pauselengthSeconds to wait. Default 1.
hangupreasonOptional, recorded on the call record.
redirecturlFetch the next instructions from another URL (posted like the first request).
tagdataKey/value metadata attached to the call record.

4. Verify the signature

With a signing secret set, every request carries an X-Signature header: the HMAC-SHA256 of the request body, hex encoded, with no prefix.

const crypto = require("crypto");

const expected = crypto
.createHmac("sha256", process.env.ORBIT_SIGNING_SECRET)
.update(rawBody) // the raw bytes, BEFORE JSON.parse
.digest("hex");

if (expected !== req.headers["x-signature"]) {
return res.sendStatus(401);
}
Use the raw body

A parsed and re-serialised JSON object has different bytes, and the signature will never match. In Express, use express.raw({ type: "application/json" }), or the verify callback on the JSON parser.

Orbit stores the secret encrypted and never returns it — the screen shows only its last four characters. Keep your own copy when you create it.


5. Timeouts and failures

SituationWhat happens
No reply within 10 secondsThe request is retried — 3 attempts in total, 0.5 s then 1 s apart.
Any non-2xx statusCounted as a failure and retried the same way.
An empty bodyRead as "no instructions": the call is ended.
An empty array, or an unknown verbThe response is rejected.

The caller is waiting in silence while your endpoint thinks, and a reply that never arrives leaves them with nothing to hear. So treat the webhook as a latency budget, not a place to work:

Answer first, work later

Return a short instruction immediately — a say and a queue — and do the slow part afterwards. A CRM lookup inside the webhook reply makes every caller wait for your slowest dependency, and a 500 from it costs you the call.


6. Test before you go live

  1. Point the number at a request-capture service and place a call, so you can see the exact payload your endpoint will receive.
  2. Reply with the smallest valid body and confirm you hear it:
    [{ "verb": "say", "text": "Webhook is working." }, { "verb": "hangup" }]
  3. Turn on signature checking and confirm a request signed with the wrong secret is rejected by your own code.
  4. Switch back to a call flow at any time from the same screen.

7. The call record is unchanged

A webhook-answered call produces the same call record, recording, transcript and analytics as any other call — see Recordings, Transcripts & AI Analytics. Use traceId from the payload to line your own records up with Orbit's, and the Developer chapter's API to pull them.