> ## Documentation Index
> Fetch the complete documentation index at: https://developers.teambuilder.ge/llms.txt
> Use this file to discover all available pages before exploring further.

# Rate limits

> Per-key request limits, the headers that report them, and how to back off.

Each API key may make **60 requests per minute** by default. The limit is per key, not per IP address or per company, so separate integrations with separate keys never starve each other.

## Headers

Responses to authenticated requests, including a per-minute `429`, report the key's current per-minute window:

```http theme={null}
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1792488060
```

| Header | Meaning |
| - | - |
| `X-RateLimit-Limit` | Requests allowed per window for this key. |
| `X-RateLimit-Remaining` | Requests left in the current window. |
| `X-RateLimit-Reset` | When the window resets, as a Unix timestamp in seconds. |

A few roster sync responses carry no `X-RateLimit-*` headers: `411` and `413`, and a `409` or `429` refusal of a sync body larger than 100 KB, which is refused before the key's window is read. On a `429` for the hourly sync budget, the headers still describe the per-minute window; `Retry-After` is the value to follow.

## When you exceed the limit

The API returns `429 Too Many Requests` with a `Retry-After` header in seconds:

```http theme={null}
HTTP/1.1 429 Too Many Requests
Retry-After: 23
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1792488060
```

```json theme={null}
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Too many requests. Retry after 23 seconds.",
    "request_id": "req_4Kq8ZbT2xNw7RmYc9LpVh3"
  }
}
```

Wait at least `Retry-After` seconds before the next request. Do not retry in a tight loop: requests made while limited do not succeed.

<CodeGroup>
  ```javascript Node.js theme={null}
  async function request(url, init, attempts = 3) {
    for (let attempt = 1; ; attempt++) {
      const res = await fetch(url, init);
      if (res.status !== 429 || attempt === attempts) return res;
      const wait = Number(res.headers.get("Retry-After") ?? 1);
      await new Promise((resolve) => setTimeout(resolve, wait * 1000 + Math.random() * 250));
    }
  }
  ```

  ```python Python theme={null}
  import random, time, requests

  def request(method, url, attempts=3, **kwargs):
      for attempt in range(1, attempts + 1):
          res = requests.request(method, url, timeout=10, **kwargs)
          if res.status_code != 429 or attempt == attempts:
              return res
          time.sleep(int(res.headers.get("Retry-After", 1)) + random.random() / 4)
  ```
</CodeGroup>

## Roster sync limits

[`PUT /v1/employees/sync`](/guides/roster-sync) writes thousands of rows, so it has two extra limits on top of the per-minute one:

| Limit | Response | `Retry-After` |
| - | - | - |
| 30 syncs per hour per key | `429 rate_limit_exceeded` | Seconds until the hourly budget frees up |
| One sync per company at a time | `409 conflict` | `30` |

A `409` means another sync of your company is still running, or the platform is briefly busy with other syncs. It is not counted against your hourly budget. Retry after the delay.

## Staying within the limit

<AccordionGroup>
  <Accordion title="Cache on your server" icon="database">
    Leaderboards are cached by TeamBuilder for up to 30 seconds, and stats and catalogue data change slowly. Serve your pages from your own cache and refresh it on a timer, instead of calling the API on every page view. See [Leaderboards: refresh cadence](/guides/leaderboards#refresh-cadence).
  </Accordion>

  <Accordion title="Use the biggest page" icon="layer-group">
    Walk lists with `limit=100`. A full walk then costs one request per 100 items.
  </Accordion>

  <Accordion title="Prefer one grouped call" icon="object-group">
    For an overview of all departments, one `GET /v1/leaderboard/by-department` call replaces a filtered leaderboard call per department.
  </Accordion>

  <Accordion title="Watch the headers" icon="gauge">
    Slow down when `X-RateLimit-Remaining` runs low, rather than waiting for a `429`.
  </Accordion>
</AccordionGroup>

## Higher limits

If your integration genuinely needs more, for example a large company's nightly export, email [contact@teambuilder.ge](mailto:contact@teambuilder.ge) with the key's purpose and expected volume. Limits can be raised per key.

<Note>
  A separate per-IP ceiling protects the API before authentication. It is far above what any single key can use, so a well-behaved integration never meets it; if many keys share one egress IP and do hit it, the response is the same `429` with `Retry-After`.
</Note>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.