By the end of this page you will have called the API with a key, read your organisation's whole employee list, and run a small server that hears about approved leave the moment it happens. Allow about ten minutes.
Before you start
You need an API key with two kinds of access: Employee Management: View and Leave: View. Keys are created in KalHR by an administrator, under Settings → API keys. Authentication explains how, step by step.
The key is shown once. Keep it out of your code: every sample in these pages reads it from an environment variable.
export KALHR_API_KEY="kal_live_…"
1. Check the key
Ask the API which key you're using. Pick your language; every sample on the site follows your choice.
curl "https://api.kalhr.com/v1/integrations/api-keys/me" \
-H "Authorization: Bearer $KALHR_API_KEY"A working key answers with its name, the organisation it acts for, and what it may do:
{
"isSuccessful": true,
"code": "00",
"message": "Success",
"data": {
"apiKeyId": "3f2a9c1e-5b7d-4e8a-9c21-7d4e5f6a8b90",
"name": "HR sync",
"organizationId": "8c1d0e2f-4a6b-4c8d-9e0f-1a2b3c4d5e6f",
"permissions": ["GetDepartment", "GetEmployee", "GetLeaveRequests", "GetLeaveTypes"]
}
}
If you get 401 instead, the key was mistyped, revoked or has expired; the message says which.
2. Read every employee
Lists come back one page at a time. Keep asking for the next page until you reach totalPages. The loops below also
wait and retry when the API asks you to slow down (429).
JavaScript (Node.js 20 or later). Save it as employees.mjs and run node employees.mjs.
const BASE = "https://api.kalhr.com/";
const headers = { Authorization: `Bearer ${process.env.KALHR_API_KEY}` };
async function allEmployees() {
const employees = [];
for (let page = 1; ; page++) {
const response = await fetch(`${BASE}v1/employee/list?PageNumber=${page}&PageSize=100`, { headers });
if (response.status === 429) {
const wait = Number(response.headers.get("Retry-After") ?? 5);
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
page--; // ask for the same page again
continue;
}
const result = await response.json();
if (!result.isSuccessful) throw new Error(result.message);
employees.push(...result.data.data);
if (page >= result.data.totalPages) return employees;
}
}
const employees = await allEmployees();
console.log(`${employees.length} employees`);
for (const e of employees) {
// id is KalHR's id for the record; employeeId is your own employee number.
console.log(e.id, e.employeeId, e.firstName, e.lastName, e.email);
}
Python (with pip install requests). Save it as employees.py and run python employees.py.
import os, time
import requests
BASE = "https://api.kalhr.com/"
HEADERS = {"Authorization": f"Bearer {os.environ['KALHR_API_KEY']}"}
def all_employees():
employees, page = [], 1
while True:
response = requests.get(f"{BASE}v1/employee/list",
params={"PageNumber": page, "PageSize": 100},
headers=HEADERS, timeout=30)
if response.status_code == 429:
time.sleep(int(response.headers.get("Retry-After", 5)))
continue # ask for the same page again
result = response.json()
if not result["isSuccessful"]:
raise RuntimeError(result.get("message"))
employees += result["data"]["data"]
if page >= result["data"]["totalPages"]:
return employees
page += 1
for e in all_employees():
# id is KalHR's id for the record; employeeId is your own employee number.
print(e["id"], e["employeeId"], e["firstName"], e["lastName"], e["email"])
To read only some employees, add filters such as Status=Active or DepartmentId=…. The
Employees reference lists them all.
3. React to approved leave
Instead of asking KalHR for changes every few minutes, let it tell you. A webhook is a URL on your server that KalHR calls, signed, when something happens. This receiver checks the signature, answers straight away, and then fetches the approved request with your API key.
- In KalHR, open Settings → Webhooks and select Add endpoint. Point it at your server's
/kalhr-webhooks, choose the leave.approved event, and store the signing secret it shows you:export KALHR_WEBHOOK_SECRET="whsec_…". - Run the receiver below. Your server must be reachable from the internet over
https; while developing, a tunnelling tool such as ngrok or Cloudflare Tunnel gives your machine a public address. - Select Test on the endpoint in KalHR to check it's reachable, then approve a leave request.
JavaScript (Node.js 20 or later, Express). Run npm install express, save it as receiver.js and run
node receiver.js.
const crypto = require("crypto");
const express = require("express");
const BASE = "https://api.kalhr.com/";
const app = express();
function verifyKalhr(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=", 2)));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const handled = new Set(); // use your database in production
// express.raw keeps the body exactly as sent: the signature is over those bytes.
app.post("/kalhr-webhooks", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
if (!verifyKalhr(raw, req.get("KalHR-Signature") || "", process.env.KALHR_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}
res.sendStatus(200); // answer within 10 seconds, then do the work
const event = JSON.parse(raw);
if (handled.has(event.id)) return; // an event can arrive more than once
handled.add(event.id);
if (event.type === "leave.approved") onLeaveApproved(event.data).catch(console.error);
});
async function onLeaveApproved({ leaveRequestId }) {
const response = await fetch(`${BASE}v1/leaverequest/details/${leaveRequestId}`, {
headers: { Authorization: `Bearer ${process.env.KALHR_API_KEY}` },
});
const result = await response.json();
if (!result.isSuccessful) throw new Error(result.message);
const leave = result.data;
console.log(`${leave.employeeName}: ${leave.leaveTypeName}, ${leave.startDate} to ${leave.endDate}`);
// Block the dates in your rota or calendar here.
}
app.listen(3000, () => console.log("Listening on :3000"));
The same check in Python, C# and PHP is on Verifying signatures.
Where to go next
- Requests and responses: the envelope, paging, dates and filters
- API reference: every endpoint a key can call, with its fields and an example answer
- Event catalogue: every webhook event and what it carries
- Security checklist: what to check before going live