Quickstart
Five minutes from nothing to a paginated list of employees.
1. Create a key
In the app, go to Settings → API keys and choose Create key. You need
the api.manage permission; if the page is not there, ask whoever administers
your workspace to grant it.
Pick the narrowest scopes that do the job. employees:read and org:read are
enough for everything on this page.
The full key is displayed exactly once, at creation. We store a hash, so we cannot show it to you again and cannot recover it — if it is lost, revoke the key and create another. Put it straight into whatever your application uses for secrets.
A key looks like this:
opk_live_QyHoRPMowevteWjvLMV5pRB9jCkkwpzSqUD7cDuKnqyzzBdV
2. Check that it works
/v1/me describes the calling key and touches nothing else, so it is the
safest possible first request.
curl https://app.operentra.com/api/v1/me \
-H "Authorization: Bearer $OPERENTRA_API_KEY"
If that returns 200, you are authenticated. If it returns a
problem document, the code field says exactly what is wrong —
invalid_api_key for a bad key, insufficient_scope if it is valid but not
allowed here.
3. Read the workspace
Fetch /v1/company once and keep it. Every money field in the API is in this
workspace's currency and every calendar date is in its timezone; without those
two values you cannot correctly display anything else.
curl https://app.operentra.com/api/v1/company \
-H "Authorization: Bearer $OPERENTRA_API_KEY"
4. List employees
curl "https://app.operentra.com/api/v1/employees?limit=25" \
-H "Authorization: Bearer $OPERENTRA_API_KEY"
{
"data": [
{
"object": "employee",
"id": "e83f17a0-5771-4369-8a01-50c360d1caca",
"employee_code": "HH-EMP-0001",
"first_name": "Ayesha",
"last_name": "Khan",
"status": "active",
"joining_date": "2024-02-01",
"deleted": false,
"created_at": "2024-02-01T06:12:44.000Z",
"updated_at": "2026-08-19T11:03:10.000Z"
}
],
"has_more": true,
"next_cursor": "MjAyNC0wMi0wMVQwNjoxMjo0NC4wMDBafGU4M2Yx"
}
Note what is not there: no national ID, no salary, no bank account. Those
need employees.sensitive:read, and without it they are omitted rather than
returned as null — a missing key means "your key may not see this", never
"this employee has no such value". See Authentication.
5. Page through the rest
has_more tells you another page exists; pass next_cursor back as
starting_after.
curl "https://app.operentra.com/api/v1/employees?limit=25&starting_after=MjAyNC0w..." \
-H "Authorization: Bearer $OPERENTRA_API_KEY"
Do not build the cursor yourself — it is opaque and its contents will change. Pagination explains why this is a cursor and not a page number, and why that matters for a sync that runs while people are still working.
A first sync, in full
const KEY = process.env.OPERENTRA_API_KEY;
const BASE = 'https://app.operentra.com/api/v1';
async function fetchAll(resource, params = {}) {
const out = [];
let cursor = null;
do {
const query = new URLSearchParams({ ...params, limit: '100' });
if (cursor) query.set('starting_after', cursor);
const res = await fetch(`${BASE}/${resource}?${query}`, {
headers: { Authorization: `Bearer ${KEY}` },
});
if (!res.ok) {
// Branch on `code`, never on the prose in `title` or `detail`.
const problem = await res.json();
throw new Error(`${resource} failed: ${problem.code} (${problem.request_id})`);
}
const page = await res.json();
out.push(...page.data);
cursor = page.next_cursor;
} while (cursor);
return out;
}
const employees = await fetchAll('employees');
console.log(`${employees.length} employees`);
Next
- Writing data? Read Idempotency before your first POST. A
timed-out request that you retry without an
Idempotency-Keycreates a second record, and for employees that means a second person. - Syncing on a schedule? Use
updated_sincerather than refetching everything, and read Webhooks — being told about a change beats asking. - Hitting 429s? Rate limits explains the per-plan budget and how to stay inside it.