In depth
Pagination and dates
Written beside the API’s code, and read from it every hour.
List routes take page, from 1, and limit, and answer with meta:
meta in GET /v1/calls
{
"meta": {
"page": 2,
"limit": 500,
"total_pages": 4,
"total_calls": 1873
}
}| Route | limit | Total in meta |
|---|---|---|
GET /v1/calls | 1 to 500, default 500 | total_calls |
GET /v1/messages | 1 to 500, default 500 | total_messages |
GET /v1/faxes | 1 to 500, default 500 | total_faxes |
GET /v1/contacts | 1 to 500, default 100 | total_contacts |
GET /v1/webhooks/failures | 1 to 100, default 20 | total |
GET /v1/callers/{number} | 1 to 50 calls, default 10, no pages | counts |
GET /v1/messages/queue | Up to 200 items, no pages | queue.pending |
Dates and time zones
start_dateandend_dateareYYYY-MM-DD, and you send both or neither. Neither, or only one, means today.- They are read in
timezone, an IANA name such asAmerica/Chicago. The default isAmerica/New_York. A name the API does not know falls back to New York, so checkquery.timezonein the answer. - A request covers at most 366 days, and
end_datemay not come beforestart_date. Either answers400. For more history, walk a year at a time. - The answer echoes what it used in
query: the local dates and the time zone. - Every time in an answer is UTC, except
created_atin the send answers ofPOST /v1/messagesandPOST /v1/faxes, which carries New York time with its offset. Webhook endpoint, failure and queue listings write times as2026-09-24 13:06:53, also UTC.
Stable order
Rows that tie on the sort key come back in a fixed order, so a page never repeats or skips a row while the data stands still. New calls land while you page, though. To walk a range cleanly, sort day_asc, or walk a window that has closed, such as yesterday.
# Every page, oldest first, until page reaches meta.total_pages (uses jq)
page=1
while :; do
body=$(curl -s -H "Authorization: Bearer $VOCATECH_API_KEY" \
"https://api.vocatech.com/v1/messages?start_date=2026-09-01&end_date=2026-09-23&sort=day_asc&limit=500&page=$page")
echo "$body" | jq -c '.messages[]'
[ "$page" -ge "$(echo "$body" | jq '.meta.total_pages')" ] && break
page=$((page + 1))
doneimport { vt } from './vocatech.mjs';
// Walks every page of /v1/calls, /v1/messages or /v1/faxes, oldest first.
async function* walk(path, listKey, query = {}) {
for (let page = 1; ; page++) {
const qs = new URLSearchParams({ sort: 'day_asc', limit: '500', ...query, page: String(page) });
const data = await vt('GET', `${path}?${qs}`);
yield* data[listKey];
if (page >= data.meta.total_pages) return;
}
}
for await (const msg of walk('/v1/messages', 'messages', { start_date: '2026-09-01', end_date: '2026-09-23' })) {
console.log(msg.sent_time, msg.direction, msg.remote_number);
}from vocatech import vt
def walk(path, list_key, **query):
"""Every page of /v1/calls, /v1/messages or /v1/faxes, oldest first."""
page = 1
while True:
data = vt("GET", path, params={"sort": "day_asc", "limit": 500, **query, "page": page})
yield from data[list_key]
if page >= data["meta"]["total_pages"]:
return
page += 1
for msg in walk("/v1/messages", "messages", start_date="2026-09-01", end_date="2026-09-23"):
print(msg["sent_time"], msg["direction"], msg["remote_number"])<?php
require __DIR__ . '/vocatech.php';
// Every page of /v1/calls, /v1/messages or /v1/faxes, oldest first.
function vt_walk(string $path, string $listKey, array $query = []): Generator
{
for ($page = 1; ; $page++) {
$qs = http_build_query(array_merge(['sort' => 'day_asc', 'limit' => 500], $query, ['page' => $page]));
$data = vt('GET', $path . '?' . $qs);
yield from $data[$listKey];
if ($page >= $data['meta']['total_pages']) {
return;
}
}
}
foreach (vt_walk('/v1/messages', 'messages', ['start_date' => '2026-09-01', 'end_date' => '2026-09-23']) as $msg) {
echo $msg['sent_time'], ' ', $msg['direction'], ' ', $msg['remote_number'], PHP_EOL;
}For a nightly sync, walk yesterday in full each night. For a first import, walk a year at a time.
