In depth
Rate limits
Written beside the API’s code, and read from it every hour.
Limits are counted per key, in fixed windows in UTC: each minute, each hour, and each day from 00:00 UTC. Each kind of request has its own counter, so polling reports never uses up your room to send a text.
| Counter | Counts | Per minute | Per hour | Per day |
|---|---|---|---|---|
default | Every request not listed below | 600 | 3,600 | 100,000 |
reports | GET /v1/calls, GET /v1/messages, /v1/reports/* | 300 | 3,000 | 100,000 |
messages | POST /v1/messages on a main key | 20 | None | 200 |
faxes | POST /v1/faxes, per account | 3 | None | 50 faxes sent (Eastern day) |
dial | POST /v1/agents/{id}/calls | 10 | None | 500 |
tickets | POST /v1/support/tickets | 10 | None | 200 |
knowledge | GET or POST /v1/knowledge/answer | 30 | None | 2,000 |
- Every request counts, including requests that were refused.
- A request without a live key (no key, a wrong key, a key turned off) counts against its source address instead: 60 a minute and 600 an hour.
- API Messaging keys are not held to the 20 and 200 on sends. Their queue paces them (see The API Messaging queue), under a backstop of 100,000 send requests a day.
- Texts sent with the main key also count toward the account's included texting limit, 50 an hour and 150 a day as standard (100 and 300 when Vocatech raises it), shared with the texts the account's people send. Past it the send is answered
429with the hour or the day named. API Messaging keys are paid per message and are outside that limit. - Faxes are counted per account, not per key. The 50 a day counts faxes actually sent: a dry run or a refused request does not use one.
- Faxes also stop at the account's monthly allowance of outbound pages, 1,000 for each fax line on the account unless Vocatech set another figure, pooled across the lines.
Over a limit, the API answers 429 with a Retry-After header: the seconds until the window resets. The body names the window, the limit and the counter:
{
"error": "Rate limit exceeded",
"period": "minute",
"limit": 20,
"counter": "messages",
"retry_after": 37
}When an API Messaging key's day allowance refuses a send, the body says so in words:
{
"error": "Rate limit exceeded",
"message": "This key is limited to 5000 messages per day. 4990 have been sent in the last 24 hours and 10 are waiting in the queue, so this send does not fit today.",
"period": "day",
"limit": 5000,
"used": 4990,
"queued": 10,
"retry_after": 36120
}Back off and retry
Wait for Retry-After when it is there. Without it, back off exponentially with a little jitter. Retry 429 and 503. Never retry a text send that answered 504 without checking GET /v1/messages first, and stop rather than sleep for hours on a day limit.
# curl retries 408, 429, 500, 502, 503 and 504 by itself and honors
# Retry-After (curl 7.66 or newer). Use it on reads, never on text sends.
curl --retry 5 --retry-max-time 120 \
-H "Authorization: Bearer $VOCATECH_API_KEY" \
https://api.vocatech.com/v1/callsimport { vt } from './vocatech.mjs';
const sleep = (s) => new Promise((resolve) => setTimeout(resolve, s * 1000));
export async function withRetry(call, tries = 5) {
for (let attempt = 1; ; attempt++) {
try {
return await call();
} catch (err) {
const retryable = err.status === 429 || err.status === 503;
if (!retryable || attempt >= tries) throw err;
const wait = err.retryAfter ?? Math.min(60, 2 ** attempt) + Math.random();
if (wait > 120) throw err; // a day window: stop, do not sleep for hours
await sleep(wait);
}
}
}
const calls = await withRetry(() => vt('GET', '/v1/calls'));import random
import time
from vocatech import VocatechError, vt
def with_retry(call, tries=5):
for attempt in range(1, tries + 1):
try:
return call()
except VocatechError as err:
if err.status not in (429, 503) or attempt == tries:
raise
wait = err.retry_after or min(60, 2 ** attempt) + random.random()
if wait > 120: # a day window: stop, do not sleep for hours
raise
time.sleep(wait)
calls = with_retry(lambda: vt("GET", "/v1/calls"))<?php
require __DIR__ . '/vocatech.php';
function vt_with_retry(callable $call, int $tries = 5): mixed
{
for ($attempt = 1; ; $attempt++) {
try {
return $call();
} catch (VocatechError $e) {
if (!in_array($e->status, [429, 503], true) || $attempt >= $tries) {
throw $e;
}
$wait = $e->retryAfter ?? min(60, 2 ** $attempt) + mt_rand(0, 1000) / 1000;
if ($wait > 120) {
throw $e; // a day window: stop, do not sleep for hours
}
usleep((int) ($wait * 1000000));
}
}
}
$calls = vt_with_retry(fn () => vt('GET', '/v1/calls'));To stay well under the limits, use webhooks instead of polling, and ask for 500 rows a page.
