KalHR logo

Get started

Quickstart

Make your first call to the KalHR API, read every employee a page at a time, and react to a webhook. Complete code you can copy.

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.

GET v1/integrations/api-keys/me
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.

  1. 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_…".
  2. 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.
  3. 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

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