Guide
Limits and quotas
Bounds exist so one integration cannot degrade an organisation’s own use of the product. They are reported in response headers, so a well-written client paces itself instead of discovering them.
Rate limits
| Bucket | Limit | Window |
|---|---|---|
| Per API key | 600 requests | Rolling minute |
| Per organisation (all keys) | 3000 requests | Rolling minute |
Two buckets can refuse a request, so the headers report the binding one:
X-RateLimit-Remaining is the smaller of the two budgets, and
RateLimit-Policy names both windows.
HTTP/1.1 200 OK
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 483
X-RateLimit-Reset: 1785312000
RateLimit-Policy: 600;w=60;scope=key, 3000;w=60;scope=tenant
X-Request-Id: 0f0a2f6c-3f2f-4f7a-9f0e-2d1b8c4a55e1
A 429 carries Retry-After in seconds. Honour it — retrying sooner extends your own
window.
async function call(url, init, attempt = 0) {
const response = await fetch(url, init);
if (response.status !== 429) return response;
// Retry-After is authoritative; the exponential term only spreads a fleet
// of workers that all woke up at the same second.
const wait = Number(response.headers.get('retry-after') ?? '5') * 1000
+ Math.random() * 250 * 2 ** attempt;
if (attempt >= 5) throw new Error('rate limited');
await new Promise((r) => setTimeout(r, wait));
return call(url, init, attempt + 1);
}
Browser-based clients can read all of these: the headers above, plus
Deprecation, Sunset and Link, are exposed through CORS.
Pages
| Bound | Value | Notes |
|---|---|---|
| Default page size | 50 | When limit is omitted. |
| Maximum page size | 100 | A larger limit is clamped, not refused. |
| Cursor lifetime | Until the query changes |
A cursor belongs to the query that minted it — send the same filters (including
updatedSince) on every page of a sweep.
|
Date windows
Reads over a period take an inclusive from–to. A window wider than the
maximum is refused with 422 range_too_long rather than truncated, so a client never
mistakes a partial answer for a complete one.
| Operation | Maximum window |
|---|---|
GET /v1/work-days | 366 days |
GET /v1/reports/{type} | 366 days |
| Per-member day ranges, shift plans, rosters | Bounded per operation; see the reference |
Payloads
| Bound | Value |
|---|---|
| JSON request body | 12 MB |
| Bulk member import | 1–500 rows per request |
Idempotency-Key | 200 characters, deduplicated for 24 h |
| Webhook delivery attempts | 5, then the delivery is dead-lettered |
| Integration event retention | 90 days |
If a limit is in your way
Most limit trouble is a sweep that could be incremental. Before asking for a raise, check
whether the list you are polling accepts updatedSince
(pagination guide) or whether a
webhook would replace the poll entirely. If the
ceiling is genuinely too low for your use case, contact support with the workload — the limits
are per organisation and can be raised.