# Rate limits

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](https://vocatech.com/developers/guide/messages#ref-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 `429` with 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:

**Response429 Too Many RequestsCopy**

```json
{
  "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:

**Response429 Too Many RequestsCopy**

```json
{
  "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**

```bash
# 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/calls
```

**Node**

```javascript
import { 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'));
```

**Python**

```python
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**

```php
<?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.
