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.
| Situation | Answer |
|---|---|
| First request with a key | Processed as normal; the answer is kept for 24 hours |
| Same key, same request, within 24 hours | The first answer again, with Idempotent-Replayed: true |
| Same key while the first is still being processed | 409. Retry in a few seconds. |
| Same key, different request | 422. A key belongs to one request. |
The first answer was 429 or a 5xx | Not 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}` },
})
);