VoiML call routing, the Notify API, the recordings, contacts, voicemail and SMS endpoints, and the Grouply API.
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.
Everything is HTTPS. Voitex sends your webhook JSON; you send Voitex form fields and get JSON back. There are no SDKs to install.
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.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.
Auth-KeyheaderEvery Notify, recordings, contacts, voicemail, SMS and Grouply request. We give it to you when your account opens. Keep it on your server.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.
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/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 top to bottom, then ask again whenever we need to know what happens next.
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.Every request is a POST with a JSON body. It always carries the call and your saved state, plus the details for that event.
{
"version": "1.0",
"event": "call.started",
"call": {
"id": "c_8f2k41",
"from": { "number": "+12125550187", "name": "SARAH KLEIN" },
"to": { "did": "+12125550100" }
},
"state": null
}versionstringPayload version. Currently "1.0".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.Reply 200 OK with application/json: a list of actions and, if you like, new state. The whole reply is checked before anything plays.
{
"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" }
}stateanyOptional. Saved and sent back on every later request for this call.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.
"state": {…}Replaces the saved state.no state keyKeeps the old state."state": nullClears it.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.
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.Each item in actions is an object with an action name and its options. Mix them in any order.
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.
textstringWhat to say.requiredvoicestringOverride the account voice.languagestringDefault en-US.speednumber0.5 to 2.0.barge_inbooleanLet 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.
urlstringThe audio file.requiredloopintegerRepeat up to 10 times.barge_inbooleanLet a keypress cut it short.{ "action": "play", "url": "https://cdn.example.com/greeting.mp3" }playerLong audio callers can move around in.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 →
urlstringThe audio file.requiredstart_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.Use these in controls, in paused_controls and inside menus.
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.{
"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"
}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.
{
"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
}
}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.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.
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.{ "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.
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.{ "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.
urlstringRelative or absolute.requiredmethodstringPOST 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 and arrives with call.ended.
max_secondsintegerDefault 60, up to 600.play_beepbooleanBeep before recording.finish_on_keystring#, * or any key.action_urlstringWhere record.completed goes.[ { "action": "say", "text": "Leave a message after the beep." },
{ "action": "record", "max_seconds": 120 } ]pauseAdd a moment of silence.secondsnumber0.1 to 30.required{ "action": "pause", "seconds": 1.5 }hangupEnd the call.reasonstringFree text, optional. Kept in our logs only.{ "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.
Every request includes the call, a sequence number and a timestamp, plus a block with the details for that event.
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.Every dial.completed carries one. Branch on it to send unanswered calls to voicemail or another number.
connectedanswerednot answeredbusyno_answerrejecteddeclinedcanceledunavailablecaller leftescapedcould not connectinvalid_numberunroutablecongestionfailedEach 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.
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.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)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.One request sends a call or a text to one person, a list, or your whole contact list.
Set type to SMS or CALL, pick who it reaches, and send it now or on a schedule.
typestringSMS or CALL.requiredcidstringThe number it comes from. One of your numbers (SMS-enabled for texts). from also works.requiredtitlestringA 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.Larger sends are queued as a broadcast you can follow in the portal.
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).Up to 1,000 characters, from any of your SMS-enabled numbers. Add an attachment to send it as MMS.
bodystringThe message, up to 1,000 characters. message also works.requiredattachmentsfileA file to send as MMS. Send the request as multipart/form-data.{
"type": "SMS",
"cid": "2125550100",
"to": "2125550187",
"body": "See you tomorrow at 3 PM!"
}{
"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"
}Choose what happens when they answer with call_type.
call_typestringstream or goto.requiredrecordingstringstream: the recording’s file name (no prefix), played when they answer.streamsfile_idintegerstream: a Library file ID, instead of recording.branch_numberintegergoto: the IVR box to connect them to.gotoroute_numberstringgoto: the number used to bridge the call.gotogreeting · variablesJSONgoto: optional greeting and variables, as a JSON string.{
"type": "CALL",
"cid": "2125550100",
"to": "2125550187",
"call_type": "stream",
"recording": "closing_notice"
}{
"type": "CALL",
"cid": "2125550100",
"to": "2125550187",
"call_type": "goto",
"branch_number": "5",
"route_number": "2125550100"
}Bad numbers are reported, never silently dropped.
{
"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": []
}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.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.
Upload audio or create speech from text in any Library album, list what is on your account, and download the audio.
/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.https://recording.voitexapi.com. Album numbers are listed in the portal.Send upload_file to upload audio, or type=tts with text to have it spoken. Send as multipart/form-data when uploading a file.
album_numberintegerThe album the recording goes into.requiredupload_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.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
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.
{
"status": "success",
"data": "918357",
"errors": []
}data is the new recording’s ID. Use it as recording_id to download the audio.
Returns recordings oldest first, up to 100 at a time.
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 https://recording.voitexapi.com/list/?limit=25&album_number=1 Auth-Key: your_auth_key
{
"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": []
}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.
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.
recording_idintegerThe id from List recordings.requiredGET https://recording.voitexapi.com/file/?recording_id=918357 Auth-Key: your_auth_key
{
"status": "error",
"errors": ["file not found"]
}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.
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.functionstringcreaterequiredphonestringOne phone, or several separated by commas.requiredphone_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.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
{
"status": "success",
"data": "Contact created"
}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.
functionstringupdaterequiredphonestringAny phone already on the contact.requiredfirst_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.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
{
"status": "success",
"data": "Contact updated"
}functionstringselectrequiredphonestringAny phone on the contact.requiredPOST https://contacts.voitexapi.com/ Auth-Key: your_auth_key function=select phone=2125550187
{
"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]
}
}Removes the whole contact. To remove just one number, use update with remove_phone.
POST https://contacts.voitexapi.com/ Auth-Key: your_auth_key function=delete phone=2125550187
{
"status": "error",
"errors": ["No contact found for phone: 2125550187"]
}Pull new voicemails and download the audio into your CRM, ticketing or inbox. Both endpoints are GET with query parameters.
Returns messages oldest first, up to 100 at a time.
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 https://vm.voitexapi.com/messages/?limit=25&last_id=4180 Auth-Key: your_auth_key
{
"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
}
}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.
Returns the raw WAV audio (audio/x-wav), not JSON. If it is not found you get HTTP 404 with a JSON error.
vm_idintegerThe id from List voicemails.requiredGET https://vm.voitexapi.com/file/?vm_id=4181 Auth-Key: your_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.
fromstringYour SMS-enabled number.requiredtostringThe number to text.requiredbodystringThe message.requiredPOST https://sms.voitexapi.com/send/ Auth-Key: your_auth_key from=2125550100 to=2125550187 body=Your appointment is confirmed.
{
"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.
Manage your Grouply text groups from your own system: invite and remove members, message the whole group, and list who is in it.
One endpoint: pick the action with function. group is the group’s phone number and must be a Grouply group on your account.
functionstringadd_member, remove_member, send or list_members.requiredgroupstringThe group’s phone number.requiredrequest_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.Sends an invite. A number that was removed earlier is invited again.
phonestringThe member’s phone number.requirednamestringShown 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.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
{
"status": "success",
"errors": "",
"data": {
"phone": "2125550187",
"status": "invited",
"type": "sms",
"message": "Invite sent to 2125550187. They will be added once they accept."
}
}They stop receiving group messages right away.
POST https://grouply.voitexapi.com/ Auth-Key: your_auth_key function=remove_member group=2125550199 phone=2125550187 request_id=rem-1042
{
"status": "success",
"errors": "",
"data": {
"phone": "2125550187",
"status": "removed",
"type": "sms",
"message": "2125550187 has been removed from 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.
messagestringThe text. Cannot start with #, which is reserved for commands.requiredPOST https://grouply.voitexapi.com/ Auth-Key: your_auth_key function=send group=2125550199 message=Practice is canceled tonight. request_id=msg-7781
{
"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.
POST https://grouply.voitexapi.com/ Auth-Key: your_auth_key function=list_members group=2125550199
{
"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 }
]
}
}invitednoWaiting for the person to confirm.activeyesA member of the group.leftnoThe person left the group.removednoRemoved by an admin or the API.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.
Limits at a glance, and the questions we hear most.
webhook reply5 seconds, no retries (except call.ended: 1, 5, 15 s)instant inputs3 menus deep per replygather.play_countUp to 10play.loopUp to 10player file3 hours, 120 MBplayer.pause_timeout5 to 3600 s, default 300player seek1 to 3600 s per pressplayer menus2 levels deeprecord.max_secondsUp to 600, default 60pause.seconds0.1 to 30say.speed0.5 to 2.0SMS body1,000 charactersphonesUp to 19 sent one by one; 20+ as a broadcastlist limit100 recordings or voicemails per requestGrouply name50 charactersrequest_id64 charactersEach 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.
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.
Use call.to.did, the number the caller dialed. One webhook can serve every number on your account.
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.
Yes. Voitex reports positions; bookmarks, likes, playlists and listening history are yours to keep in your own system.