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#
/integration/employeesScope: employees:readThe 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
institutionIdqueryintegerOne of the key's institutions; omitted = all of them
statusquerystringactive | inactive | terminated | all
default "all"
pagequeryinteger1-based
default 1
pageSizequeryinteger1-200
default 50
Responses
Fields of Employee page
countintegerlistarray of EmployeeFields of Employee
createdAtintegerEpoch seconds, UTC
emailstringfirstNamestringidintegerlanguagestringen, bs or tr
lastNamestringmembershipsarray of MembershipMemberships in the key's institutions, active and ended
Fields of Membership
contractTypestringcreatedAtintegerEpoch seconds, UTC
endedOndatehourlyRatenumberTwo decimals; absent when not set
institutionIdintegerstartedOndatestatusstringactive or ended
updatedAtinteger
personalIdstringstatusstringactive, inactive or terminated
updatedAtinteger
pageintegerpageSizeintegertotalPagesinteger
Errors this route can answer
- 400Bad Request
validation_failedidempotency_requiredevent_too_old - 401Unauthorized
invalid_api_keykey_expiredkey_revoked - 403Forbidden
ip_not_allowedscope_missinginstitution_not_allowednot_api_eventadmin_linked_employee - 429Too Many Requests
rate_limited - 500Internal Server Error
internal_error - 503Service Unavailable
unavailable
Each error has the same JSON body; see Errors for what every code means.
Get employee#
/integration/employees/{id}Scope: employees:readOne 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
Fields of Employee
createdAtintegerEpoch seconds, UTC
emailstringfirstNamestringidintegerlanguagestringen, bs or tr
lastNamestringmembershipsarray of MembershipMemberships in the key's institutions, active and ended
Fields of Membership
contractTypestringcreatedAtintegerEpoch seconds, UTC
endedOndatehourlyRatenumberTwo decimals; absent when not set
institutionIdintegerstartedOndatestatusstringactive or ended
updatedAtinteger
personalIdstringstatusstringactive, inactive or terminated
updatedAtinteger
Errors this route can answer
- 400Bad Request
validation_failedidempotency_requiredevent_too_old - 401Unauthorized
invalid_api_keykey_expiredkey_revoked - 403Forbidden
ip_not_allowedscope_missinginstitution_not_allowednot_api_eventadmin_linked_employee - 404Not Found
not_found - 429Too Many Requests
rate_limited - 500Internal Server Error
internal_error - 503Service Unavailable
unavailable
Each error has the same JSON body; see Errors for what every code means.
Create employee#
/integration/employeesScope: employees:writeCreate 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-KeyheaderstringOptional. 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.
emailstringat most 255 characters
firstNamestringrequiredat most 100 characters
languagestringen, bs or tr; en when omitted
one of en, bs, tr
lastNamestringrequiredat most 100 characters
membershipsarray of MembershipCreateRequestrequiredat least 1 item
Fields of MembershipCreateRequest
contractTypestringat most 50 characters
hourlyRatenumberAt least 0, at most two decimals
at least 0
institutionIdintegerrequiredstartedOndateISO date
personalIdstringat most 50 characters
Responses
Fields of Employee
createdAtintegerEpoch seconds, UTC
emailstringfirstNamestringidintegerlanguagestringen, bs or tr
lastNamestringmembershipsarray of MembershipMemberships in the key's institutions, active and ended
Fields of Membership
contractTypestringcreatedAtintegerEpoch seconds, UTC
endedOndatehourlyRatenumberTwo decimals; absent when not set
institutionIdintegerstartedOndatestatusstringactive or ended
updatedAtinteger
personalIdstringstatusstringactive, inactive or terminated
updatedAtinteger
Errors this route can answer
- 400Bad Request
validation_failedidempotency_requiredevent_too_old - 401Unauthorized
invalid_api_keykey_expiredkey_revoked - 403Forbidden
ip_not_allowedscope_missinginstitution_not_allowednot_api_eventadmin_linked_employee - 404Not Found
not_found - 409Conflict
conflictidempotency_in_progress - 422Unprocessable Content
idempotency_mismatch - 429Too Many Requests
rate_limited - 500Internal Server Error
internal_error - 503Service Unavailable
unavailable
Each error has the same JSON body; see Errors for what every code means.
Update employee#
/integration/employees/{id}Scope: employees:writeChange 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
conflictwhen 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
idpathintegerrequiredIdempotency-KeyheaderstringOptional. 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.
emailstringat most 255 characters
firstNamestringat most 100 characters
languagestringone of en, bs, tr
lastNamestringat most 100 characters
personalIdstringat most 50 characters
statusstringactive, 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
Fields of Employee
createdAtintegerEpoch seconds, UTC
emailstringfirstNamestringidintegerlanguagestringen, bs or tr
lastNamestringmembershipsarray of MembershipMemberships in the key's institutions, active and ended
Fields of Membership
contractTypestringcreatedAtintegerEpoch seconds, UTC
endedOndatehourlyRatenumberTwo decimals; absent when not set
institutionIdintegerstartedOndatestatusstringactive or ended
updatedAtinteger
personalIdstringstatusstringactive, inactive or terminated
updatedAtinteger
Errors this route can answer
- 400Bad Request
validation_failedidempotency_requiredevent_too_old - 401Unauthorized
invalid_api_keykey_expiredkey_revoked - 403Forbidden
ip_not_allowedscope_missinginstitution_not_allowednot_api_eventadmin_linked_employee - 404Not Found
not_found - 409Conflict
conflictidempotency_in_progress - 422Unprocessable Content
idempotency_mismatch - 429Too Many Requests
rate_limited - 500Internal Server Error
internal_error - 503Service Unavailable
unavailable
Each error has the same JSON body; see Errors for what every code means.
Add membership#
/integration/employees/{id}/membershipsScope: employees:writeAdd 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
idpathintegerrequiredIdempotency-KeyheaderstringOptional. 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.
contractTypestringat most 50 characters
hourlyRatenumberAt least 0, at most two decimals
at least 0
institutionIdintegerrequiredstartedOndateISO date
Responses
Fields of Employee
createdAtintegerEpoch seconds, UTC
emailstringfirstNamestringidintegerlanguagestringen, bs or tr
lastNamestringmembershipsarray of MembershipMemberships in the key's institutions, active and ended
Fields of Membership
contractTypestringcreatedAtintegerEpoch seconds, UTC
endedOndatehourlyRatenumberTwo decimals; absent when not set
institutionIdintegerstartedOndatestatusstringactive or ended
updatedAtinteger
personalIdstringstatusstringactive, inactive or terminated
updatedAtinteger
Errors this route can answer
- 400Bad Request
validation_failedidempotency_requiredevent_too_old - 401Unauthorized
invalid_api_keykey_expiredkey_revoked - 403Forbidden
ip_not_allowedscope_missinginstitution_not_allowednot_api_eventadmin_linked_employee - 404Not Found
not_found - 409Conflict
conflictidempotency_in_progress - 422Unprocessable Content
idempotency_mismatch - 429Too Many Requests
rate_limited - 500Internal Server Error
internal_error - 503Service Unavailable
unavailable
Each error has the same JSON body; see Errors for what every code means.
Update membership#
/integration/employees/{id}/memberships/{institutionId}Scope: employees:writeChange 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"}.endedOnmust not lie beforestartedOn, andendedwithout an end date (in the request or already stored) is 400. - Reactivate an ended membership with
{"status":"active"}: without a newendedOnthe old end date is cleared. - A membership that is still
activekeeps 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
idpathintegerrequiredinstitutionIdpathintegerrequiredIdempotency-KeyheaderstringOptional. 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.
contractTypestringat most 50 characters
endedOndatehourlyRatenumberAt least 0, at most two decimals
at least 0
startedOndatestatusstringactive or ended; ended needs an endedOn (in the request or already on the membership)
one of active, ended
Responses
Fields of Employee
createdAtintegerEpoch seconds, UTC
emailstringfirstNamestringidintegerlanguagestringen, bs or tr
lastNamestringmembershipsarray of MembershipMemberships in the key's institutions, active and ended
Fields of Membership
contractTypestringcreatedAtintegerEpoch seconds, UTC
endedOndatehourlyRatenumberTwo decimals; absent when not set
institutionIdintegerstartedOndatestatusstringactive or ended
updatedAtinteger
personalIdstringstatusstringactive, inactive or terminated
updatedAtinteger
Errors this route can answer
- 400Bad Request
validation_failedidempotency_requiredevent_too_old - 401Unauthorized
invalid_api_keykey_expiredkey_revoked - 403Forbidden
ip_not_allowedscope_missinginstitution_not_allowednot_api_eventadmin_linked_employee - 404Not Found
not_found - 409Conflict
conflictidempotency_in_progress - 422Unprocessable Content
idempotency_mismatch - 429Too Many Requests
rate_limited - 500Internal Server Error
internal_error - 503Service Unavailable
unavailable
Each error has the same JSON body; see Errors for what every code means.