KalHR logo

Guides

Requests and responses

The API's address, the envelope every answer comes in, and how ids, dates, numbers, pages and filters work.

The KalHR API is JSON over HTTPS. Every answer has the same shape, so one small helper in your code can handle all of them.

The address

Every request goes to:

https://api.kalhr.com/

followed by the endpoint's path, for example https://api.kalhr.com/v1/employee/list. The v1 in the path is the version of that endpoint.

The envelope

Every answer, successful or not, is an object with four fields:

{
  "isSuccessful": true,
  "code": "00",
  "message": "Success",
  "data": { }
}
FieldMeaning
isSuccessfultrue when the request did what you asked. Always check this.
code"00" when it worked. When it didn't: "ValidationError" if a value you sent failed a check, otherwise "01"
messageWhen it didn't work, why, in a sentence you can log or show
dataThe result: one record, a list, or a page of results

A request that KalHR understood but refused for a business reason, such as an id that isn't in your organisation, answers HTTP 200 with isSuccessful: false. Checking only the status code would treat it as success. See Errors.

A small helper that does both checks:

async function kalhr(path) {
  const response = await fetch(`https://api.kalhr.com/${path}`, {
    headers: { Authorization: `Bearer ${process.env.KALHR_API_KEY}` },
  });
  const result = await response.json().catch(() => null);
  if (!response.ok || !result?.isSuccessful) {
    throw new Error(`${response.status}: ${result?.message ?? response.statusText}`);
  }
  return result.data;
}

Fields and values

Missing means empty. A field with no value is left out of the answer rather than sent as null. Treat a missing field as empty, and don't fail on fields you don't recognise: new ones are added over time.

Ids are UUIDs, such as 4b9d2c1e-7a3f-4e8b-9c21-5d6e7f8a9b0c. Use the id of a record to fetch it or to filter by it. A few records also carry a number people use (an employee's employeeId is their employee number, a payroll's payrollCode its code); those are for display and matching, not for calling the API.

Choices (a status, a work mode) come either as a number or as a name, and each reference page says which. Where a field is a number, the page lists what each number means. Filters accept the number or the name: LeaveStatus=2 and LeaveStatus=Approved are the same.

Money is a JSON number in naira, such as 650000 or 512340.5. Don't round it before you store it.

Dates and times

KindLooks likeRead it as
A calendar date (a leave day, a pay day, a date of birth)2026-11-02T00:00:00The date part only. The time means nothing.
A moment (when something was created or requested)2026-10-14T08:02:55.381UTC, even without a Z
A moment with a Z2026-10-19T14:30:00ZUTC

All moments are UTC. Some are written without the Z; read a time with no offset as UTC, never as your local time.

When you send a date, as a filter, write it as YYYY-MM-DD: StartDate=2026-11-01.

Pages

Lists come back a page at a time, inside data:

{
  "isSuccessful": true,
  "code": "00",
  "message": "Success",
  "data": {
    "data": [ { "id": "…" }, { "id": "…" } ],
    "pageNumber": 2,
    "pageSize": 50,
    "totalRecords": 134,
    "totalPages": 3
  }
}
FieldMeaning
dataThis page's records
pageNumberThe page you asked for
pageSizeRecords per page
totalRecordsRecords across all pages
totalPagesHow many pages there are

Ask for a page with PageNumber (from 1) and PageSize (25 when you don't say). Keep asking for the next page until pageNumber reaches totalPages. A PageSize of 100 keeps the number of requests down without making each one slow.

GET /v1/employee/list?PageNumber=2&PageSize=50

Ignore a totalCount field if you see one; it is always 0. Use totalRecords.

Filters

Filters are query parameters. Their names are written as in the reference, with a capital first letter, though the API doesn't mind the case.

A filter that takes several ids repeats the parameter:

GET /v1/employee/list?Companies=1f2e3d4c-…&Companies=7a8b9c0d-…

Filters combine: Status=Active&DepartmentId=… returns active employees in that department.

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