Integration API API reference
Events
Clock events from the terminals, from administrators and from API keys, with their corrections and voids. Reads need events:read, writes events:write.
Clock events are the record MASK keeps: every tap on a terminal, every manual entry and every event a key writes. Events are never edited or deleted. A correction marks the original corrected and adds a new event whose correctsEventId names it; a void marks it voided.
An event has id, employeeId, institutionId, businessDate, eventCode, eventTime, source (manual from the web app, controller from a terminal, api from a key, portal, mobile), status (active, corrected, voided), correctsEventId, anomalies[] (stale_closure, duplicate, no_open_session, out_of_sequence; empty when clean), note, createdAt and updatedAt.
MASK recomputes the anomalies of a person's events after every write to their shifts, so read an event again for its current flags: a clock_in answered with stale_closure loses it once its clock_out arrives.
List events#
/integration/eventsScope: events:readClock events between two business dates (inclusive, at most 31 days; each institution's own timezone and day boundary), oldest first (eventTime, id). 403 institution_not_allowed for an institutionId outside the key.
status=all adds the corrected and voided rows, so a mirror can apply corrections (see Mirroring events).
curl -s "$MASK/integration/events?from=2026-11-03&to=2026-11-04&employeeId=24&status=all" -H "Authorization: Bearer $KEY"
{
"list": [
{"id": 7607, "employeeId": 24, "institutionId": 1, "businessDate": "2026-11-03", "eventCode": "clock_in",
"eventTime": 1793689200, "source": "api", "status": "active", "anomalies": [], "createdAt": 1793812944},
{"id": 7608, "employeeId": 24, "institutionId": 1, "businessDate": "2026-11-03", "eventCode": "clock_out",
"eventTime": 1793718900, "source": "api", "status": "corrected", "anomalies": [], "createdAt": 1793812944, "updatedAt": 1793812951},
{"id": 7609, "employeeId": 24, "institutionId": 1, "businessDate": "2026-11-03", "eventCode": "clock_out",
"eventTime": 1793719800, "source": "api", "status": "active", "correctsEventId": 7608, "anomalies": [],
"note": "Corrected by ERP: left at 16:30", "createdAt": 1793812951},
{"id": 7605, "employeeId": 24, "institutionId": 1, "businessDate": "2026-11-04", "eventCode": "clock_in",
"eventTime": 1793802122, "source": "api", "status": "voided", "anomalies": [], "note": "Gate 2 (ERP event 88123)",
"createdAt": 1793812922, "updatedAt": 1793812958},
{"id": 7606, "employeeId": 24, "institutionId": 1, "businessDate": "2026-11-04", "eventCode": "clock_out",
"eventTime": 1793812871, "source": "api", "status": "voided", "anomalies": [], "createdAt": 1793812931, "updatedAt": 1793812958}
],
"count": 5,
"page": 1,
"pageSize": 50,
"totalPages": 1
}
An institutionId outside a key that lists institutions:
{"status": "error", "code": "institution_not_allowed", "message": "Access denied to institution 4", "requestId": "84eb63ae-a444-47c0-a24c-ae400825993c"}
Parameters
fromquerydaterequiredFirst business date, ISO yyyy-MM-dd, from 2000-01-01 to 2037-12-31
toquerydaterequiredLast business date, ISO yyyy-MM-dd, from 2000-01-01 to 2037-12-31; from..to spans at most 31 days, both included
institutionIdqueryintegerOne of the key's institutions; omitted = all of them
employeeIdqueryintegerOnly this person's events; a person the key cannot see gives an empty list
statusquerystringactive | all (adds corrected and voided rows, so a mirror can apply corrections)
default "active"
pagequeryinteger1-based
default 1
pageSizequeryinteger1-200
default 50
Responses
Fields of Event page
countintegerlistarray of EventFields of Event
anomaliesarray of stringRollup flags, e.g. duplicate, no_open_session; empty when clean
businessDatedateThe business date at the institution (local time minus its day boundary), ISO date
correctsEventIdintegerFor a correction: the id of the event it replaces
createdAtintegeremployeeIdintegereventCodestringeventTimeintegerEpoch seconds, UTC
idintegerinstitutionIdintegernotestringsourcestringmanual, controller, api, portal or mobile
statusstringactive, corrected or voided
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 event#
/integration/events/{id}Scope: events:readOne clock event, whatever its status; 404 when unknown or outside the key's institutions.
Use it to read the event a correction's correctsEventId names: corrected and voided events stay readable by id.
Parameters
idpathintegerrequired
Responses
Fields of Event
anomaliesarray of stringRollup flags, e.g. duplicate, no_open_session; empty when clean
businessDatedateThe business date at the institution (local time minus its day boundary), ISO date
correctsEventIdintegerFor a correction: the id of the event it replaces
createdAtintegeremployeeIdintegereventCodestringeventTimeintegerEpoch seconds, UTC
idintegerinstitutionIdintegernotestringsourcestringmanual, controller, api, portal or mobile
statusstringactive, corrected or voided
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 event#
/integration/eventsScope: events:writeRecord a clock event (source api) for an employee at an institution of the key; the Idempotency-Key header is required.
The event is attributed to the key. MASK files it into the person's shift at once, by the same rules as a card tap: a clock_in opens a shift and the next clock_out closes it.
Refusals: an institution outside the key 403; a person the key cannot see 404; an archived institution 409; an unknown code, a code not enabled there, a person who is not a member there or whose membership ended before the event's business date, or an eventTime more than 300 s ahead: 400 validation_failed; a business date more than 62 days back: 400 event_too_old.
curl -s -X POST "$MASK/integration/events" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-H 'Idempotency-Key: erp-clock-88101' \
-d '{"employeeId":24,"institutionId":1,"eventCode":"clock_in","eventTime":1793689200}'
201 Created:
{
"id": 7607,
"employeeId": 24,
"institutionId": 1,
"businessDate": "2026-11-03",
"eventCode": "clock_in",
"eventTime": 1793689200,
"source": "api",
"status": "active",
"anomalies": ["stale_closure"],
"createdAt": 1793812944
}
stale_closure, because the shift opened at 08:00 the day before had no clock-out yet when this answer was written; it cleared when the clock-out arrived. Some refusals:
{"status": "error", "code": "validation_failed", "message": "Event code break_start is not enabled for institution 1", "requestId": "6ce3c4f0-947f-47de-bafb-1a6db37a6958"}
{"status": "error", "code": "validation_failed", "message": "Employee 24's membership at institution 4 ended on 2026-11-03", "requestId": "55a6b10a-51b6-4c5e-bf4e-d7ee94e4194e"}
{"status": "error", "code": "event_too_old", "message": "eventTime lies on business date 2026-08-20, more than 62 days back; an API key cannot write that far back", "requestId": "a0a1180c-ef95-45eb-89fd-977f6b745285"}
{"status": "error", "code": "idempotency_required", "message": "This endpoint requires an Idempotency-Key header", "requestId": "c86a6c29-1e6e-494a-956d-fa2ac98618ba"}
Parameters
Idempotency-KeyheaderstringrequiredRequired. 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, EventCreateRequest. Fields not listed here are ignored.
employeeIdintegerrequiredeventCodestringrequiredA code from GET /integration/event-codes that is enabled at the institution
at most 50 characters
eventTimeintegerrequiredEpoch seconds, UTC; at most 300 s in the future and at most 62 days back (business date)
institutionIdintegerrequirednotestringat most 500 characters
Responses
Fields of Event
anomaliesarray of stringRollup flags, e.g. duplicate, no_open_session; empty when clean
businessDatedateThe business date at the institution (local time minus its day boundary), ISO date
correctsEventIdintegerFor a correction: the id of the event it replaces
createdAtintegeremployeeIdintegereventCodestringeventTimeintegerEpoch seconds, UTC
idintegerinstitutionIdintegernotestringsourcestringmanual, controller, api, portal or mobile
statusstringactive, corrected or voided
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.
Correct event#
/integration/events/{id}Scope: events:writeCorrect an event the integration API created: a new api event replaces it (correctsEventId = id), the original becomes corrected.
Any key of the same MASK system may correct an event with source: "api". Send eventCode, eventTime or note, at least one; omitted ones keep the original's value. The answer is the new event.
- Correct the newest event of a chain: a
correctedorvoidedevent is 409conflict. - Any other source is 403
not_api_event. Once an administrator has corrected an API event in the web app, the replacement ismanualand out of the key's reach. - The original's business date and the new
eventTimemust both lie within 62 days (400event_too_old).
curl -s -X PATCH "$MASK/integration/events/7608" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-H 'Idempotency-Key: erp-fix-88102-1' \
-d '{"eventTime":1793719800,"note":"Corrected by ERP: left at 16:30"}'
200 OK:
{
"id": 7609,
"employeeId": 24,
"institutionId": 1,
"businessDate": "2026-11-03",
"eventCode": "clock_out",
"eventTime": 1793719800,
"source": "api",
"status": "active",
"correctsEventId": 7608,
"anomalies": [],
"note": "Corrected by ERP: left at 16:30",
"createdAt": 1793812951
}
{"status": "error", "code": "not_api_event", "message": "Only events created through the integration API can be corrected; this one has source controller", "requestId": "7134a7cb-42fb-42ae-9486-f53c01358eba"}
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, EventCorrectRequest. Fields not listed here are ignored.
eventCodestringat most 50 characters
eventTimeintegerEpoch seconds, UTC; at most 300 s in the future and at most 62 days back (business date)
notestringat most 500 characters
Responses
Fields of Event
anomaliesarray of stringRollup flags, e.g. duplicate, no_open_session; empty when clean
businessDatedateThe business date at the institution (local time minus its day boundary), ISO date
correctsEventIdintegerFor a correction: the id of the event it replaces
createdAtintegeremployeeIdintegereventCodestringeventTimeintegerEpoch seconds, UTC
idintegerinstitutionIdintegernotestringsourcestringmanual, controller, api, portal or mobile
statusstringactive, corrected or voided
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.
Void event#
/integration/events/{id}Scope: events:writeVoid an event the integration API created.
The rules of a correction apply: source: "api", active, business date within 62 days. The answer is 204 No Content, and the shift is recomputed without the event.
curl -s -o /dev/null -w '%{http_code}\n' -X DELETE "$MASK/integration/events/7606" -H "Authorization: Bearer $KEY" \
-H 'Idempotency-Key: erp-void-88124'
204
Voiding it again, without the same Idempotency-Key:
{"status": "error", "code": "conflict", "message": "Only an active event can be voided; this one is voided", "requestId": "1cd6844c-3437-4fc4-8ab6-f109c66ee881"}
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
Responses
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.