Rate limits
Limits are per key, per environment. Reads and writes share one pool; payroll calculation has its own, because a run does far more work than a lookup.
| Pool | Default limit | Covers |
|---|---|---|
| General | 600 requests / minute | Everything except the calculation endpoints. |
| Payroll | 60 runs / hour | /payroll-runs/preview, /calculate and /recalculate. |
| Bulk | 10 requests / minute | /employees/bulk, /attendance/bulk and /imports. |
GET /me returns the limits in force for your key. Partners running payroll for many client companies at month end are usually raised on request - tell us the shape of the load.
Headers
| Header | Meaning |
|---|---|
| X-RateLimit-Limit | Requests allowed in the current window. |
| X-RateLimit-Remaining | Requests left in it. |
| X-RateLimit-Reset | Unix seconds at which the window resets. |
| Retry-After | On a 429 only: seconds to wait. |
Backing off
async function call(request, attempt = 0) {
const res = await fetch(request);
if (res.status !== 429 && res.status < 500) return res;
if (attempt >= 5) throw new Error('gave up after ' + attempt + ' retries');
const retryAfter = Number(res.headers.get('Retry-After'));
const backoff = Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1000
: 2 ** attempt * 500 + Math.random() * 250;
await new Promise((r) => setTimeout(r, backoff));
return call(request, attempt + 1);
}Month end is the spikeSpread bulk pushes across the days before the cut-off rather than the morning of the run, and use
updated_since to sync only what changed.