Route incoming calls from your own server with VoiML, and send calls and texts to one person or thousands. Simple JSON over HTTPS, on our own carrier network.
{
"event": "call.started",
"call": { "from": { "number": "+12125550187" } }
}{
"actions": [
{ "action": "say", "text": "Hi Sarah!" },
{ "action": "dial", "extension": "101" }
]
}Every call to your numbers is sent to your webhook. You reply with what happens next.
→Notify APIOutgoing notificationsSend a call or a text to one number, a list, or whole contact labels, now or on a schedule.
→More APIsRecordings, contacts & voicemailUpload and download recordings, keep contacts in sync and pull new voicemails.
→VoiML, our JSON call routing. Voitex sends each incoming call to your webhook and runs the actions you reply with.
A call arrives, we send you its state, you reply with an ordered list of actions. We run them, then ask again.
call.startedA call comes in. We POST you the call, you reply with actions.
Prompts play, digits are collected and calls are dialed, top to bottom.
A gather, dial, record or player result, or a redirect, sends you the next request.
call.endedSent when the call is over, with every recording and the hangup bookmark.
Every request carries the call and your saved state. Every reply is actions plus, if you like, new state.
{
"version": "1.0",
"event": "call.started",
"call": {
"id": "c_8f2k41",
"from": { "number": "+12125550187",
"name": "SARAH KLEIN" },
"to": { "did": "+12125550100" }
},
"state": null
}{
"actions": [
{ "action": "say", "text": "Thanks for calling Riverside Academy." },
{ "action": "gather", "num_digits": 1, "timeout": 6,
"prompts": [
{ "action": "say",
"text": "Press 1 for the office, 2 for today's schedule." }
],
"inputs": {
"1": [ { "action": "dial", "extension": "101" } ],
"2": [ { "action": "play", "url": "https://example.com/today.mp3" },
{ "action": "hangup" } ]
}
}
],
"state": { "menu": "main" }
}null clears it. An empty reply hangs up.Every request is dead air for the caller, and a menu press is the most common thing on any phone line. Put the answers you already know in inputs and they run the moment the key is pressed, up to three menus deep.
Anything that depends on a lookup, like a balance or who is on shift, still goes to your server.
inputsThe branch runs on the spot. No request, no wait.gather.completed is sent to you, so you still decide.Pick an action to see what it does, its main options and an example. Mix them in any order.
saySpeak text with text-to-speech.Uses your account voice unless you pick another. Set barge_in to false for a message callers must hear in full.
textWhat to say. Required.voiceOverride the account voice.languageDefault en-US.speed0.5 to 2.0.barge_inLet a keypress cut it short.{ "action": "say",
"text": "Thanks for calling.",
"barge_in": false }playPlay audio you host.mp3, wav, ogg, flac and more. Files are fetched early, cached and re-checked hourly, so a cached clip starts instantly.
urlThe audio file. Required.loopRepeat up to 10 times.barge_inLet a keypress cut it short.{ "action": "play",
"url": "https://cdn.example.com/greeting.mp3" }playerLong audio callers can move around in.For lectures and audiobooks up to 3 hours. Map keys to seek, pause, change speed or volume, open spoken menus, or return a key to you with the exact position.
urlThe audio file. Required.start_msWhere to start, to resume.speed0.5 to 2. Default 1.volume−6 to +6. Default 0.controlsKeys while playing.paused_controlsKeys while paused.pause_timeout5 to 3600 s. Default 300.{ "action": "player",
"url": "https://cdn.example.org/lectures/24.mp3",
"start_ms": 1112000, "speed": 1.5,
"controls": {
"1": { "seek": -15 }, "3": { "seek": 15 },
"2": { "pause_toggle": true },
"8": { "return": "bookmark" }
} }gatherAsk a question, collect keypresses.Prompts repeat until the caller answers. Put answers you already know in inputs and they run instantly, with no request to your server.
promptssay, play or pause.inputsDigits to actions, run instantly.num_digitsDefault 1.timeoutSeconds to wait, default 5.play_countTimes to ask, up to 10.{ "action": "gather",
"num_digits": 4, "timeout": 8, "play_count": 2,
"prompts": [
{ "action": "say", "text": "Enter your four digit code." }
] }dialCall a number or a PBX seat.Ring an outside number or an extension, its forward, or both at once, with your caller ID, hold music and an escape key.
numberAn outside number.extensionA seat on your PBX.ringextension, forward or both.ringbackring, moh or silence.confirmScreen the call first.{ "action": "dial", "extension": "101",
"ring": "both", "ringback": "moh",
"escape_key": "*", "timeout": 25 }redirectFetch and run the next document.Hand the call to another URL on your server. A redirect after a gather is your no-input handler.
urlRelative or absolute. Required.methodPOST or GET.{ "action": "redirect", "url": "/ivr/no-input" }recordRecord the caller, get a link.Up to 10 minutes, with a beep and a stop key. If the caller hangs up, the audio so far is still kept.
max_secondsDefault 60, up to 600.play_beepBeep before recording.finish_on_key#, * or any key.action_urlWhere record.completed goes.[ { "action": "say",
"text": "Leave a message after the beep." },
{ "action": "record", "max_seconds": 120 } ]pauseAdd a moment of silence.Anywhere from a tenth of a second to 30 seconds.
seconds0.1 to 30. Required.{ "action": "pause", "seconds": 1.5 }hangupEnd the call.The reason is kept in our logs only.
reasonFree text, optional.{ "action": "hangup", "reason": "menu complete" }Actions run in order, and the whole reply is checked before anything plays.
If you already said what happens after a gather, dial, record or player, we do it. Otherwise we ask you.
A gather with no answer moves to the next action, so a trailing redirect is your no-input handler.
Running off the end of the list ends the call. Finish with a gather, dial or redirect to carry on.
Long audio up to 3 hours that callers control from the keypad: skip, pause, change speed and volume, and bookmark. You get their exact place back.
See how it worksEvery request includes the call, a sequence number and a timestamp, plus the details for that event.
call.startedA call comes in.gather.completedDigits were collected.+ inputgather.timeoutA gather with action_on_timeout expired.+ inputdial.completedA dial finished, answered or not.+ dialrecord.completedA recording finished.+ recordplayer.completedA player ended while the caller is still on.+ playerredirectA redirect action ran.call.endedThe call is over. Retried up to 3 times.+ hangup · recordings · playerEvery dial.completed carries one. Branch on it to send unanswered calls to voicemail or another number.
answered
busy · no_answer · rejected · declined · canceled · unavailable
escaped
invalid_number · unroutable · congestion · failed
Each request is signed with your account secret using HMAC-SHA256. Check it against the raw body, compare in constant time and reject old timestamps.
X-Voitex-Signaturev1= HMAC-SHA256 of timestamp + "." + bodyX-Voitex-Timestampunix secondsX-Voitex-Call-Idsame as call.idX-Voitex-Deliveryunique per attemptYour server has 5 seconds to answer each request, and requests are not retried, because the caller is waiting. call.ended is the exception: it’s retried after 1, 5 and 15 seconds, so recordings and bookmarks always reach you.
import crypto from "crypto";
// Verify a Voitex webhook request
function isFromVoitex(req, secret) {
const ts = req.headers["x-voitex-timestamp"];
const sig = req.headers["x-voitex-signature"]
.replace("v1=", "");
const expected = crypto
.createHmac("sha256", secret)
.update(ts + "." + req.rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(sig), Buffer.from(expected));
}The Notify API. One request sends a call or a text to one person, a list, or your whole contact list.
Pick exactly one way to address each request. Larger sends are queued as a broadcast you can follow in the portal.
toSend to a single phone number.
phonesUp to 19 numbers are sent one by one. 20 or more go out as a broadcast.
labelReach everyone tagged with one or more labels, by home, mobile or both.
"type": "SMS"Up to 1,000 characters, from any of your SMS-enabled numbers. Add an attachment to send it as MMS.
{
"type": "SMS",
"cid": "2125550100",
"to": "2125550187",
"body": "See you tomorrow at 3 PM!"
}"type": "CALL"streamPlay a recording from your library when they answer.gotoConnect them to one of your IVR boxes, with an optional greeting and variables.{
"type": "CALL",
"cid": "2125550100",
"to": "2125550187",
"call_type": "stream",
"recording": "closing_notice"
}Two contact labels, mobile numbers only, scheduled for 6:30 in the morning. One request, one response with the full count.
POST /notify/
Auth-Key: your_auth_key
{
"type": "SMS",
"cid": "2125550100",
"label": "10,15",
"phone_type": "M",
"title": "Snow day",
"body": "School is closed today. Stay safe and warm!",
"to_be_sent": "2026-12-08 06:30:00"
}{
"status": "success",
"request_id": "r_51c2e8",
"group_id": "g_20931",
"summary": {
"type": "SMS",
"valid_count": 1240,
"invalid_count": 3,
"recipient_estimate": 1240
},
"invalid": [
{ "phone": "212555", "reason": "invalid_format" }
],
"errors": [],
"warnings": []
}Set a date and time, or send now.
A title so you can find it in the portal.
Bad numbers are reported, never silently dropped.
Every request uses your Auth-Key header.
Manage your account’s audio, contacts and messages from your own code.
/uploadAuth-Key ✓album_number=1 · type=tts text="Holiday schedule"
{
"status": "success",
"data": "918357",
"errors": []
}Upload audio or turn text into speech in any album, then list and download every recording.
/contactsAuth-Key ✓function=update · phone=2125550187 add_label=12
{
"status": "success",
"data": "Contact updated"
}Create, update, look up and delete contacts from your own system, with their phones, fields and labels.
/messagesAuth-Key ✓vm_group_id=3 · limit=25
{
"voice_mails": [
{ "id": 77120, "clid": "2125550142",
"date": "2026-10-27 10:42:05" }
],
"last_id": 77120
}Pull new voicemails by box and download the audio into your CRM, ticketing or inbox.
A public HTTPS endpoint that accepts JSON. Set it as your account’s webhook and every incoming call to your numbers is sent there. Use call.to.did to tell your numbers apart.
Each reply must arrive within 5 seconds, and the caller hears silence while you think. A timeout, an error or invalid JSON plays a short apology and ends the call, so return valid JSON even on your own errors.
Yes. Return a state object and it is sent back to you, word for word, on every request for that call.
Only call.ended, which is retried after 1, 5 and 15 seconds if your server does not answer. De-duplicate on call.id plus sequence.
Yes. The Notify API sends a call or a text to one number, a list, or whole contact labels, right away or on a schedule.