API reference
All paths are under /api/v1 and need a key (see API access for keys,
paging and errors). Everything here is read-only except where a read & write key or a permission is named.
Calls
GET /calls
One row per call, newest first. Scanner traffic turned away at the door is never included.
| Parameter | |
|---|---|
since, until, cursor, limit |
Time window and paging. |
include=segments |
Add each call's route (its stops) to every row. |
direction |
inbound, outbound or internal. |
status |
Comma-separated or repeated: answered, missed, voicemail, busy, cancelled, failed. |
extension |
Calls this extension placed, was dialed on, answered (including through a main number, ring group or queue), or that ended in its mailbox. |
number |
Calls to or from this outside number, in any format. |
did |
Inbound calls that arrived on this number of yours. |
queue |
Calls that went through this queue (its extension or id). |
{ "items": [ {
"id": "0c6c7f0e-1c5e-4a35-9f6a-3b1d2a6b7e10", "callNumber": "017-345", "parentCallId": null,
"start": "2026-10-20T14:00:00Z", "answered": "2026-10-20T14:00:29Z", "end": "2026-10-20T14:04:12Z",
"direction": "inbound", "status": "answered",
"from": { "number": "+15132345678", "name": "PAT JONES", "extension": null },
"to": { "number": "+15139876543", "name": null, "extension": null },
"did": "+15139876543", "extension": "2003", "queue": "8010",
"answeredBy": { "number": "2003", "name": "Front Desk", "extension": "2003" },
"finalDestination": { "kind": "queue", "number": "8010", "name": "Orders" },
"segments": null,
"durationSeconds": 252, "ringSeconds": 29, "talkSeconds": 223,
"transferred": false, "hangupCause": "NORMAL_CLEARING", "hasRecording": true, "recordingId": 4411
} ], "next": "NjM5NjY0…" }
callNumberis the short number staff read out and search by. A consult call made during an attended transfer hasparentCallIdset to the call it transferred.extensionis the internal party: who placed the call, or who answered it, or where it ended.ringSecondsis the time until answer. For an unanswered call it is the whole call.
`GET /calls/
One call, always with segments: the stops it made, in order.
"segments": [
{ "seq": 1, "kind": "menu", "number": "700", "name": "Main menu", "start": "2026-10-20T14:00:00Z",
"ringSeconds": 0, "talkSeconds": 12, "outcome": "routed", "answeredBy": null, "detail": "…" },
{ "seq": 2, "kind": "queue", "number": "8010", "name": "Orders", "start": "2026-10-20T14:00:12Z",
"ringSeconds": 17, "talkSeconds": 223, "outcome": "answered",
"answeredBy": { "number": "2003", "name": "Front Desk", "extension": "2003" },
"detail": "waited 0:17, answered by Front Desk (2003)" } ]
kind is one of extension, ring_group, queue, menu, voicemail, external, forward or transfer.
outcome is one of answered, no_answer, abandoned, forwarded, routed, message_left,
no_message or ended. A ring group or queue stop names only who answered, not every phone that rang.
GET /calls/summary
Totals for a dashboard tile, so you don't have to page through every record. Takes since and until
(the last 24 hours by default, up to 366 days) and groupBy, which is none, day, hour, extension or did.
Day and hour buckets use the system's time zone, named in the response.
{ "items": [ { "bucket": "2026-10-20", "inbound": 212, "outbound": 98, "internal": 41,
"answered": 301, "missed": 27, "voicemail": 23, "talkSeconds": 61840 } ],
"timeZone": "America/New_York", "since": "…", "until": "…" }
missed counts calls that rang out unanswered, plus calls that were cancelled or failed.
Text messages
GET /sms/numbers
Your text-enabled numbers: the ones you can read history for and send from.
{ "items": [ { "number": "+15135496990", "label": "Main", "mms": true, "routesTo": "Queue Orders (8010)" } ] }
routesTo is who receives a new text to that number right now, so office hours apply. mms says Exovo can
send media from the number; the carrier and the recipient's phone must support MMS too.
GET /sms/messages
Every inbound and outbound text, newest first. This is the feed to sync from.
| Parameter | |
|---|---|
since, until, cursor, limit |
By when the message was created. |
changedSince |
Messages created or whose delivery status changed at or after this time. Use it instead of since to catch delivery updates. |
number |
The outside party, in any format. |
did |
One of your numbers. |
direction |
inbound or outbound. |
status |
received (inbound), or queued, sent, delivered, failed. |
{ "id": "90211", "conversationId": "3318", "direction": "outbound",
"from": "+15135496990", "to": "+15134413752", "body": "Your order is ready for pickup.",
"media": [], "status": "delivered", "error": null,
"createdAt": "2026-10-20T14:00:00Z", "statusAt": "2026-10-20T14:00:04Z",
"sentBy": { "type": "api", "extension": null, "apiKey": "JEE-OPS" }, "reference": "jeeops-order-88412" }
sentByis{ "type": "extension", "extension": "3110" }for a text staff sent, and null for an inbound one.errorgives the carrier's reason whenstatusisfailed.statusAtis whenstatuslast changed. On messages from before this was recorded it equalscreatedAt.- An MMS picture or file arrives as its own message with an empty
body. Itsmedialinks are signed and expire after 24 hours.
`GET /sms/messages/
One message with its current delivery status.
POST /sms/messages
Sends a text. This needs a read & write key with the sms:send permission.
curl -X POST https://your-server/api/v1/sms/messages \
-H "Authorization: Bearer exovo_XXXX" -H "Content-Type: application/json" \
-H "Idempotency-Key: order-88412-ready" \
-d '{ "from": "+15135496990", "to": "5134413752", "body": "Your order is ready for pickup.",
"reference": "jeeops-order-88412" }'
| Field | |
|---|---|
from |
Required. One of GET /sms/numbers, in any format. Any other number is 403. |
to |
Required. The recipient, in any format. |
body |
Up to 1,600 characters. May be empty only when you send media. |
mediaUrls |
Optional. Up to 3 public https:// links the carrier fetches for an MMS. |
reference |
Optional. Your own id (up to 128 characters), stored and returned on the message. |
The answer is 202 with the message in status queued. Follow its progress with GET /sms/messages/{id},
changedSince, or the sms.status webhook.
- Idempotency-Key (optional header, up to 128 characters): send any unique string per logical message.
A retry with the same key within 24 hours returns the original message with 200 and
Idempotent-Replayed: trueinstead of sending again. - The text joins the conversation for that number in Exovo. If there isn't one, it opens one routed as a new
text to
fromwould be, so the customer's reply reaches the right staff. They see your message in the thread, labelled with the key's name. - Carrier limits still apply. For example, some carriers cap daily volume until their A2P registration is
complete. A carrier refusal shows up as status
failedwith its reason inerror.
GET /sms/conversations
One conversation per outside number and number of yours, latest activity first. Takes since, cursor,
limit, number and did.
{ "id": "3318", "number": "+15134413752", "did": "+15135496990", "routesTo": "Extension 3110 (Sam)",
"lastMessageAt": "…", "lastMessagePreview": "Your order is ready…", "messageCount": 6, "closed": false }
Voicemail
GET /voicemails
Voicemail details and transcripts. The audio itself isn't available through the API.
| Parameter | |
|---|---|
since, until, cursor, limit |
By when the message was left. |
mailbox |
An extension or shared mailbox number. |
number |
Messages left by this caller. |
unheard |
true or false. |
{ "id": "6f1c…", "mailbox": "3110", "from": { "number": "+15132345678", "name": "PAT JONES", "extension": null },
"receivedAt": "2026-10-20T14:00:00Z", "durationSeconds": 42, "unheard": true, "transcript": "Hi, it's Pat…" }
transcript is null until the message has been transcribed (when transcription is on).
Recordings
GET /recordings
Call recording details. Takes since, until, cursor and limit, plus extension, number, and callId
(the id from /calls).
{ "id": 4411, "callId": "0c6c7f0e-…", "start": "…", "end": "…", "durationSeconds": 223,
"direction": "inbound", "from": { … }, "to": { … }, "did": "+15139876543", "sizeBytes": 1784320,
"transcript": null, "summary": null, "sentiment": null }
sentiment runs from −100 (negative) to 100 (positive) when the recording has been analysed.
`GET /recordings/
The audio file. This needs the recordings:audio permission, and every download is written to the audit log.
Extensions
GET /extensions, `GET /extensions/
Extensions and users with their live state. Credentials are never included.
{ "extension": "3110", "name": "Sam Doe", "email": "sam@example.com", "enabled": true,
"presence": "Available", "registered": true, "inCall": false,
"dids": ["+15135496990"], "queues": ["8010"] }
registered means at least one phone or app is online. dids are direct numbers assigned to the extension.
`POST /extensions/
Sets an extension's status. This needs a read & write key. The body is { "presence": "Away" }, and
the values are Available, Away, Do Not Disturb, Custom 1 and Custom 2.
Queues and agents
These are snapshots of right now, made for wallboards. Polling every 10–15 seconds is plenty, or use the
queue.updated webhook.
GET /queues
{ "asOf": "…", "items": [ {
"id": "12", "name": "Orders", "extension": "8010",
"waiting": 2, "longestWaitSeconds": 48, "activeCalls": 3, "agentsLoggedIn": 5, "agentsAvailable": 1,
"today": { "offered": 141, "answered": 129, "abandoned": 9, "averageWaitSeconds": 21, "averageTalkSeconds": 184 } } ] }
today runs from midnight in the system's time zone. A call that reached the queue and was sent on, for
example by overflow, counts as offered but neither answered nor abandoned.
`GET /queues/
One queue's performance for a period: since and until, the last 24 hours by default. id is the queue's
id or extension.
{ "queue": "8010", "since": "…", "until": "…", "offered": 141, "answered": 129, "abandoned": 9,
"callbacksRequested": 4, "averageWaitSeconds": 21, "longestWaitSeconds": 312, "averageTalkSeconds": 184,
"byAgent": [ { "extension": "2003", "name": "Front Desk", "answered": 61, "talkSeconds": 11020 } ] }
GET /agents
Queue agents and what they are doing now. Pass queue= (an extension or id) for one queue's agents.
{ "extension": "2003", "name": "Front Desk", "queues": ["8010"], "loggedIn": true, "onBreak": false,
"state": "talking", "presence": "Available", "withParty": "+15132345678",
"loggedInSince": "2026-10-20T12:58:00Z", "today": { "answered": 31, "talkSeconds": 5410 } }
state is available, ringing, talking, on_break, logged_out or offline, where offline means no
phone is registered. A live call takes precedence, so an agent who is logged out of the queue but on a direct
call shows talking. Per-agent missed calls aren't recorded, so the API doesn't report them.