Ketendo

Workforce

Employees, and the reference lists they depend on.

UAT https://uat-ketendo-api.azurewebsites.net  ·  Production https://ketendo-api.azurewebsites.net  ·  Last updated 2026-07-17
Required must be sent, and valid, or the call returns wrong/empty data Optional can be left out Recommended the variant to use for integrations
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:

VariantReturnsUse 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 }
If you send { "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/….

JSON on the wire is camelCase, and null fields are omitted from responses entirely - so don't treat a missing key as an error. Request field names are case-insensitive on the way in (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.

FieldTypeNotes
identifierintRequiredThe CompanyId to pull lookups for. Results are filtered strictly to this company.
includeDeletedboolOptionalDefaults 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).
searchCriteriastringOptionalCase-insensitive "contains" match against name, code and description.
sortBystringOptionalProperty name to sort on, e.g. "Name" or "Order".
sortDirectionintOptional1 = ascending (default), -1 = descending.
pageNumberintOptional1-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.
pageSizeintOptionalSee 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:

FieldTypeNotes
httpStatusCodeint200 on success.
codestringMachine-readable code for the endpoint, e.g. GetJobTitleListItems.
messagestringHuman-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.
resultarrayThe lookup items (shape depends on the variant - see samples below).
totalRecordsintTotal 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:

LookupIntegration endpoint RecommendedFull-record endpointMaps to
Job TitlePOST /v1/Employee/JobTitleListItemsPOST /v1/Employee/JobTitlesJobTitleId
Expense LinePOST /v1/Employee/ExpenseLineListItemsPOST /v1/Employee/ExpenseLinesExpenseLineId
Employee TypePOST /v1/Employee/EmployeeTypeListItemsPOST /v1/Employee/EmployeeTypesEmployeeTypeId
Rate BandPOST /v1/Employee/RateBandListItemsPOST /v1/Employee/RateBandsRateBandId
Employee LevelPOST /v1/Employee/EmployeeLevelListItemsPOST /v1/Employee/EmployeeLevelsEmployeeLevelId
Headcount ClassificationPOST /v1/Employee/HeadcountClassificationListItemsPOST /v1/Employee/HeadcountClassificationsHeadcountClassificationId
Service OfferingPOST /v1/Employee/ServiceOfferingListItemsPOST /v1/Employee/ServiceOfferingsServiceOfferingId
DisciplinePOST /v1/Employee/DisciplineListItemsPOST /v1/Employee/DisciplinesDisciplineId
CompetencyPOST /v1/Employee/CompetencyListItemsPOST /v1/Employee/CompetenciesCompetencyId
All Ids in these lookups are plain integers (not GUIDs), and they are stable per company - safe to cache on your side. Lookup responses are also cached server-side, so a value added in the Ketendo portal may take a short while to appear in the endpoint response.

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
}
FieldTypeNotes
idintSame id as the ListItems variant returns.
name / code / descriptionstringdescription is only available on this variant.
orderintDisplay order configured in the portal.
companyIdintEchoes the company you requested.
createdBy/Date, updatedBy/Dateguid / datetimeAudit trail. updatedBy/Date are omitted until the record is first edited.
deletedboolSoft-delete flag.
externalIdentifierstringThe 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.