Home  /  API

Your systems,on every call.

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.

POST your-app.com/voitexVoitex → you
{
  "event": "call.started",
  "call": { "from": { "number": "+12125550187" } }
}
reply within 5 seconds
200 OKYou → Voitex
{
  "actions": [
    { "action": "say", "text": "Hi Sarah!" },
    { "action": "dial", "extension": "101" }
  ]
}
01

Incoming calls

VoiML, our JSON call routing. Voitex sends each incoming call to your webhook and runs the actions you reply with.

The loop

We ask. You answer. We run it.

A call arrives, we send you its state, you reply with an ordered list of actions. We run them, then ask again.

call.started

A call comes in. We POST you the call, you reply with actions.

We run them

Prompts play, digits are collected and calls are dialed, top to bottom.

We ask again

A gather, dial, record or player result, or a redirect, sends you the next request.

call.ended

Sent when the call is over, with every recording and the hangup bookmark.

Request & reply

One request in, one list of actions out.

Every request carries the call and your saved state. Every reply is actions plus, if you like, new state.

5 s
What we send
POST your-app.com/voitexcall.started
{
  "version": "1.0",
  "event": "call.started",
  "call": {
    "id": "c_8f2k41",
    "from": { "number": "+12125550187",
              "name": "SARAH KLEIN" },
    "to": { "did": "+12125550100" }
  },
  "state": null
}
  • call.to.did is the number they dialed. Use it to tell your accounts apart.
  • state is whatever you returned last, sent back word for word.
What you return
200 OK · application/jsonactions
{
  "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" }
}
  • inputs answers key 1 and key 2 instantly, with no extra request.
  • state left out keeps the old one, null clears it. An empty reply hangs up.
Instant answers

Menus with zero wait.

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.

1
Mapped in inputsThe branch runs on the spot. No request, no wait.
Instant
7
Any other keygather.completed is sent to you, so you still decide.
Asks you
···
No key pressedThe gather times out and the next action runs.
Moves on
Actions

Everything a call can do, in one reply.

Pick an action to see what it does, its main options and an example. Mix them in any order.

  • Speak & play
  • Collect
  • Connect
  • Capture
  • Control
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.
sayaction
{ "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.
playaction
{ "action": "play",
  "url": "https://cdn.example.com/greeting.mp3" }
playerLong audio callers can move around in.
New

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.
playeraction
{ "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.
gatheraction
{ "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.
dialaction
{ "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.
redirectaction
{ "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.
recordaction
[ { "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.
pauseaction
{ "action": "pause", "seconds": 1.5 }
hangupEnd the call.

The reason is kept in our logs only.

reasonFree text, optional.
hangupaction
{ "action": "hangup", "reason": "menu complete" }
Top to bottom

Actions run in order, and the whole reply is checked before anything plays.

Told, or asked

If you already said what happens after a gather, dial, record or player, we do it. Otherwise we ask you.

Timeouts fall through

A gather with no answer moves to the next action, so a trailing redirect is your no-input handler.

The end hangs up

Running off the end of the list ends the call. Finish with a gather, dial or redirect to carry on.

New · player action

Music, lectures, podcasts.

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 works
Events

Know the moment anything happens.

Every request includes the call, a sequence number and a timestamp, plus the details for that event.

Start
call.startedA call comes in.
During the call
gather.completedDigits were collected.+ input
gather.timeoutA gather with action_on_timeout expired.+ input
dial.completedA dial finished, answered or not.+ dial
record.completedA recording finished.+ record
player.completedA player ended while the caller is still on.+ player
redirectA redirect action ran.
End
call.endedThe call is over. Retried up to 3 times.+ hangup · recordings · player
Dial results

Know how every dial ended.

Every dial.completed carries one. Branch on it to send unanswered calls to voicemail or another number.

Connected1

answered

Not answered6

busy  ·  no_answer  ·  rejected  ·  declined  ·  canceled  ·  unavailable

Caller left1

escaped

Could not connect4

invalid_number  ·  unroutable  ·  congestion  ·  failed

Signing & delivery

Signed, timed and never lost.

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 + "." + body
X-Voitex-Timestampunix seconds
X-Voitex-Call-Idsame as call.id
X-Voitex-Deliveryunique per attempt

Your 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.

verify.jsNode.js
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));
}
5 sto answer each request
3 levelsof instant menus per reply
3 hoursper player recording
10 minper caller recording
02

Outgoing notifications

The Notify API. One request sends a call or a text to one person, a list, or your whole contact list.

Who it reaches

One person, or everyone.

Pick exactly one way to address each request. Larger sends are queued as a broadcast you can follow in the portal.

One numberto

Send to a single phone number.

A list of numbersphones

Up to 19 numbers are sent one by one. 20 or more go out as a broadcast.

Contact labelslabel

Reach everyone tagged with one or more labels, by home, mobile or both.

"type": "SMS"

Send a text

Up to 1,000 characters, from any of your SMS-enabled numbers. Add an attachment to send it as MMS.

bodyattachmentscid
SMS · single numberrequest
{
  "type": "SMS",
  "cid": "2125550100",
  "to": "2125550187",
  "body": "See you tomorrow at 3 PM!"
}
"type": "CALL"

Place a call

streamPlay a recording from your library when they answer.
gotoConnect them to one of your IVR boxes, with an optional greeting and variables.
CALL · streamrequest
{
  "type": "CALL",
  "cid": "2125550100",
  "to": "2125550187",
  "call_type": "stream",
  "recording": "closing_notice"
}
Example

A snow-day text to every parent.

Two contact labels, mobile numbers only, scheduled for 6:30 in the morning. One request, one response with the full count.

reply
You send
Notify API · sendPOST
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"
}
  • label sends to everyone with label 10 or 15. Use to or phones instead for specific numbers.
  • to_be_sent schedules it. Leave it out to send right away.
You get back
200 OK · application/jsonresponse
{
  "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": []
}
  • group_id identifies the broadcast. Smaller sends return a batch_id instead.
  • invalid lists every number that was skipped, and why.
Scheduled sends

Set a date and time, or send now.

Named broadcasts

A title so you can find it in the portal.

Clean lists

Bad numbers are reported, never silently dropped.

One key

Every request uses your Auth-Key header.

03

Recordings, contacts & voicemail

Manage your account’s audio, contacts and messages from your own code.

POST/uploadAuth-Key ✓
album_number=1 · type=tts
text="Holiday schedule"
200 OKapplication/json
{
  "status": "success",
  "data": "918357",
  "errors": []
}
3.1

Recordings

Upload audio or turn text into speech in any album, then list and download every recording.

UploadText-to-speechListDownload
POST/contactsAuth-Key ✓
function=update · phone=2125550187
add_label=12
200 OKapplication/json
{
  "status": "success",
  "data": "Contact updated"
}
3.2

Contacts

Create, update, look up and delete contacts from your own system, with their phones, fields and labels.

CreateUpdateLook upDelete
GET/messagesAuth-Key ✓
vm_group_id=3 · limit=25
200 OKapplication/json
{
  "voice_mails": [
    { "id": 77120, "clid": "2125550142",
      "date": "2026-10-27 10:42:05" }
  ],
  "last_id": 77120
}
3.3

Voicemail

Pull new voicemails by box and download the audio into your CRM, ticketing or inbox.

List by boxDownload audio
What you can build

Your data, on every call.

Questions

Good to know.

What do I need to get started?

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.

What happens if my server is slow or down?

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.

Can I keep track of a caller between requests?

Yes. Return a state object and it is sent back to you, word for word, on every request for that call.

Will I ever get the same event twice?

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.

Can I send calls and texts from my own system?

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.

Connect your systems to Voitex.

More from Voitex