By area
Old paths
Read from the API’s own reference file every hour.
Older paths that still answer. They behave exactly like the routes they point to. Use the new paths in new code.
List calls (old path)
GET/v1/Old path
The same as GET /v1/calls: the same parameters, the same answer, the same reports counter. Kept for older integrations. Use GET /v1/calls.
Request
curl -G https://api.vocatech.com/v1/reports/calls \
-H "Authorization: Bearer YOUR_API_KEY" \
--data-urlencode "start_date=2026-09-21" \
--data-urlencode "end_date=2026-09-22" \
--data-urlencode "timezone=America/New_York"const params = new URLSearchParams({
start_date: "2026-09-21",
end_date: "2026-09-22",
timezone: "America/New_York",
});
const res = await fetch(`https://api.vocatech.com/v1/reports/calls?${params}`, {
headers: { Authorization: "Bearer YOUR_API_KEY" },
});
console.log(res.status, await res.json());import requests
r = requests.get(
"https://api.vocatech.com/v1/reports/calls",
headers={"Authorization": "Bearer YOUR_API_KEY"},
params={
"start_date": "2026-09-21",
"end_date": "2026-09-22",
"timezone": "America/New_York",
},
timeout=30,
)
print(r.status_code, r.json())<?php
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client([
'base_uri' => 'https://api.vocatech.com',
'headers' => ['Authorization' => 'Bearer YOUR_API_KEY'],
'http_errors' => false,
]);
$res = $client->get('/v1/reports/calls', [
'query' => [
'start_date' => '2026-09-21',
'end_date' => '2026-09-22',
'timezone' => 'America/New_York',
],
]);
echo $res->getStatusCode(), PHP_EOL, $res->getBody(), PHP_EOL;Parameters
| Name | In | Type | Description |
|---|---|---|---|
start_date | query | string (date) | First local day of the window, YYYY-MM-DD, in timezone. Send it with end_date. Send neither for today. A single date is ignored and you get today. For example 2026-09-01. |
end_date | query | string (date) | Last local day of the window, YYYY-MM-DD, included in full. At most 366 days after start_date. For example 2026-09-07. |
timezone | query | string | IANA time zone for the dates, for example America/Chicago. An unknown zone falls back to America/New_York. The response echoes the zone used. Default America/New_York. |
direction | query | string | incoming or outgoing, as each call's direction reads. voicemail matches the rare calls stored that way. One of any, incoming, outgoing or voicemail. Default any. |
status | query | string | Only calls with a leg in this status, for example answered or missed. missed never returns an outgoing call. Default any. For example missed. |
extension | query | string | Only calls with a leg on this extension (exact match). For example 104. |
type | query | string | Only calls with a leg of this type, for example user, auto_attendant, hunt_group or call_center. Default any. |
search | query | string | Text to find in the call id, the other party's name or number, your number, or an extension. |
sort | query | string | Order by start time or by duration. Calls that tie keep a stable order, so paging is safe. One of day_desc, day_asc, duration_desc or duration_asc. Default day_desc. |
page | query | integer | Page number, from 1. Default 1. At least 1. |
limit | query | integer | Rows per page, 1 to 500. A value outside that range is brought inside it. Default 500. From 1 to 500. |
company_id | query | integer | Optional. If you send it, it must be your own company id, or the request is refused with 403. The key already decides the company. |
Answers
200
The calls in the window.
| Field | Type | Description |
|---|---|---|
query required | object | The filters that were applied, echoed back. extension and type are applied but not echoed. |
query. | integer | |
query. | string (date) | The first local day used. |
query. | string (date) | The last local day used. |
query. | string | |
query. | string | |
query. | string | |
query. | string | The time zone used. |
calls required | array of objects | |
calls[]. required | string | The call's id. |
calls[]. required | string | incoming or outgoing. A call your staff placed through the dial-out portal reads outgoing. A few calls read voicemail. |
calls[]. required | string | The status of the first leg in journey, for example answered or missed. It is not the call's final outcome: a call a menu answered and a person then missed reads answered. Read the legs to see who answered. |
calls[]. required | string or null | The last person who held the call. When no person did, the first extension on it. |
calls[]. required | string or null | That extension's name, written First Last. |
calls[]. required | string | The other party's name from caller ID, written First Last. Can be empty. |
calls[]. required | string | The other party's number, digits without the leading 1. Can be empty. |
calls[]. required | string | Your number on the call, digits without the leading 1. Can be empty. |
calls[]. required | string (date-time) or null | UTC, ISO 8601 with milliseconds. |
calls[]. required | string (date-time) or null | UTC, ISO 8601 with milliseconds. |
calls[]. required | integer | The call's length in seconds. |
calls[]. required | array of objects | Every leg, in order. When you filter by status, extension or type, only the matching legs are listed. |
calls[]. required | integer | Position in the journey, from 1. |
calls[]. required | string | What handled this leg, as the phone system wrote it. For example user (a person), auto_attendant, hunt_group or call_center. |
calls[]. required | string or null | The name of the person or feature on this leg, as set in the portal, written First Last. |
calls[]. required | string or null | The extension on this leg. |
calls[]. required | string (date-time) or null | UTC, ISO 8601 with milliseconds. |
calls[]. required | string (date-time) or null | UTC, ISO 8601 with milliseconds. |
calls[]. required | integer | Seconds on this leg. |
calls[]. required | string or null | The AI summary of this leg's recording (its latest part). Null when there is none, and when the leg's only recording belongs to another call leg. |
calls[]. required | string or null | The transcript of this leg's recording (its latest part), with speaker labels. Null when there is none, and when the leg's only recording belongs to another call leg. |
calls[]. required | string (uri) or null | Where to fetch this leg's recording (its latest part). Call it with your key (GET /v1/media/rec_<n>) to get a signed link. Null when the leg was not recorded. |
calls[]. required | array of objects | Every recording of this leg, oldest first. A call parked and picked up again has two parts (rarely three). Nearly every recorded leg has one. Empty when the leg was not recorded. |
calls[]. required | integer | This part's place, from 1, oldest first. |
calls[]. required | integer | How many parts the leg's recording has. |
calls[]. required | integer | The recording's id. The call.transcription event of this part carries the same id. |
calls[]. required | string (date-time) or null | When this part started. UTC, ISO 8601 with milliseconds. |
calls[]. required | integer or null | Seconds. |
calls[]. required | string (uri) or null | Where to fetch this part (GET /v1/media/rec_<n>?part=N) with your key. Null while the part's file is not stored yet. |
meta required | object | Paging for list routes. Each list adds its own total. |
meta. required | integer | This page, from 1. |
meta. required | integer | Rows per page, after limits were applied. |
meta. required | integer | How many pages there are. |
meta. required | integer | Calls that match, across all pages. |
Example
{
"query": {
"company_id": 1234567890,
"start_date": "2026-09-21",
"end_date": "2026-09-22",
"direction": "any",
"status": "any",
"search": "",
"timezone": "America/New_York"
},
"calls": [
{
"call_id": "3c9d7e1f-2a4b-4c6d-8e0f-1a2b3c4d5e6f",
"direction": "outgoing",
"status": "answered",
"extension": "104",
"extension_name": "Maya Chen",
"remote_name": "",
"remote_number": "7185550142",
"group_number": "7185550100",
"start_time": "2026-09-22T15:40:02.000Z",
"end_time": "2026-09-22T15:41:10.000Z",
"duration": 68,
"journey": [
{
"order": 1,
"type": "user",
"extension_name": "Maya Chen",
"extension": "104",
"start_time": "2026-09-22T15:40:02.000Z",
"end_time": "2026-09-22T15:41:10.000Z",
"duration": 68,
"summary": null,
"transcription": null,
"recording_url": null,
"recordings": []
}
]
},
{
"call_id": "8a1f3c2e-5b7d-4e9a-9c1b-2d4e6f8a0b1c",
"direction": "incoming",
"status": "answered",
"extension": "104",
"extension_name": "Maya Chen",
"remote_name": "Jordan Reyes",
"remote_number": "7185550142",
"group_number": "7185550100",
"start_time": "2026-09-21T14:03:12.000Z",
"end_time": "2026-09-21T14:09:40.000Z",
"duration": 388,
"journey": [
{
"order": 1,
"type": "auto_attendant",
"extension_name": "Main Menu",
"extension": "100",
"start_time": "2026-09-21T14:03:12.000Z",
"end_time": "2026-09-21T14:03:31.000Z",
"duration": 19,
"summary": null,
"transcription": null,
"recording_url": null,
"recordings": []
},
{
"order": 2,
"type": "user",
"extension_name": "Maya Chen",
"extension": "104",
"start_time": "2026-09-21T14:03:31.000Z",
"end_time": "2026-09-21T14:09:40.000Z",
"duration": 369,
"summary": "Jordan Reyes asked to move the Thursday appointment to Friday at 10 AM. Maya Chen confirmed the new time and said a reminder text will follow.",
"transcription": "Speaker 1: Good morning, thanks for calling. This is Maya.\nSpeaker 2: Hi Maya, this is Jordan Reyes. I need to move my Thursday appointment.\nSpeaker 1: No problem. Would Friday at 10 AM work?\nSpeaker 2: Friday at 10 is perfect. Thank you.",
"recording_url": "https://api.vocatech.com/v1/media/rec_48213907",
"recordings": [
{
"part": 1,
"parts": 2,
"recording_id": 10577640,
"start_time": "2026-09-21T14:03:32.000Z",
"duration": 41,
"url": "https://api.vocatech.com/v1/media/rec_48213907?part=1"
},
{
"part": 2,
"parts": 2,
"recording_id": 10577702,
"start_time": "2026-09-21T14:04:31.000Z",
"duration": 309,
"url": "https://api.vocatech.com/v1/media/rec_48213907?part=2"
}
]
}
]
}
],
"meta": {
"page": 1,
"limit": 500,
"total_pages": 1,
"total_calls": 2
}
}400
A date that is not
YYYY-MM-DD, an end before the start, or a window over 366 days.Example bodies
Example
{
"error": {
"code": 400,
"type": "BAD_REQUEST",
"message": "Invalid date format or timezone. Use YYYY-MM-DD and a valid IANA timezone."
}
}Example
{
"error": {
"code": 400,
"type": "BAD_REQUEST",
"message": "end_date is before start_date."
}
}Example
{
"error": {
"code": 400,
"type": "BAD_REQUEST",
"message": "Ask for at most 366 days at a time (this request covers 400). For more history, walk it a year at a time."
}
}401
No key, or a key that is wrong or turned off.
403
Refused. The key is scoped away from this route, the request came from an address not on the key's allow-list, or the route refused the request (the message says why).
429
A rate limit was reached. Wait
Retry-After seconds. The body names the counter, the window and the limit.| Header | Type | Description |
|---|---|---|
Retry-After | integer | Seconds to wait before you try again. |
500
An unexpected fault on our side, or a search that ran longer than 30 seconds and was stopped.
503
Try again shortly. The key could not be checked just now (
Retry-After: 5), or a service this route needs could not be reached.| Header | Type | Description |
|---|---|---|
Retry-After | integer | Seconds to wait before you try again. |
List messages (old path)
GET/v1/Old path
The same as GET /v1/messages: the same parameters, the same answer, the same reports counter. Keys with the messaging scope can call it. Use GET /v1/messages.
Request
curl -G https://api.vocatech.com/v1/reports/messages \
-H "Authorization: Bearer YOUR_API_KEY" \
--data-urlencode "start_date=2026-09-21" \
--data-urlencode "end_date=2026-09-21" \
--data-urlencode "channel=any"const params = new URLSearchParams({
start_date: "2026-09-21",
end_date: "2026-09-21",
channel: "any",
});
const res = await fetch(`https://api.vocatech.com/v1/reports/messages?${params}`, {
headers: { Authorization: "Bearer YOUR_API_KEY" },
});
console.log(res.status, await res.json());import requests
r = requests.get(
"https://api.vocatech.com/v1/reports/messages",
headers={"Authorization": "Bearer YOUR_API_KEY"},
params={
"start_date": "2026-09-21",
"end_date": "2026-09-21",
"channel": "any",
},
timeout=30,
)
print(r.status_code, r.json())<?php
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client([
'base_uri' => 'https://api.vocatech.com',
'headers' => ['Authorization' => 'Bearer YOUR_API_KEY'],
'http_errors' => false,
]);
$res = $client->get('/v1/reports/messages', [
'query' => [
'start_date' => '2026-09-21',
'end_date' => '2026-09-21',
'channel' => 'any',
],
]);
echo $res->getStatusCode(), PHP_EOL, $res->getBody(), PHP_EOL;Parameters
| Name | In | Type | Description |
|---|---|---|---|
start_date | query | string (date) | First local day of the window, YYYY-MM-DD, in timezone. Send it with end_date. Send neither for today. A single date is ignored and you get today. For example 2026-09-01. |
end_date | query | string (date) | Last local day of the window, YYYY-MM-DD, included in full. At most 366 days after start_date. For example 2026-09-07. |
timezone | query | string | IANA time zone for the dates, for example America/Chicago. An unknown zone falls back to America/New_York. The response echoes the zone used. Default America/New_York. |
direction | query | string | incoming or outgoing. One of any, incoming or outgoing. Default any. |
status | query | string | Filters WhatsApp messages by their status, for example read. Texts always read delivered here, so delivered keeps every text and any other value leaves texts out. Default any. |
channel | query | string | text for texts, whatsapp for WhatsApp, any for both. One of any, text or whatsapp. Default any. |
search | query | string | A number or words to find in your number, the other party's number, or the message text. |
sort | query | string | Order by time. Rows that tie keep a stable order, so paging is safe. One of day_desc or day_asc. Default day_desc. |
page | query | integer | Page number, from 1. Default 1. At least 1. |
limit | query | integer | Rows per page, 1 to 500. A value outside that range is brought inside it. Default 500. From 1 to 500. |
company_id | query | integer | Optional. If you send it, it must be your own company id, or the request is refused with 403. The key already decides the company. |
Answers
200
The messages in the window.
| Field | Type | Description |
|---|---|---|
query required | object | The filters that were applied, echoed back. |
query. | integer | |
query. | string (date) | |
query. | string (date) | |
query. | string | |
query. | string | |
query. | string | |
query. | string | |
query. | string | |
messages required | array of objects | |
messages[]. required | string or null | For a text, the carrier's message id: the one a send in api or email mode returns, and the one on message webhooks. For WhatsApp, the WhatsApp message id. |
messages[]. required | string | incoming or outgoing. |
messages[]. required | string | A text always reads delivered here: this list does not carry carrier receipts (the message.status_updated webhook does). A WhatsApp message carries its own status, for example sent, delivered, read or failed. |
messages[]. required | string | Empty today. |
messages[]. required | string or null | The other party's number, digits without the leading 1. |
messages[]. required | string or null | Your number, digits without the leading 1. |
messages[]. required | string (date-time) or null | UTC, ISO 8601 with milliseconds. |
messages[]. required | string | One of text or whatsapp. |
messages[]. required | string | media when the message has an attachment. Otherwise a text reads webex, and a WhatsApp message carries its conversation type. |
messages[]. required | string | The message text, trimmed. |
messages[]. required | array of objects | |
messages[]. required | string | The attachment's media id. Fetch it with GET /v1/media/{id}. Matches ^att_[0-9]+$. |
messages[]. required | string or null | The file's type, for example image/jpeg. |
messages[]. required | string or null | |
messages[]. required | integer or null | Bytes, when known. |
meta required | object | Paging for list routes. Each list adds its own total. |
meta. required | integer | This page, from 1. |
meta. required | integer | Rows per page, after limits were applied. |
meta. required | integer | How many pages there are. |
meta. required | integer | Messages that match, across all pages. |
Example
{
"query": {
"company_id": 1234567890,
"start_date": "2026-09-21",
"end_date": "2026-09-21",
"direction": "any",
"status": "any",
"channel": "any",
"search": "",
"timezone": "America/New_York"
},
"messages": [
{
"message_id": "msg_4c1d7e",
"direction": "incoming",
"status": "delivered",
"remote_name": "",
"remote_number": "7185550142",
"group_number": "7185550100",
"sent_time": "2026-09-21T16:40:11.000Z",
"channel": "whatsapp",
"type": "media",
"body": "Here is the photo of the leak.",
"attachments": [
{
"id": "att_90211",
"content_type": "image/jpeg",
"filename": "photo-0921.jpg",
"size": 184233
}
]
},
{
"message_id": "msg_7f3a9c",
"direction": "incoming",
"status": "delivered",
"remote_name": "",
"remote_number": "7185550142",
"group_number": "7185550100",
"sent_time": "2026-09-21T15:22:04.000Z",
"channel": "text",
"type": "webex",
"body": "Running 10 minutes late, see you soon.",
"attachments": []
}
],
"meta": {
"page": 1,
"limit": 500,
"total_pages": 1,
"total_messages": 2
}
}400
A date that is not
YYYY-MM-DD, an end before the start, or a window over 366 days.Example bodies
Example
{
"error": {
"code": 400,
"type": "BAD_REQUEST",
"message": "Invalid date format or timezone."
}
}Example
{
"error": {
"code": 400,
"type": "BAD_REQUEST",
"message": "end_date is before start_date."
}
}401
No key, or a key that is wrong or turned off.
403
Refused. The key is scoped away from this route, the request came from an address not on the key's allow-list, or the route refused the request (the message says why).
429
A rate limit was reached. Wait
Retry-After seconds. The body names the counter, the window and the limit.| Header | Type | Description |
|---|---|---|
Retry-After | integer | Seconds to wait before you try again. |
500
An unexpected fault on our side, or a search that ran longer than 30 seconds and was stopped.
503
Try again shortly. The key could not be checked just now (
Retry-After: 5), or a service this route needs could not be reached.| Header | Type | Description |
|---|---|---|
Retry-After | integer | Seconds to wait before you try again. |
