Developers

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

BucketLimitWindow
Per API key600 requestsRolling minute
Per organisation (all keys)3000 requestsRolling 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.

Every response
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.

Backoff
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

BoundValueNotes
Default page size50When 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 fromto. 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.

OperationMaximum window
GET /v1/work-days366 days
GET /v1/reports/{type}366 days
Per-member day ranges, shift plans, rosters Bounded per operation; see the reference

Payloads

BoundValue
JSON request body12 MB
Bulk member import1–500 rows per request
Idempotency-Key200 characters, deduplicated for 24 h
Webhook delivery attempts5, then the delivery is dead-lettered
Integration event retention90 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.