Docs navigation

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…" }
  • callNumber is the short number staff read out and search by. A consult call made during an attended transfer has parentCallId set to the call it transferred.
  • extension is the internal party: who placed the call, or who answered it, or where it ended.
  • ringSeconds is 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" }
  • sentBy is { "type": "extension", "extension": "3110" } for a text staff sent, and null for an inbound one.
  • error gives the carrier's reason when status is failed.
  • statusAt is when status last changed. On messages from before this was recorded it equals createdAt.
  • An MMS picture or file arrives as its own message with an empty body. Its media links 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: true instead 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 from would 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 failed with its reason in error.

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.