Skip to content
Chefotech HRMS
All documentation

Developers and API

Connecting to the API

The base URL, the one header that authenticates you, and a first working request in four languages.

Open this in the app

What the API is for

Everything the web app can do, another system can do too — read employees, push attendance from a turnstile, pull payslips into an accounting package, raise a leave request from an intranet. The web app is itself built on this API, so nothing is held back for internal use.

There are two ways to authenticate. People sign in with an email and password and receive a token that expires. Programs use an API key: one header, no expiry timer, and not tied to an individual who might leave the company. Use a key for anything unattended.

Find your base URL

Every path here is relative to your base URL — your API host followed by the version prefix. Open Settings → API & webhooks in the app: the guide there shows the exact base URL for your deployment with a copy button, because a self-hosted customer's differs from ours.

text
https://your-api-host/api/v1
So /employees means https://your-api-host/api/v1/employees

Create a key

In Settings → API & webhooks → API keys, create one and tick only the permissions the integration needs. A key can never do more than the person who created it, and a narrow key limits the damage if it leaks.

The key is shown once, at creation. We store only a hash of it, so it cannot be revealed again — copy it into your password manager or your server's environment variables straight away. If you lose it, revoke it and create another.

Your first request

Send the key in an x-api-key header. This lists five employees, which is the fastest proof that the key works and has read access.

bash
curl "https://your-api-host/api/v1/employees?limit=5" \
  -H "x-api-key: ct_live_xxxxxx_your_key_here"
javascript
const res = await fetch("https://your-api-host/api/v1/employees?limit=5", {
  headers: { "x-api-key": process.env.CHEFOTECH_API_KEY },
});

const body = await res.json();
if (!body.success) throw new Error(body.error.message);
console.log(body.data, body.meta);
python
import os, requests

res = requests.get(
    "https://your-api-host/api/v1/employees",
    headers={"x-api-key": os.environ["CHEFOTECH_API_KEY"]},
    params={"limit": 5},
    timeout=60,
)
body = res.json()
if not body["success"]:
    raise RuntimeError(body["error"]["message"])
print(body["data"])
php
<?php
$ch = curl_init("https://your-api-host/api/v1/employees?limit=5");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-api-key: " . getenv("CHEFOTECH_API_KEY")]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);

print_r($body["data"]);

What comes back

Every response uses the same envelope, so one parser handles all of them. Rows are in data; paging is in meta.

json
{
  "success": true,
  "data": [{ "id": "6a9d…", "employeeCode": "EMP0001", "fullName": "Rohan Deshmukh" }],
  "meta": { "page": 1, "limit": 5, "total": 12, "totalPages": 3, "hasNext": true, "hasPrev": false }
}

Failures use the same envelope with success false. Branch on error.code, which is stable, rather than on message, which is written for people and may be reworded.

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Enter a company name",
    "details": [{ "field": "name", "message": "Required" }]
  },
  "requestId": "ea4791db-a21c-41ce-907d-ab6a43b13022"
}

Quote requestId when reporting a problem — it locates the exact request in the server log.

Paging through a list

List endpoints all take the same parameters: page, limit, q for free-text search, and sort with a leading minus for descending. Loop until meta.hasNext is false rather than guessing how many pages there are.

javascript
async function* allEmployees(key) {
  let page = 1;
  for (;;) {
    const url = "https://your-api-host/api/v1/employees?limit=100&page=" + page;
    const res = await fetch(url, { headers: { "x-api-key": key } });
    const body = await res.json();

    yield* body.data;
    if (!body.meta.hasNext) return;
    page += 1;
  }
}

Status codes worth knowing

  • 401 — no key, or it is wrong, expired or revoked. Check the header name is exactly x-api-key.
  • 403 — the key is valid but was not granted that permission. The in-app endpoint reference names the one each endpoint needs.
  • 404 — no such record, or it belongs to another organization. We answer 404 rather than 403 so ids cannot be probed for existence.
  • 422 — validation failed; error.details names the field.
  • 429 — rate limited. The default is 300 requests a minute per IP; wait for the window to roll over and continue.
  • 202 — accepted and queued, used by long jobs such as payroll processing. Poll the resource until its status changes.

Related