MASKDocs

Integration API API reference

Employees

People and their memberships, one per institution they work at, with contract type, hourly rate and dates. Reads need employees:read, writes employees:write.

An employee is a person who clocks in. Their memberships are their employment at an institution: one per institution, with the contract type, the hourly rate and the dates. The MASK id is the only identifier; there is no field for your own reference, so keep a mapping on your side.

List employees#

GET/integration/employeesScope: employees:read

The people of the key's institutions (or of one), oldest first, with their memberships there. 403 institution_not_allowed for an institutionId outside the key.

An employee has id, firstName, lastName, email, personalId, language (en, bs, tr), status (active, inactive, terminated), createdAt, updatedAt, and memberships[] in the key's institutions, active and ended.

curl -s "$MASK/integration/employees?institutionId=4&page=1&pageSize=50" -H "Authorization: Bearer $KEY"
{
  "list": [
    {
      "id": 24,
      "firstName": "Ana",
      "lastName": "Primjer-Kovač",
      "email": "ana.kovac@example.com",
      "personalId": "EX-1001",
      "language": "en",
      "status": "active",
      "createdAt": 1793812864,
      "updatedAt": 1793812906,
      "memberships": [
        {"institutionId": 1, "contractType": "full_time", "hourlyRate": 12.5, "startedOn": "2026-11-01", "status": "active", "createdAt": 1793812864},
        {"institutionId": 4, "contractType": "part_time", "hourlyRate": 10.0, "startedOn": "2026-11-03", "endedOn": "2026-11-03",
         "status": "ended", "createdAt": 1793812878, "updatedAt": 1793812987}
      ]
    }
  ],
  "count": 1,
  "page": 1,
  "pageSize": 50,
  "totalPages": 1
}

Parameters

  • institutionIdqueryinteger

    One of the key's institutions; omitted = all of them

  • statusquerystring

    active | inactive | terminated | all

    default "all"

  • pagequeryinteger

    1-based

    default 1

  • pageSizequeryinteger

    1-200

    default 50

Responses

200OKEmployee page
Fields of Employee page
  • countinteger
  • listarray of Employee
    Fields of Employee
    • createdAtinteger

      Epoch seconds, UTC

    • emailstring
    • firstNamestring
    • idinteger
    • languagestring

      en, bs or tr

    • lastNamestring
    • membershipsarray of Membership

      Memberships in the key's institutions, active and ended

      Fields of Membership
      • contractTypestring
      • createdAtinteger

        Epoch seconds, UTC

      • endedOndate
      • hourlyRatenumber

        Two decimals; absent when not set

      • institutionIdinteger
      • startedOndate
      • statusstring

        active or ended

      • updatedAtinteger
    • personalIdstring
    • statusstring

      active, inactive or terminated

    • updatedAtinteger
  • pageinteger
  • pageSizeinteger
  • totalPagesinteger
Errors this route can answer
  • 400Bad Requestvalidation_failedidempotency_requiredevent_too_old
  • 401Unauthorizedinvalid_api_keykey_expiredkey_revoked
  • 403Forbiddenip_not_allowedscope_missinginstitution_not_allowednot_api_eventadmin_linked_employee
  • 429Too Many Requestsrate_limited
  • 500Internal Server Errorinternal_error
  • 503Service Unavailableunavailable

Each error has the same JSON body; see Errors for what every code means.

Get employee#

GET/integration/employees/{id}Scope: employees:read

One person with their memberships in the key's institutions; 404 when unknown or outside the key.

The same person as in the list. With a key that covers only institution 1, GET /integration/employees/24 answers Ana with only her institution 1 membership.

Parameters

  • idpathintegerrequired

Responses

200OKEmployee
Fields of Employee
  • createdAtinteger

    Epoch seconds, UTC

  • emailstring
  • firstNamestring
  • idinteger
  • languagestring

    en, bs or tr

  • lastNamestring
  • membershipsarray of Membership

    Memberships in the key's institutions, active and ended

    Fields of Membership
    • contractTypestring
    • createdAtinteger

      Epoch seconds, UTC

    • endedOndate
    • hourlyRatenumber

      Two decimals; absent when not set

    • institutionIdinteger
    • startedOndate
    • statusstring

      active or ended

    • updatedAtinteger
  • personalIdstring
  • statusstring

    active, inactive or terminated

  • updatedAtinteger
Errors this route can answer
  • 400Bad Requestvalidation_failedidempotency_requiredevent_too_old
  • 401Unauthorizedinvalid_api_keykey_expiredkey_revoked
  • 403Forbiddenip_not_allowedscope_missinginstitution_not_allowednot_api_eventadmin_linked_employee
  • 404Not Foundnot_found
  • 429Too Many Requestsrate_limited
  • 500Internal Server Errorinternal_error
  • 503Service Unavailableunavailable

Each error has the same JSON body; see Errors for what every code means.

Create employee#

POST/integration/employeesScope: employees:write

Create an active employee with at least one membership, each in an institution of the key.

An institution listed twice is 400, one outside the key 403 institution_not_allowed, an archived one 409; an all-institutions key that names an institution that does not exist gets a 404. language is en when omitted. Keep the returned id: it is how you name this person from now on.

curl -s -X POST "$MASK/integration/employees" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: hr-sync-2026-11-04-ana-primjer' \
  -d '{"firstName":"Ana","lastName":"Primjer","email":"ana.primjer@example.com","personalId":"EX-1001","language":"bs",
       "memberships":[{"institutionId":1,"contractType":"full_time","hourlyRate":12.50,"startedOn":"2026-11-01"}]}'

201 Created:

{
  "id": 24,
  "firstName": "Ana",
  "lastName": "Primjer",
  "email": "ana.primjer@example.com",
  "personalId": "EX-1001",
  "language": "bs",
  "status": "active",
  "createdAt": 1793812864,
  "memberships": [
    {"institutionId": 1, "contractType": "full_time", "hourlyRate": 12.5, "startedOn": "2026-11-01", "status": "active", "createdAt": 1793812864}
  ]
}

Parameters

  • Idempotency-Keyheaderstring

    Optional. 1 to 128 visible ASCII characters. A retry with the same key and request within 24 h gets the first reply back (Idempotent-Replayed: true) instead of writing again

Request body

JSON, EmployeeCreateRequest. Fields not listed here are ignored.

  • emailstring

    at most 255 characters

  • firstNamestringrequired

    at most 100 characters

  • languagestring

    en, bs or tr; en when omitted

    one of en, bs, tr

  • lastNamestringrequired

    at most 100 characters

  • membershipsarray of MembershipCreateRequestrequired

    at least 1 item

    Fields of MembershipCreateRequest
    • contractTypestring

      at most 50 characters

    • hourlyRatenumber

      At least 0, at most two decimals

      at least 0

    • institutionIdintegerrequired
    • startedOndate

      ISO date

  • personalIdstring

    at most 50 characters

Responses

201The employee as the key sees itEmployee
Fields of Employee
  • createdAtinteger

    Epoch seconds, UTC

  • emailstring
  • firstNamestring
  • idinteger
  • languagestring

    en, bs or tr

  • lastNamestring
  • membershipsarray of Membership

    Memberships in the key's institutions, active and ended

    Fields of Membership
    • contractTypestring
    • createdAtinteger

      Epoch seconds, UTC

    • endedOndate
    • hourlyRatenumber

      Two decimals; absent when not set

    • institutionIdinteger
    • startedOndate
    • statusstring

      active or ended

    • updatedAtinteger
  • personalIdstring
  • statusstring

    active, inactive or terminated

  • updatedAtinteger
Errors this route can answer
  • 400Bad Requestvalidation_failedidempotency_requiredevent_too_old
  • 401Unauthorizedinvalid_api_keykey_expiredkey_revoked
  • 403Forbiddenip_not_allowedscope_missinginstitution_not_allowednot_api_eventadmin_linked_employee
  • 404Not Foundnot_found
  • 409Conflictconflictidempotency_in_progress
  • 422Unprocessable Contentidempotency_mismatch
  • 429Too Many Requestsrate_limited
  • 500Internal Server Errorinternal_error
  • 503Service Unavailableunavailable

Each error has the same JSON body; see Errors for what every code means.

Update employee#

PATCH/integration/employees/{id}Scope: employees:write

Change an employee's master data or status; every active membership of the person must lie in the key's institutions, and the status of a protected employee record cannot change.

Send at least one field; an omitted field keeps its value. Clearing a value is not possible in v1 (a blank one is 400). Leaving active signs the person out of the employee portal, and their card taps are refused from then on.

A PATCH that changes nothing (every field equal to the stored value; names and the email are compared without surrounding spaces) writes nothing and answers the employee as it is. updatedAt stays the same, so pushing your full state every night is safe and updatedAt still tells you what really changed.

The guards, in this order after the scope:

  • Every active membership of the person must lie in the key's institutions (for a key that lists institutions), otherwise 403 institution_not_allowed. Ended memberships elsewhere don't count; all-institutions keys are exempt.
  • The status of some protected employee records cannot be changed through the API (403 admin_linked_employee). Re-sending the current status is accepted and leaves it as it is.
  • 409 conflict when every institution through which the key sees the person is archived.
curl -s -X PATCH "$MASK/integration/employees/24" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"lastName":"Primjer-Kovač","email":"ana.kovac@example.com"}'

200 OK, the employee with the new values and "updatedAt": 1793812878. The guard for people who also work elsewhere, for a key that covers only institution 1 while the person has an active membership in 4:

{
  "status": "error",
  "code": "institution_not_allowed",
  "message": "Employee has an active membership outside this key's institutions",
  "requestId": "ffad5ceb-3614-4d9c-bdb7-c5b801155058"
}

Parameters

  • idpathintegerrequired
  • Idempotency-Keyheaderstring

    Optional. 1 to 128 visible ASCII characters. A retry with the same key and request within 24 h gets the first reply back (Idempotent-Replayed: true) instead of writing again

Request body

JSON, EmployeeUpdateRequest. Fields not listed here are ignored.

  • emailstring

    at most 255 characters

  • firstNamestring

    at most 100 characters

  • languagestring

    one of en, bs, tr

  • lastNamestring

    at most 100 characters

  • personalIdstring

    at most 50 characters

  • statusstring

    active, inactive or terminated; leaving active signs the person out of the employee portal. Refused (403 admin_linked_employee) for a protected employee record

    one of active, inactive, terminated

Responses

200OKEmployee
Fields of Employee
  • createdAtinteger

    Epoch seconds, UTC

  • emailstring
  • firstNamestring
  • idinteger
  • languagestring

    en, bs or tr

  • lastNamestring
  • membershipsarray of Membership

    Memberships in the key's institutions, active and ended

    Fields of Membership
    • contractTypestring
    • createdAtinteger

      Epoch seconds, UTC

    • endedOndate
    • hourlyRatenumber

      Two decimals; absent when not set

    • institutionIdinteger
    • startedOndate
    • statusstring

      active or ended

    • updatedAtinteger
  • personalIdstring
  • statusstring

    active, inactive or terminated

  • updatedAtinteger
Errors this route can answer
  • 400Bad Requestvalidation_failedidempotency_requiredevent_too_old
  • 401Unauthorizedinvalid_api_keykey_expiredkey_revoked
  • 403Forbiddenip_not_allowedscope_missinginstitution_not_allowednot_api_eventadmin_linked_employee
  • 404Not Foundnot_found
  • 409Conflictconflictidempotency_in_progress
  • 422Unprocessable Contentidempotency_mismatch
  • 429Too Many Requestsrate_limited
  • 500Internal Server Errorinternal_error
  • 503Service Unavailableunavailable

Each error has the same JSON body; see Errors for what every code means.

Add membership#

POST/integration/employees/{id}/membershipsScope: employees:write

Add a membership in an institution of the key (409 if the employee already has one there)

Answers 201 with the employee. The 409 conflict also covers an ended membership there: reactivate it with the membership PATCH instead.

curl -s -X POST "$MASK/integration/employees/24/memberships" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: hr-sync-membership-24-4' \
  -d '{"institutionId":4,"contractType":"part_time","hourlyRate":10,"startedOn":"2026-11-03"}'
{
  "id": 24,
  "firstName": "Ana",
  "lastName": "Primjer-Kovač",
  "email": "ana.kovac@example.com",
  "personalId": "EX-1001",
  "language": "bs",
  "status": "active",
  "createdAt": 1793812864,
  "updatedAt": 1793812878,
  "memberships": [
    {"institutionId": 1, "contractType": "full_time", "hourlyRate": 12.5, "startedOn": "2026-11-01", "status": "active", "createdAt": 1793812864},
    {"institutionId": 4, "contractType": "part_time", "hourlyRate": 10.0, "startedOn": "2026-11-03", "status": "active", "createdAt": 1793812878}
  ]
}

Parameters

  • idpathintegerrequired
  • Idempotency-Keyheaderstring

    Optional. 1 to 128 visible ASCII characters. A retry with the same key and request within 24 h gets the first reply back (Idempotent-Replayed: true) instead of writing again

Request body

JSON, MembershipCreateRequest. Fields not listed here are ignored.

  • contractTypestring

    at most 50 characters

  • hourlyRatenumber

    At least 0, at most two decimals

    at least 0

  • institutionIdintegerrequired
  • startedOndate

    ISO date

Responses

201The employee as the key sees itEmployee
Fields of Employee
  • createdAtinteger

    Epoch seconds, UTC

  • emailstring
  • firstNamestring
  • idinteger
  • languagestring

    en, bs or tr

  • lastNamestring
  • membershipsarray of Membership

    Memberships in the key's institutions, active and ended

    Fields of Membership
    • contractTypestring
    • createdAtinteger

      Epoch seconds, UTC

    • endedOndate
    • hourlyRatenumber

      Two decimals; absent when not set

    • institutionIdinteger
    • startedOndate
    • statusstring

      active or ended

    • updatedAtinteger
  • personalIdstring
  • statusstring

    active, inactive or terminated

  • updatedAtinteger
Errors this route can answer
  • 400Bad Requestvalidation_failedidempotency_requiredevent_too_old
  • 401Unauthorizedinvalid_api_keykey_expiredkey_revoked
  • 403Forbiddenip_not_allowedscope_missinginstitution_not_allowednot_api_eventadmin_linked_employee
  • 404Not Foundnot_found
  • 409Conflictconflictidempotency_in_progress
  • 422Unprocessable Contentidempotency_mismatch
  • 429Too Many Requestsrate_limited
  • 500Internal Server Errorinternal_error
  • 503Service Unavailableunavailable

Each error has the same JSON body; see Errors for what every code means.

Update membership#

PATCH/integration/employees/{id}/memberships/{institutionId}Scope: employees:write

Change a membership, or end it with {"status":"ended","endedOn":"…"}

Changes contractType, hourlyRate, startedOn, endedOn or status (active, ended); send at least one. There is no DELETE.

  • End a membership with {"status":"ended","endedOn":"2026-11-04"}. endedOn must not lie before startedOn, and ended without an end date (in the request or already stored) is 400.
  • Reactivate an ended membership with {"status":"active"}: without a new endedOn the old end date is cleared.
  • A membership that is still active keeps a planned end date it was given ({"endedOn": …} alone). Events after that date are refused; once it has passed, clearing it takes two writes: end the membership, then reactivate it.
  • A PATCH that changes nothing writes nothing and answers the employee as it is.
  • An employee's events are accepted only at an institution where they are a member, and not after the end date.
curl -s -X PATCH "$MASK/integration/employees/24/memberships/4" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"status":"ended","endedOn":"2026-11-04"}'

200 OK; the membership now reads:

{"institutionId": 4, "contractType": "part_time", "hourlyRate": 10.0, "startedOn": "2026-11-03", "endedOn": "2026-11-04",
 "status": "ended", "createdAt": 1793812878, "updatedAt": 1793812906}

Parameters

  • idpathintegerrequired
  • institutionIdpathintegerrequired
  • Idempotency-Keyheaderstring

    Optional. 1 to 128 visible ASCII characters. A retry with the same key and request within 24 h gets the first reply back (Idempotent-Replayed: true) instead of writing again

Request body

JSON, MembershipUpdateRequest. Fields not listed here are ignored.

  • contractTypestring

    at most 50 characters

  • endedOndate
  • hourlyRatenumber

    At least 0, at most two decimals

    at least 0

  • startedOndate
  • statusstring

    active or ended; ended needs an endedOn (in the request or already on the membership)

    one of active, ended

Responses

200OKEmployee
Fields of Employee
  • createdAtinteger

    Epoch seconds, UTC

  • emailstring
  • firstNamestring
  • idinteger
  • languagestring

    en, bs or tr

  • lastNamestring
  • membershipsarray of Membership

    Memberships in the key's institutions, active and ended

    Fields of Membership
    • contractTypestring
    • createdAtinteger

      Epoch seconds, UTC

    • endedOndate
    • hourlyRatenumber

      Two decimals; absent when not set

    • institutionIdinteger
    • startedOndate
    • statusstring

      active or ended

    • updatedAtinteger
  • personalIdstring
  • statusstring

    active, inactive or terminated

  • updatedAtinteger
Errors this route can answer
  • 400Bad Requestvalidation_failedidempotency_requiredevent_too_old
  • 401Unauthorizedinvalid_api_keykey_expiredkey_revoked
  • 403Forbiddenip_not_allowedscope_missinginstitution_not_allowednot_api_eventadmin_linked_employee
  • 404Not Foundnot_found
  • 409Conflictconflictidempotency_in_progress
  • 422Unprocessable Contentidempotency_mismatch
  • 429Too Many Requestsrate_limited
  • 500Internal Server Errorinternal_error
  • 503Service Unavailableunavailable

Each error has the same JSON body; see Errors for what every code means.