KalHR logo

Guides

Limits and retries

How many requests a key can make, what to do when you're told to slow down, and how to retry a request without it happening twice.

Rate limit

Each key can make 300 requests a minute. Above that, the API answers 429 Too Many Requests with a Retry-After header giving the number of seconds to wait:

HTTP/1.1 429 Too Many Requests
Retry-After: 12

Wait that long, then send the same request again. The limit is per key, so two systems with their own keys don't slow each other down.

To stay under it:

  • Ask for 100 records a page rather than the default 25.
  • Use webhooks to hear about changes instead of polling for them.
  • Keep what you've already fetched, and fetch a record again only when a webhook says it changed.

If you need a higher limit, contact us and tell us what you're building.

Retry safely with an idempotency key

A request can time out after KalHR has done the work but before you get the answer. Sending it again could then do the work twice. An idempotency key prevents that.

Send an Idempotency-Key header with every request that changes something (POST, PUT, PATCH or DELETE). Use a new, unique value for each new request; a UUID is ideal:

Idempotency-Key: 6f1c2a9e-3b7d-4c55-9a0e-2d4f8b61c3a7

If you don't get an answer, send the request again with the same key. KalHR recognises it and returns the first answer instead of doing the work again. A replayed answer carries the header Idempotent-Replayed: true.

SituationAnswer
First request with a keyProcessed as normal; the answer is kept for 24 hours
Same key, same request, within 24 hoursThe first answer again, with Idempotent-Replayed: true
Same key while the first is still being processed409. Retry in a few seconds.
Same key, different request422. A key belongs to one request.
The first answer was 429 or a 5xxNot kept, so a retry runs the request again

Every sample in the reference that changes something sends a fresh key. Today's public endpoints only read, and reads are always safe to retry, so you'll need this when write endpoints arrive.

A retry helper

This wraps a request with the retry rules from Errors: it waits out 429, backs off on server errors and network failures, and gives up on anything that needs fixing.

async function withRetries(send, { attempts = 6 } = {}) {
  for (let attempt = 1; ; attempt++) {
    let response;
    try {
      response = await send();
    } catch (networkError) {
      if (attempt >= attempts) throw networkError;
      await sleep(2 ** attempt * 500);
      continue;
    }
    if (response.status === 429 && attempt < attempts) {
      await sleep(Number(response.headers.get("Retry-After") ?? 5) * 1000);
      continue;
    }
    if (response.status >= 500 && attempt < attempts) {
      await sleep(2 ** attempt * 500);
      continue;
    }
    return response;
  }
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// For a write, create the idempotency key once, outside the retries, so every attempt sends the same one.
const response = await withRetries(() =>
  fetch("https://api.kalhr.com/v1/employee/list?PageSize=100", {
    headers: { Authorization: `Bearer ${process.env.KALHR_API_KEY}` },
  })
);

Last updated October 19, 2026

Missing an endpoint or an event?

Tell us what you're building. The API grows from what integrators ask for.

See KalHR in action - 30 min demo