Home  /  API  /  Docs

API Reference

VoiML call routing, the Notify API, the recordings, contacts, voicemail and SMS endpoints, and the Grouply API.

00

Getting started

Two directions, one account. Voitex calls your webhook when a call comes in, and you call Voitex to send calls, texts and manage your data.

Overview

Everything is HTTPS. Voitex sends your webhook JSON; you send Voitex form fields and get JSON back. There are no SDKs to install.

APIWhat it does
VoiMLVoitex → you. Every incoming call to your numbers is POSTed to your webhook. You reply with an ordered list of actions.
Notify APIYou → Voitex. Send a call or a text to one number, a list, or whole contact labels, now or on a schedule.
More APIsYou → Voitex. Upload and download recordings, keep contacts in sync, pull new voicemails and send single texts.
Grouply APIYou → Voitex. Invite and remove group members, message a whole group and list its members.
To get started you need a public HTTPS endpoint that accepts JSON. Set it as your account’s webhook in the portal and every incoming call to your numbers is sent there.

Authentication

Requests you make to Voitex carry your Auth-Key header. Requests Voitex makes to your webhook are signed with your account secret, so you can prove they came from us.

CredentialWhereUsed for
Auth-KeyheaderEvery Notify, recordings, contacts, voicemail, SMS and Grouply request. We give it to you when your account opens. Keep it on your server.
account secretHMAC keyVerifying X-Voitex-Signature on webhook requests. See Signing.
POST /notify/header
POST https://notify.voitexapi.com/notify/
Auth-Key: your_auth_key
Content-Type: application/x-www-form-urlencoded

Send fields as form data: application/x-www-form-urlencoded, or multipart/form-data when you attach a file. Request examples below show the fields as JSON or key=value lines for readability.

APIMethodBase URL
NotifyPOSThttps://notify.voitexapi.com/notify/
RecordingsPOST · GEThttps://recording.voitexapi.com/
ContactsPOSThttps://contacts.voitexapi.com/
VoicemailGEThttps://vm.voitexapi.com/
Send SMSPOSThttps://sms.voitexapi.com/send/
GrouplyPOSThttps://grouply.voitexapi.com/
01

VoiML · Incoming calls

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

How it works

A call arrives, we send you its state, you reply with an ordered list of actions. We run them top to bottom, then ask again whenever we need to know what happens next.

StepWhat happens
call.startedA call comes in. We POST you the call, you reply with actions.
runPrompts play, digits are collected and calls are dialed, in order.
ask againA 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 player’s last position.

The request

Every request is a POST with a JSON body. It always carries the call and your saved state, plus the details for that event.

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
}
FieldTypeDescription
versionstringPayload version. Currently "1.0".
eventstringWhat happened. See Events.
sequenceintegerOrder of this request within the call. With call.id, use it to de-duplicate.
call.idstringUnique per call. Also sent as the X-Voitex-Call-Id header.
call.from.numberstringCaller’s number, E.164.
call.from.namestringCaller ID name, when the network provides one.
call.to.didstringThe number they dialed. Use it to tell your numbers and accounts apart.
stateanyWhatever you returned last, sent back word for word. null on the first request.

The reply

Reply 200 OK with application/json: a list of actions and, if you like, new state. The whole reply is checked before anything plays.

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" }
}
FieldTypeDescription
actionsarrayOrdered list of actions. An empty reply hangs up.
stateanyOptional. Saved and sent back on every later request for this call.

State

Return a state object and it comes back to you, word for word, on every request for that call. Use it for the menu they are in, an account you looked up, or how many times they missed a prompt.

You returnResult
"state": {…}Replaces the saved state.
no state keyKeeps the old state.
"state": nullClears it.

Instant inputs

Every request is dead air for the caller. Put the answers you already know in a gather’s 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.

Caller pressesWhat happens
a mapped keyInstant. The branch in inputs runs on the spot. No request.
any other keyAsks you. gather.completed is sent to your webhook, so you still decide.
nothingMoves on. The gather times out and the next action runs.

Actions

Each item in actions is an object with an action name and its options. Mix them in any order.

ActionWhat it does
saySpeak text with text-to-speech.
playPlay audio you host.
playerLong audio callers can move around in.
gatherAsk a question, collect keypresses.
dialCall a number or a PBX seat.
redirectFetch and run the next document.
recordRecord the caller, get a link.
pauseAdd a moment of silence.
hangupEnd the call.
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.

OptionTypeDescription
textstringWhat to say.required
voicestringOverride the account voice.
languagestringDefault en-US.
speednumber0.5 to 2.0.
barge_inbooleanLet 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.

OptionTypeDescription
urlstringThe audio file.required
loopintegerRepeat up to 10 times.
barge_inbooleanLet 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, music, podcasts and audiobooks up to 3 hours and 120 MB. Map keys to seek, pause, change speed or volume, open spoken menus, or return a key to you with the exact position. Keys you don’t map do nothing, so nobody loses their place by accident. Player guide →

OptionTypeDescription
urlstringThe audio file.required
start_msintegerWhere to start, to resume. Default 0.
speednumber0.5 to 2. Default 1.
volumeinteger−6 to +6. Default 0.
controlsobjectKeys while playing. Key → control.
paused_controlsobjectKeys while paused. Key → control.
pause_timeoutintegerSeconds a caller can stay paused, 5 to 3600. Default 300.
resume_rewind_msintegerHow far to step back on resume so no word is lost. Default 500.
action_urlstringWhere player.completed is sent.

Controls

Use these in controls, in paused_controls and inside menus.

ControlValueDescription
seeksecondsJump back (negative) or forward, 1 to 3600 seconds.
pause_toggletruePause or resume.
speednumberChange speed. Their place is kept.
volumeintegerA step louder or quieter, up to ±6.
volume_cycletrueCycle 0 → +2 → +4 → 0.
volume_resettrueBack to normal volume.
moh_toggletrueWhile paused, switch between the pause beep and hold music.
returnstringStop and send player.completed to you, with this name.
menuobjectOpen a spoken menu: prompts, timeout and options (key → control). Two levels deep. Callers who know it can type straight through, like a fast 5 7.
confirmarrayOptional on any control: say or play heard after it works.

The lecture keypad

1−15 s
2Pause
3+15 s
4−1 min
5Options
6+1 min
7−5 min
8Bookmark
9+5 min
*Menu
0Previous
#Next
SeekPause / menuReturns to you
player · lecture keypadaction
{
  "action": "player",
  "url": "https://cdn.example.org/lectures/24.mp3",
  "start_ms": 0,
  "controls": {
    "1": {"seek": -15},  "3": {"seek": 15},
    "4": {"seek": -60},  "6": {"seek": 60},
    "7": {"seek": -300}, "9": {"seek": 300},
    "2": {"pause_toggle": true},
    "8": {"return": "bookmark"},
    "0": {"return": "previous"}, "#": {"return": "next"}, "*": {"return": "menu"},
    "5": {"menu": {
      "prompts": [{"action": "play", "url": "https://cdn.example.org/prompts/options.mp3"}],
      "timeout": 3,
      "options": {
        "1": {"menu": {"timeout": 3, "options": {
               "1": {"volume": -2}, "2": {"volume_reset": true}, "3": {"volume": 2}}}},
        "2": {"return": "liked"}, "3": {"return": "playlists"},
        "5": {"return": "next_date"}, "6": {"return": "skip_10"},
        "7": {"speed": 0.5, "confirm": [{"action": "say", "text": "Half speed"}]},
        "8": {"speed": 1},
        "9": {"speed": 1.5, "confirm": [{"action": "say", "text": "One and a half speed"}]}
      }}}
  },
  "paused_controls": {
    "2": {"pause_toggle": true},
    "0": {"moh_toggle": true},
    "5": {"return": "length"}
  },
  "pause_timeout": 300,
  "action_url": "/ivr/player"
}

What comes back

Every player.completed, and the player block on call.ended, reports the caller’s place and settings, accurate to about ±0.2 s. Send the same player back with them and the caller carries on as if nothing happened.

player.completedVoitex → you
{
  "event": "player.completed",
  "player": {
    "reason": "key", "name": "bookmark", "digits": "8",
    "position_ms": 1383400, "duration_ms": 2895000,
    "speed": 1.5, "volume": 2, "paused": false,
    "listened_ms": 1102000
  }
}
FieldTypeDescription
reasonstringWhy playback stopped, e.g. key.
namestringThe return name of the control that was pressed.
digitsstringThe key or keys pressed.
position_msintegerWhere they stopped. Send it back as start_ms to resume.
duration_msintegerLength of the recording.
speed · volumenumberTheir current settings.
paused · mohbooleanPaused, or paused on hold music.
listened_msintegerTime actually listened.
paused_ms · menu_msintegerTime spent paused and in menus.
Voitex reports positions; bookmarks, likes, playlists and listening history are yours to keep in your own system. If the caller hangs up mid-recording, call.ended brings their position as an automatic 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.

OptionTypeDescription
promptsarraysay, play or pause actions.
inputsobjectDigits → list of actions, run instantly. Up to three menus deep.
num_digitsintegerDigits to collect. Default 1.
timeoutintegerSeconds to wait. Default 5.
play_countintegerTimes to ask, up to 10.
action_on_timeout—When set, a timeout sends gather.timeout to you instead of moving on.
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. Every dial ends with a dial result.

OptionTypeDescription
numberstringAn outside number. Use this or extension.
extensionstringA seat on your PBX.
ringstringextension, forward or both.
ringbackstringWhat the caller hears: ring, moh or silence.
confirm—Screen the call before connecting.
escape_keystringA key the caller can press to stop waiting. Ends with escaped.
timeoutintegerSeconds to ring.
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 right after a gather is your no-input handler.

OptionTypeDescription
urlstringRelative or absolute.required
methodstringPOST 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 and arrives with call.ended.

OptionTypeDescription
max_secondsintegerDefault 60, up to 600.
play_beepbooleanBeep before recording.
finish_on_keystring#, * or any key.
action_urlstringWhere record.completed goes.
recordactions
[ { "action": "say", "text": "Leave a message after the beep." },
  { "action": "record", "max_seconds": 120 } ]
pauseAdd a moment of silence.
OptionTypeDescription
secondsnumber0.1 to 30.required
pauseaction
{ "action": "pause", "seconds": 1.5 }
hangupEnd the call.
OptionTypeDescription
reasonstringFree text, optional. Kept in our logs only.
hangupaction
{ "action": "hangup", "reason": "menu complete" }

Execution rules

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.

Events

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

EventAddsWhen
call.started—A call comes in.
gather.completedinputDigits were collected that inputs didn’t answer.
gather.timeoutinputA gather with action_on_timeout expired.
dial.completeddialA dial finished, answered or not.
record.completedrecordA recording finished.
player.completedplayerA player ended while the caller is still on.
redirect—A redirect action ran.
call.endedhangup · recordings · playerThe call is over. The only event that is retried.

Dial results

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

GroupResults
connectedanswered
not answeredbusyno_answerrejecteddeclinedcanceledunavailable
caller leftescaped
could not connectinvalid_numberunroutablecongestionfailed

Signing

Each request is signed with your account secret using HMAC-SHA256 over the timestamp, a dot, and the raw body. Check it against the raw body, compare in constant time and reject old timestamps.

HeaderFormatDescription
X-Voitex-Signaturev1=hexHMAC-SHA256 of timestamp + "." + body.
X-Voitex-Timestampunix sWhen the request was sent.
X-Voitex-Call-IdstringSame as call.id.
X-Voitex-DeliverystringUnique per attempt.
verify
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));
}
import hmac, hashlib

# Verify a Voitex webhook request
def is_from_voitex(headers, raw_body: bytes, secret: str) -> bool:
    ts  = headers["X-Voitex-Timestamp"]
    sig = headers["X-Voitex-Signature"].removeprefix("v1=")
    expected = hmac.new(secret.encode(), ts.encode() + b"." + raw_body,
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(sig, expected)

Timeouts & retries

Your server has 5 seconds to answer each request, and requests are not retried, because the caller is waiting. A timeout, an error or invalid JSON plays a short apology and ends the call, so return valid JSON even on your own errors.

call.ended is the exception: it’s retried after 1, 5 and 15 seconds, so recordings and bookmarks always reach you. De-duplicate on call.id plus sequence.
02

Notify API · Outgoing

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

Send a notification

POSThttps://notify.voitexapi.com/notify/Auth-Key

Set type to SMS or CALL, pick who it reaches, and send it now or on a schedule.

FieldTypeDescription
typestringSMS or CALL.required
cidstringThe number it comes from. One of your numbers (SMS-enabled for texts). from also works.required
to · phones · labelstringWho it reaches. Exactly one. See Addressing.
titlestringA name for the broadcast, so you can find it in the portal. Default IVR Notification.
to_be_sentdatetimeYYYY-MM-DD HH:MM:SS. Schedules it. Leave it out to send right away.
dncflagDo-not-call list handling.
confirmflagConfirmation flag.

Addressing

Larger sends are queued as a broadcast you can follow in the portal.

FieldReachesDescription
toone numberSend to a single phone number.
phonesa listUp to 19 numbers are sent one by one. 20 or more go out as a broadcast.
labelcontactsComma-separated label IDs, e.g. "10,15". Reaches everyone tagged with any of them.
phone_typewith labelWhich contact phones to use: H home, M mobile, or H,M both (default).

SMS

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

FieldTypeDescription
bodystringThe message, up to 1,000 characters. message also works.required
attachmentsfileA file to send as MMS. Send the request as multipart/form-data.
SMS · single numberrequest
{
  "type": "SMS",
  "cid": "2125550100",
  "to": "2125550187",
  "body": "See you tomorrow at 3 PM!"
}
SMS · labels, scheduledrequest
{
  "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"
}

Calls

Choose what happens when they answer with call_type.

FieldTypeDescription
call_typestringstream or goto.required
recordingstringstream: the recording’s file name (no prefix), played when they answer.stream
sfile_idintegerstream: a Library file ID, instead of recording.
branch_numberintegergoto: the IVR box to connect them to.goto
route_numberstringgoto: the number used to bridge the call.goto
greeting · variablesJSONgoto: optional greeting and variables, as a JSON string.
CALL · streamrequest
{
  "type": "CALL",
  "cid": "2125550100",
  "to": "2125550187",
  "call_type": "stream",
  "recording": "closing_notice"
}
CALL · goto IVR boxrequest
{
  "type": "CALL",
  "cid": "2125550100",
  "to": "2125550187",
  "call_type": "goto",
  "branch_number": "5",
  "route_number": "2125550100"
}

Response

Bad numbers are reported, never silently dropped.

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": {
    "212555": "invalid_format"
  },
  "errors": [],
  "warnings": []
}
FieldTypeDescription
statusstringsuccess, partial_success (some numbers were skipped), failure or error.
request_idstringUnique per request.
group_idstringIdentifies a broadcast. Smaller sends return a batch_id instead.
summaryobjectroute, type, mode, valid_count, invalid_count and recipient_estimate.
invalidobjectEvery number that was skipped, and why, e.g. invalid_format or unallocated_nanp.
errors · warningsarrayAnything that stopped or affected the send.
03

Recordings, contacts & voicemail

Manage your account’s audio, contacts and messages from your own code. Every request uses your Auth-Key header and returns status, data and errors.

Recordings

Upload audio or create speech from text in any Library album, list what is on your account, and download the audio.

EndpointMethodPurpose
/upload/POSTUpload an audio file, or create a recording from text.
/list/GETList the recordings on your account.
/file/GETDownload the audio for one recording.
Base URL: https://recording.voitexapi.com. Album numbers are listed in the portal.

Upload a recording or create one with TTS

POSThttps://recording.voitexapi.com/upload/Auth-Key

Send upload_file to upload audio, or type=tts with text to have it spoken. Send as multipart/form-data when uploading a file.

FieldTypeDescription
album_numberintegerThe album the recording goes into.required
upload_filefileThe audio, wav or mp3. Required unless type=tts.
typestringtts to create the recording from text.
textstringWith type=tts: the words to speak.
sort_numberintegerThe number callers press to pick this recording in the album. Leave it out to use the next free number.
display_namestringThe name shown for the recording in the portal.
overridestringyes deletes any recording already at this sort_number first.
Upload a filerequest
POST https://recording.voitexapi.com/upload/
Auth-Key: your_auth_key

album_number=1
sort_number=5
display_name=Holiday hours
upload_file=@holiday-hours.wav
Create with TTSrequest
POST https://recording.voitexapi.com/upload/
Auth-Key: your_auth_key

album_number=1
sort_number=15
type=tts
text=We are closed Monday for the holiday.
200 OKresponse
{
  "status": "success",
  "data": "918357",
  "errors": []
}

data is the new recording’s ID. Use it as recording_id to download the audio.

List recordings

GEThttps://recording.voitexapi.com/list/Auth-Key

Returns recordings oldest first, up to 100 at a time.

ParameterTypeDescription
limitintegerHow many to return. Default and maximum 100.
last_idintegerPaging cursor. Pass back the last_id from the previous response. Leave it out on the first request.
album_numberintegerOnly this album. Leave it out for all albums.
include_trashstringyes includes recordings in the trash. Default no.
GET /list/request
GET https://recording.voitexapi.com/list/?limit=25&album_number=1
Auth-Key: your_auth_key
200 OKresponse
{
  "status": "success",
  "data": {
    "recordings": [{
      "id": 918357,
      "album_number": 1,
      "album_name": "Announcements",
      "sort_number": 5,
      "display_name": "Holiday hours",
      "saved_datetime": "2026-08-05 14:23:11",
      "file_size": 352.23,
      "duration": "00:00:22",
      "format": "wav",
      "verify": "Y",
      "in_trash": false
    }],
    "last_id": 918357
  },
  "errors": []
}
FieldTypeDescription
idintegerThe recording ID, the same one returned by upload. Use it as recording_id.
album_number · album_name—The album and its name in the portal.
sort_numberintegerIts number in the album. Not unique: recordings that share a number play in saved_datetime order.
display_namestringIts name in the portal.
saved_datetimedatetimeWhen it was saved. When verify is D, the date it starts playing, which may be in the future.
file_size · duration—Size in KB, and length as HH:MM:SS.
formatstringThe stored audio format. Matches the Content-Type of the download.
verifystringY playable · N waiting for admin approval · D scheduled for saved_datetime.
in_trashbooleantrue if it is in the trash. Only with include_trash=yes.

Keep requesting with the returned last_id until recordings comes back empty. Recordings added while you page show up on a later page, so nothing shifts under you.

Download a recording

GEThttps://recording.voitexapi.com/file/Auth-Key

Returns the raw audio, not JSON, with a Content-Type that matches the recording’s format. If it is not found you get HTTP 404 with a JSON error, so check the status code first.

ParameterTypeDescription
recording_idintegerThe id from List recordings.required
GET /file/request
GET https://recording.voitexapi.com/file/?recording_id=918357
Auth-Key: your_auth_key
404 Not Founderror
{
  "status": "error",
  "errors": ["file not found"]
}

Contacts

POSThttps://contacts.voitexapi.com/Auth-Key

Create, update, look up and delete contacts, with their phones, fields and labels. One endpoint: pick the operation with function. A contact can have several phones and labels, and is always found by any one of its phone numbers.

functionFinds byWhat it does
create—Adds a new contact. Fails if any of its phones already belongs to a contact.
updatephoneChanges fields, phone types, phones and labels.
selectphoneReturns the contact with all its phones and labels.
deletephoneDeletes the contact with all its phones and label memberships. The labels themselves stay.

Create a contact

FieldTypeDescription
functionstringcreaterequired
phonestringOne phone, or several separated by commas.required
phone_typestringH home (default) or M mobile. Applies to every phone in the request.
first_name · last_namestringThe contact’s name.
labelintegerA label number to add the contact to.
createrequest
POST https://contacts.voitexapi.com/
Auth-Key: your_auth_key

function=create
phone=2125550187,2125550133
phone_type=M
first_name=Sarah
last_name=Klein
label=2
200 OKresponse
{
  "status": "success",
  "data": "Contact created"
}

Update a contact

phone finds the contact and must already be on it. Send only what you want to change; send a field empty to clear it. You can add one phone and remove one phone per request.

FieldTypeDescription
functionstringupdaterequired
phonestringAny phone already on the contact.required
first_name · last_name · email · notesstringContact fields.
expdateExpiration date, e.g. 2026-02-01.
field_a … field_jstringCustom fields, plus any other custom fields enabled on your account.
phone_typestringH or M. Changes the type of the phone you sent only. Other values are ignored.
add_phonestringAdds a phone. Fails if it belongs to another contact; ignored if it is already on this one.
add_phone_typestringH (default) or M for add_phone.
remove_phonestringRemoves a phone. A contact always keeps at least one; use delete to remove it entirely.
add_label · remove_labelstringLabel numbers, comma-separated, e.g. 2,3,7. Any unknown label fails the request. Removing only ends membership; the label stays.
updaterequest
POST https://contacts.voitexapi.com/
Auth-Key: your_auth_key

function=update
phone=2125550187
email=sarah@example.com
field_a=1234
add_phone=2125550164
add_phone_type=M
add_label=3,7
remove_label=5
200 OKresponse
{
  "status": "success",
  "data": "Contact updated"
}

Look up a contact

FieldTypeDescription
functionstringselectrequired
phonestringAny phone on the contact.required
selectrequest
POST https://contacts.voitexapi.com/
Auth-Key: your_auth_key

function=select
phone=2125550187
200 OKresponse
{
  "status": "success",
  "data": {
    "contact": {
      "first_name": "Sarah", "last_name": "Klein",
      "email": "sarah@example.com", "notes": "", "exp": null,
      "address": "", "city": "", "state": "", "zip": "",
      "field_a": "1234", "field_b": "", "field_j": ""
    },
    "phones": [
      { "phone": "2125550187", "type": "M" },
      { "phone": "2125550164", "type": "M" }
    ],
    "labels": [2, 3, 7]
  }
}

Delete a contact

Removes the whole contact. To remove just one number, use update with remove_phone.

deleterequest
POST https://contacts.voitexapi.com/
Auth-Key: your_auth_key

function=delete
phone=2125550187
Errorresponse
{
  "status": "error",
  "errors": ["No contact found for phone: 2125550187"]
}

Voicemail

Pull new voicemails and download the audio into your CRM, ticketing or inbox. Both endpoints are GET with query parameters.

List voicemails

GEThttps://vm.voitexapi.com/messages/Auth-Key

Returns messages oldest first, up to 100 at a time.

ParameterTypeDescription
limitintegerHow many to return. Default and maximum 100.
last_idintegerPaging cursor. Pass back the last_id from the previous response to get only newer messages.
vm_group_idintegerOnly this voicemail box. Leave it out for all boxes.
GET /messages/request
GET https://vm.voitexapi.com/messages/?limit=25&last_id=4180
Auth-Key: your_auth_key
200 OKresponse
{
  "status": "success",
  "data": {
    "voice_mails": [{
      "id": 4181,
      "vm_group_id": 3,
      "call_id": "AST10-1689867312.264",
      "file_name": "vm_4181_1754500991",
      "clid": "2125550142",
      "date": "2026-10-27 10:42:05"
    }],
    "last_id": 4181
  }
}
FieldTypeDescription
idintegerThe voicemail ID. Use it as vm_id to download the audio.
vm_group_idintegerThe box it was left in.
call_idstringThe call’s ID, to match against your call logs.
file_namestringInternal file name, for reference only.
clidstringThe caller ID of whoever left it.
datedatetimeWhen it was left, YYYY-MM-DD HH:MM:SS.

Store the last last_id you received and send it next time to fetch only new voicemails.

Download a voicemail

GEThttps://vm.voitexapi.com/file/Auth-Key

Returns the raw WAV audio (audio/x-wav), not JSON. If it is not found you get HTTP 404 with a JSON error.

ParameterTypeDescription
vm_idintegerThe id from List voicemails.required
GET /file/request
GET https://vm.voitexapi.com/file/?vm_id=4181
Auth-Key: your_auth_key

Send SMS

POSThttps://sms.voitexapi.com/send/Auth-Key

A simple one-to-one text from one of your SMS-enabled numbers. For lists, labels, MMS or scheduling, use the Notify API.

FieldTypeDescription
fromstringYour SMS-enabled number.required
tostringThe number to text.required
bodystringThe message.required
POST /send/request
POST https://sms.voitexapi.com/send/
Auth-Key: your_auth_key

from=2125550100
to=2125550187
body=Your appointment is confirmed.
200 OKresponse
{
  "status": "success",
  "data": {
    "from": "2125550100",
    "to": "2125550187",
    "body": "Your appointment is confirmed.",
    "did_type": "TN",
    "referenceID": "u7T0um"
  },
  "errors": []
}

did_type is TN for a local number or TF for toll-free.

04

Grouply API

Manage your Grouply text groups from your own system: invite and remove members, message the whole group, and list who is in it.

Overview

POSThttps://grouply.voitexapi.com/Auth-Key

One endpoint: pick the action with function. group is the group’s phone number and must be a Grouply group on your account.

FieldTypeDescription
functionstringadd_member, remove_member, send or list_members.required
groupstringThe group’s phone number.required
request_idstringYour own ID for the request, up to 64 letters, digits and . _ : -. Repeating a request with the same ID never runs it twice, so retries are safe.
Members are invited, not added. Each person joins only after replying YES to the invite text, or pressing 1 on the invite call. API messages follow the same rules as an admin’s: member limit, quiet hours, message length and the daily sending limit. Grouply messages are priced like standard messaging; ask support@voitex.com about volume discounts.

Add a member

Sends an invite. A number that was removed earlier is invited again.

FieldTypeDescription
phonestringThe member’s phone number.required
namestringShown to the group, e.g. “Sarah Klein has joined the group”. Up to 50 characters; cannot end with CALL or CONFIRM.
typestringsms (default) gets texts. call gets each message as a phone call, for landlines; the invite is a call too.
confirminteger1 accepts the per-message overage charge when the group is at its member limit. Without it, adding to a full group fails.
add_memberrequest
POST https://grouply.voitexapi.com/
Auth-Key: your_auth_key

function=add_member
group=2125550199
phone=2125550187
name=Sarah Klein
type=sms
request_id=add-1042
200 OKresponse
{
  "status": "success",
  "errors": "",
  "data": {
    "phone": "2125550187",
    "status": "invited",
    "type": "sms",
    "message": "Invite sent to 2125550187. They will be added once they accept."
  }
}

Remove a member

They stop receiving group messages right away.

remove_memberrequest
POST https://grouply.voitexapi.com/
Auth-Key: your_auth_key

function=remove_member
group=2125550199
phone=2125550187
request_id=rem-1042
200 OKresponse
{
  "status": "success",
  "errors": "",
  "data": {
    "phone": "2125550187",
    "status": "removed",
    "type": "sms",
    "message": "2125550187 has been removed from the group."
  }
}

Send to the group

Goes to every active member from the group’s number, shown as coming from the group’s admin. Text members get a text; call members get a phone call.

FieldTypeDescription
messagestringThe text. Cannot start with #, which is reserved for commands.required
sendrequest
POST https://grouply.voitexapi.com/
Auth-Key: your_auth_key

function=send
group=2125550199
message=Practice is canceled tonight.
request_id=msg-7781
200 OKresponse
{
  "status": "success",
  "errors": "",
  "data": {
    "recipients": 42,
    "scheduled_for": null,
    "message": "Message sent."
  }
}

recipients is how many members it went to. scheduled_for is when it will go out if the group’s quiet hours held it, or null if it was sent right away.

List members

list_membersrequest
POST https://grouply.voitexapi.com/
Auth-Key: your_auth_key

function=list_members
group=2125550199
200 OKresponse
{
  "status": "success",
  "errors": "",
  "data": {
    "members": [
      { "phone": "2125550187", "name": "Sarah Klein",
        "status": "active", "type": "sms", "is_admin": false },
      { "phone": "2125550176", "name": "Jane Roe",
        "status": "invited", "type": "call", "is_admin": false }
    ]
  }
}
statusReceivesMeaning
invitednoWaiting for the person to confirm.
activeyesA member of the group.
leftnoThe person left the group.
removednoRemoved by an admin or the API.

Errors

Errors return "status": "error" with a list of messages: invalid or missing group number, group not found, invalid or missing phone, type must be sms or call, message is empty, messages starting with # are not allowed, the member is already in the group, and this group is currently full. grouply service unavailable, please try again means nothing was done; repeat the request with the same request_id.

05

Reference

Limits at a glance, and the questions we hear most.

Limits

LimitValue
webhook reply5 seconds, no retries (except call.ended: 1, 5, 15 s)
instant inputs3 menus deep per reply
gather.play_countUp to 10
play.loopUp to 10
player file3 hours, 120 MB
player.pause_timeout5 to 3600 s, default 300
player seek1 to 3600 s per press
player menus2 levels deep
record.max_secondsUp to 600, default 60
pause.seconds0.1 to 30
say.speed0.5 to 2.0
SMS body1,000 characters
phonesUp to 19 sent one by one; 20+ as a broadcast
list limit100 recordings or voicemails per request
Grouply name50 characters
request_id64 characters

Questions

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.

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.

How do I tell my numbers apart?

Use call.to.did, the number the caller dialed. One webhook can serve every number on your account.

Which audio formats work?

mp3, wav, ogg, flac, aiff and more. Files are converted for the phone line automatically, so a high-quality file sounds the same as a small one.

Do I have to store bookmarks?

Yes. Voitex reports positions; bookmarks, likes, playlists and listening history are yours to keep in your own system.