Overviewwhat this module covers, and what it does not yet
What this module covers
Workforce is the people side of Ketendo - employees, their job titles, disciplines, rate bands and the rest of the reference data attached to them.
Today this guide documents the reference lists only. Those are the eighteen
lookup endpoints below, and they are what you need to resolve an id like JobTitleId
before sending it anywhere. Employee records themselves are not yet exposed through
/v1/integration/ - when they are, they will be documented here alongside the lookups,
following the same conventions as every other module.
1. ListItems or the plain plural - which one?
Use the …ListItems variant for integration lookups. Both variants exist for every lookup, both are POST, both take the exact same request body, and both run the same underlying query with the same filtering, searching, sorting and paging. The difference is only in what comes back:
| Variant | Returns | Use when |
|---|---|---|
…ListItems Recommended |
A lightweight list item per record: id, code, name, order, deleted |
You just need the Id (and code/name) to map onto your side - which is exactly the integration case (e.g. resolving JobTitleId, ExpenseLineId) |
Plain plural (e.g. JobTitles) |
The full record: everything above plus description, companyId, audit fields (createdBy/Date, updatedBy/Date), externalIdentifier |
You also need descriptions, audit metadata, or the externalIdentifier a record was synced under |
The Ids returned by both variants are identical - id from JobTitleListItems is the same value you would store as JobTitleId.
2. Does the request need CompanyId?
Yes - all nine lookups are company-scoped and the server filters strictly by company. But note the field is not called CompanyId in the request body: these endpoints take a generic request object where the company id goes in a field named identifier:
{ "identifier": 123 }
{ "companyId": 123 } instead of { "identifier": 123 }, the field is silently ignored, identifier defaults to 0, and you get an empty result (no error). If a lookup unexpectedly returns "result": [] with "totalRecords": 0, check this first.3. Headers every call needs
Authorization: Bearer <token>- same token you already use for the other integration endpoints.Content-Type: application/json
All endpoints in this guide are POST (even though they only read data) and live under /v1/Employee/….
Identifier and identifier both work).Common request bodyidentical for all 18 endpoints
Every lookup endpoint (both variants) accepts the same body. Only identifier is genuinely required; everything else has a default.
| Field | Type | Notes | |
|---|---|---|---|
identifier | int | Required | The CompanyId to pull lookups for. Results are filtered strictly to this company. |
includeDeleted | bool | Optional | Defaults to true - if you omit it, soft-deleted lookup values are included in the result. For integration purposes send false explicitly (or filter on the deleted flag that comes back on each item). |
searchCriteria | string | Optional | Case-insensitive "contains" match against name, code and description. |
sortBy | string | Optional | Property name to sort on, e.g. "Name" or "Order". |
sortDirection | int | Optional | 1 = ascending (default), -1 = descending. |
pageNumber | int | Optional | 1-based. Paging only kicks in when both pageNumber and pageSize are > 0; omit both to get all records in one call. Lookup lists are small, so pulling everything is the normal approach. |
pageSize | int | Optional | See above. |
Minimal integration request - active lookup values only, for company 123:
{
"identifier": 123,
"includeDeleted": false
}
Common response envelope
All responses come wrapped in the standard Ketendo envelope:
| Field | Type | Notes |
|---|---|---|
httpStatusCode | int | 200 on success. |
code | string | Machine-readable code for the endpoint, e.g. GetJobTitleListItems. |
message | string | Human-readable status text. Don't key any logic off this - a few endpoints carry copy-pasted labels (e.g. JobTitleListItems literally answers "Rate Band List Items successfully retrieved" and ExpenseLineListItems answers "Invoice Type List Items successfully retrieved"). That's cosmetic only - the data is correct. Use httpStatusCode / code instead. |
result | array | The lookup items (shape depends on the variant - see samples below). |
totalRecords | int | Total matching records before paging - equals result.length when you don't page. |
Lookups - worked samples
POST /v1/Employee/JobTitleListItemsJob Title lookup values (maps to JobTitleId)
Sample request
POST /v1/Employee/JobTitleListItems
Authorization: Bearer <token>
Content-Type: application/json
{
"identifier": 123,
"includeDeleted": false
}
Sample successful response (200)
{
"httpStatusCode": 200,
"code": "GetJobTitleListItems",
"message": "Rate Band List Items successfully retrieved",
"result": [
{
"id": 12,
"code": "JT-ENG",
"name": "Engineer",
"order": 1,
"deleted": false
},
{
"id": 15,
"code": "JT-PM",
"name": "Project Manager",
"order": 2,
"deleted": false
}
],
"totalRecords": 2
}
result[].id is the value to store as JobTitleId on your side. (The "Rate Band" wording in message is a known cosmetic label issue - see the response envelope section.)
POST /v1/Employee/ExpenseLineListItemsExpense Line lookup values (maps to ExpenseLineId)
Sample request
POST /v1/Employee/ExpenseLineListItems
Authorization: Bearer <token>
Content-Type: application/json
{
"identifier": 123,
"includeDeleted": false
}
Sample successful response (200)
{
"httpStatusCode": 200,
"code": "GetExpenseLineListItems",
"message": "Invoice Type List Items successfully retrieved",
"result": [
{
"id": 4,
"code": "EL-TRAVEL",
"name": "Travel",
"order": 1,
"deleted": false
},
{
"id": 7,
"code": "EL-ACCOM",
"name": "Accommodation",
"order": 2,
"deleted": false
}
],
"totalRecords": 2
}
result[].id is the value to store as ExpenseLineId on your side. (The "Invoice Type" wording in message is a known cosmetic label issue.)
All lookup endpoints
All nine lookups behave identically - same request body, same response envelope, same list-item shape. Swap the path and you're done:
| Lookup | Integration endpoint Recommended | Full-record endpoint | Maps to |
|---|---|---|---|
| Job Title | POST /v1/Employee/JobTitleListItems | POST /v1/Employee/JobTitles | JobTitleId |
| Expense Line | POST /v1/Employee/ExpenseLineListItems | POST /v1/Employee/ExpenseLines | ExpenseLineId |
| Employee Type | POST /v1/Employee/EmployeeTypeListItems | POST /v1/Employee/EmployeeTypes | EmployeeTypeId |
| Rate Band | POST /v1/Employee/RateBandListItems | POST /v1/Employee/RateBands | RateBandId |
| Employee Level | POST /v1/Employee/EmployeeLevelListItems | POST /v1/Employee/EmployeeLevels | EmployeeLevelId |
| Headcount Classification | POST /v1/Employee/HeadcountClassificationListItems | POST /v1/Employee/HeadcountClassifications | HeadcountClassificationId |
| Service Offering | POST /v1/Employee/ServiceOfferingListItems | POST /v1/Employee/ServiceOfferings | ServiceOfferingId |
| Discipline | POST /v1/Employee/DisciplineListItems | POST /v1/Employee/Disciplines | DisciplineId |
| Competency | POST /v1/Employee/CompetencyListItems | POST /v1/Employee/Competencies | CompetencyId |
Full-record (plural) response shape
If you do call the plain plural variant (e.g. POST /v1/Employee/JobTitles), each item in result is the full record instead of the trimmed list item. Same envelope, same request body:
{
"httpStatusCode": 200,
"code": "GetJobTitles",
"message": "Rate Bands successfully retrieved",
"result": [
{
"id": 12,
"name": "Engineer",
"code": "JT-ENG",
"description": "Engineering job family",
"order": 1,
"companyId": 123,
"createdBy": "6f9619ff-8b86-d011-b42d-00cf4fc964ff",
"createdDate": "2025-11-03T08:14:22+00:00",
"updatedBy": "6f9619ff-8b86-d011-b42d-00cf4fc964ff",
"updatedDate": "2026-02-10T10:01:05+00:00",
"deleted": false,
"externalIdentifier": "HR-JT-0001"
}
],
"totalRecords": 1
}
| Field | Type | Notes |
|---|---|---|
id | int | Same id as the ListItems variant returns. |
name / code / description | string | description is only available on this variant. |
order | int | Display order configured in the portal. |
companyId | int | Echoes the company you requested. |
createdBy/Date, updatedBy/Date | guid / datetime | Audit trail. updatedBy/Date are omitted until the record is first edited. |
deleted | bool | Soft-delete flag. |
externalIdentifier | string | The external key the record was created/synced under, if any. Omitted when not set. |
Remember: null fields are omitted from the JSON entirely, so e.g. description simply won't be present on records that don't have one.