# Rate limits

> The TimeTracking API allows 600 requests per minute per API key; how the window works, what a 429 looks like, and how to stay under the limit with batch endpoints and pagination.

URL: https://timetracking.tech/developers/api/rate-limits
Last updated: 2026-10-24

| Limit | Value |
|---|---|
| Requests per API key | **600 per minute** |
| Window | Fixed calendar minute (UTC), counted per key; resets at `hh:mm:00` |
| Applies to | Requests authenticated with an API key (`tt_live_…`) |
| Batch size | 500 items per batch request |
| Page size | `limit` 1–500 (default 100) |
| Other limits | Webhook delivery timeout 10 s; inbound email: max 10 attachments, 15 MB each, XLSX ≤ 5 MB and ≤ 20,000 rows; free orgs: 25 inbound emails per day |

## When you exceed it

```http
HTTP/1.1 429 Too Many Requests
retry-after: 60
content-type: application/json

{"error":{"code":"rate_limited","message":"Rate limit of 600 requests per minute exceeded"}}
```

`Retry-After` is always `60` seconds. Requests that hit the limit still count. Wait for `Retry-After`, or until the next calendar minute, before retrying.

## Staying under the limit

- **Batch writes.** `POST /people/batch`, `/clients/batch`, `/placements/batch` and `/claims/batch` take up to 500 rows in **one** request and report results per row.
- **Paginate with large pages.** `GET /days?from=…&to=…&limit=500`. Use `updated_since` for incremental syncs instead of re-reading everything (see [pagination](https://timetracking.tech/developers/api/pagination)).
- **Use webhooks instead of polling.** Fetch details only when an event arrives (see [webhooks](https://timetracking.tech/developers/webhooks/overview)).
- **Use one key per integration.** Each key has its own budget.
- **Spread scheduled jobs.** Avoid starting every cron job at `:00`.

## Retry pattern

```python
import time, requests

def call(method, url, **kw):
    for attempt in range(6):
        r = requests.request(method, url, timeout=30, **kw)
        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", "60")))
            continue
        if r.status_code >= 500:
            time.sleep(min(2 ** attempt, 30))
            continue          # pass the same Idempotency-Key on writes
        return r
    raise RuntimeError("gave up")
```

> **Planned:** a dedicated bulk export feed for BI, so you do not need high-volume polling.
