Workforce time & attendance — clock in/out, meal and rest breaks, schedules, timecards, and meal-break compliance exceptions. Resembles tools like ADP Time and Ceridian Dayforce.
The signature flow is meal-break compliance: `POST /meal-compliance/evaluate` finds employees who have been on the clock past their country's meal threshold, records a `MEAL_BREAK_DUE` exception, and fires the `time.meal_break.due` webhook. Acting on it — `POST /clock` with `action: "meal_start"` — records the punch and automatically resolves that exception.
**Where this sits next to Purple HRS.** Purple Time is the source of truth for hours ACTUALLY WORKED: it owns clock in/out punches, the timecards derived from them, schedules, and meal-break compliance. Purple HRS owns the payroll- and HR-facing views of time — its `/timesheets` are weekly hours summaries that feed payroll (no punches, no breaks), its `/time-off-requests` are absence requests, and it remains the system of record for PTO balances. Ask Purple Time "when did they clock in, and do they owe a break?"; ask Purple HRS "how many hours go to payroll this week, and what leave is booked?".
---
**Acting as a user (`X-PS-Impersonate-User`).** Every endpoint accepts an optional `X-PS-Impersonate-User` request header whose value is the email of the instance user to act as. The effective user resolves in this order: the header if present, else the instance's configured default MCP user, else none. User-scoped paths additionally accept the literal `me` in place of an email, and Graph-style apps also serve `/me/…` twins of their `/users/{email}/…` paths that resolve the same way. This header is honored by all operations but is intentionally omitted from each operation's parameter list so it is not surfaced as a per-tool argument by MCP clients; see the `ImpersonateUser` entry under `components.parameters`.
Workforce time & attendance — clock in/out, meal and rest breaks, schedules, timecards, and meal-break compliance exceptions. Resembles tools like ADP Time and Ceridian Dayforce.
The signature flow is meal-break compliance: `POST /meal-compliance/evaluate` finds employees who have been on the clock past their country's meal threshold, records a `MEAL_BREAK_DUE` exception, and fires the `time.meal_break.due` webhook. Acting on it — `POST /clock` with `action: "meal_start"` — records the punch and automatically resolves that exception.
**Where this sits next to Purple HRS.** Purple Time is the source of truth for hours ACTUALLY WORKED: it owns clock in/out punches, the timecards derived from them, schedules, and meal-break compliance. Purple HRS owns the payroll- and HR-facing views of time — its `/timesheets` are weekly hours summaries that feed payroll (no punches, no breaks), its `/time-off-requests` are absence requests, and it remains the system of record for PTO balances. Ask Purple Time "when did they clock in, and do they owe a break?"; ask Purple HRS "how many hours go to payroll this week, and what leave is booked?".
---
**Acting as a user (`X-PS-Impersonate-User`).** Every endpoint accepts an optional `X-PS-Impersonate-User` request header whose value is the email of the instance user to act as. The effective user resolves in this order: the header if present, else the instance's configured default MCP user, else none. User-scoped paths additionally accept the literal `me` in place of an email, and Graph-style apps also serve `/me/…` twins of their `/users/{email}/…` paths that resolve the same way. This header is honored by all operations but is intentionally omitted from each operation's parameter list so it is not surfaced as a per-tool argument by MCP clients; see the `ImpersonateUser` entry under `components.parameters`.
Operations
POST /clock
Record a punch (clock in/out, meal, break)
The timeclock action a chat plugin calls. `employee_email` defaults to the effective user and accepts the literal "me". Punches are validated against the day's existing punches — you cannot clock in twice, clock out mid-meal, or take a second meal break — and a rejected punch returns 422 explaining why. Recording `meal_start` also resolves any open MEAL_BREAK_DUE / MISSED_MEAL exception for that employee and day, which is how the meal-break notification loop closes. Updates the open timecard's totals and today's shift status.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Record a punch (clock in/out, meal, break)",
"description": "The timeclock action a chat plugin calls. `employee_email` defaults to the effective user and accepts the literal \"me\". Punches are validated against the day's existing punches — you cannot clock in twice, clock out mid-meal, or take a second meal break — and a rejected punch returns 422 explaining why. Recording `meal_start` also resolves any open MEAL_BREAK_DUE / MISSED_MEAL exception for that employee and day, which is how the meal-break notification loop closes. Updates the open timecard's totals and today's shift status.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": [
"clock_in",
"clock_out",
"meal_start",
"meal_end",
"break_start",
"break_end"
],
"description": "Which punch to record.",
"example": "meal_start"
},
"employee_email": {
"type": "string",
"description": "Employee to punch for. Defaults to the effective user; accepts \"me\".",
"example": "me"
},
"timestamp": {
"type": "string",
"format": "date-time",
"description": "When the punch happened. Defaults to now."
},
"source": {
"type": "string",
"enum": [
"chat",
"web",
"timeclock",
"api"
],
"example": "chat"
},
"notes": {
"type": "string",
"example": "Stepping out for lunch"
},
"shift_id": {
"type": "string",
"description": "Defaults to today's scheduled shift."
},
"employee_id": {
"type": "string",
"example": "EMP-0001"
}
},
"required": [
"action"
]
}
}
}
},
"responses": {
"201": {
"description": "Punch recorded",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PunchResult"
}
}
}
},
"400": {
"description": "Missing/invalid action, bad timestamp or source, or \"me\" could not be resolved"
},
"422": {
"description": "Punch is not legal given the day's existing punches (e.g. already clocked in, meal already taken, clock-out during a meal)"
}
}
}
GET /clock/status
Get an employee's live clock and meal-break status
Answers "am I clocked in?" and "do I owe a lunch break yet?" from the day's punches and the employee's country meal rule. Use `minutes_until_meal_required` (negative when overdue) and `hours_worked_today` to ground notification copy.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get an employee's live clock and meal-break status",
"description": "Answers \"am I clocked in?\" and \"do I owe a lunch break yet?\" from the day's punches and the employee's country meal rule. Use `minutes_until_meal_required` (negative when overdue) and `hours_worked_today` to ground notification copy.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "employee_email",
"schema": {
"type": "string"
},
"description": "Employee to report on. Defaults to the effective user; accepts \"me\".",
"example": "me"
},
{
"in": "query",
"name": "employee",
"schema": {
"type": "string"
},
"description": "Shorthand alias for employee_email."
},
{
"in": "query",
"name": "date",
"schema": {
"type": "string",
"format": "date"
},
"description": "Work date to evaluate (UTC). Defaults to today."
}
],
"responses": {
"200": {
"description": "Clock status",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClockStatus"
}
}
}
},
"400": {
"description": "\"me\" could not be resolved"
}
}
}
POST /meal-compliance/evaluate
Evaluate meal-break compliance and raise reminders
The meal-break detector. Scans every employee with punches on the given date; for each open session within `reminder_lead_minutes` of — or already past — the country's meal threshold, it upserts one open MEAL_BREAK_DUE exception for that employee/day and fires the `time.meal_break.due` webhook. Sessions open more than 16h are treated as a forgotten clock-out and flagged MISSED_PUNCH instead. Exceptions are idempotent (re-running never stacks duplicates) but the webhook fires on every call, so this endpoint doubles as an on-cue demo trigger. Call it with no body to evaluate the whole instance.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Evaluate meal-break compliance and raise reminders",
"description": "The meal-break detector. Scans every employee with punches on the given date; for each open session within `reminder_lead_minutes` of — or already past — the country's meal threshold, it upserts one open MEAL_BREAK_DUE exception for that employee/day and fires the `time.meal_break.due` webhook. Sessions open more than 16h are treated as a forgotten clock-out and flagged MISSED_PUNCH instead. Exceptions are idempotent (re-running never stacks duplicates) but the webhook fires on every call, so this endpoint doubles as an on-cue demo trigger. Call it with no body to evaluate the whole instance.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"employee_email": {
"type": "string",
"format": "email",
"description": "Evaluate only this employee. Omit to evaluate everyone."
},
"date": {
"type": "string",
"format": "date",
"description": "Work date to evaluate (UTC). Defaults to today."
}
}
}
}
}
},
"responses": {
"200": {
"description": "Evaluation summary",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MealComplianceResult"
}
}
}
}
}
}
GET /punches
List punches
Raw clock events — the punch-level record behind every hours figure in this app. One row per clock in/out, meal start/end or break start/end, stamped with the work date (`day`) the punch belongs to. Purple HRS has no equivalent: its timesheets are weekly summaries with no punch times.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List punches",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Punch"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
},
"description": "Raw clock events — the punch-level record behind every hours figure in this app. One row per clock in/out, meal start/end or break start/end, stamped with the work date (`day`) the punch belongs to. Purple HRS has no equivalent: its timesheets are weekly summaries with no punch times."
}
POST /punches
Create punch
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create punch",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Punch"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Punch"
}
}
}
}
}
}
GET /punches/{id}
Get punch
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get punch",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Punch"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /punches/{id}
Update punch
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update punch",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Punch"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Punch"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /punches/{id}
Delete punch
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete punch",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
GET /timecards
List timecards
One employee's timecard for a PAY PERIOD, with totals recomputed from their actual punches (regular / overtime / PTO hours) plus an exception count and approver. This is the authoritative record of hours worked. Do not confuse it with Purple HRS `/timesheets`, which is a WEEKLY hours summary for payroll with no punches behind it — use that one for payroll-facing weekly hours, and this one for what was actually worked.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List timecards",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Timecard"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
},
"description": "One employee's timecard for a PAY PERIOD, with totals recomputed from their actual punches (regular / overtime / PTO hours) plus an exception count and approver. This is the authoritative record of hours worked. Do not confuse it with Purple HRS `/timesheets`, which is a WEEKLY hours summary for payroll with no punches behind it — use that one for payroll-facing weekly hours, and this one for what was actually worked."
}
POST /timecards
Create timecard
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create timecard",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Timecard"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Timecard"
}
}
}
}
}
}
GET /timecards/{id}
Get timecard
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get timecard",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Timecard"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /timecards/{id}
Update timecard
Partial update. Status must be one of open, submitted, approved, signed_off; a signed-off timecard cannot be reopened. Prefer /timecards/{id}/submit and /approve for the state machine.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update timecard",
"description": "Partial update. Status must be one of open, submitted, approved, signed_off; a signed-off timecard cannot be reopened. Prefer /timecards/{id}/submit and /approve for the state machine.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Timecard"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Timecard"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /timecards/{id}
Delete timecard
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete timecard",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
POST /timecards/{id}/submit
Submit a timecard for approval
Employee action: transitions an open timecard to submitted and stamps submitted_by/submitted_at. Emits time.timecard.submitted.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Submit a timecard for approval",
"description": "Employee action: transitions an open timecard to submitted and stamps submitted_by/submitted_at. Emits time.timecard.submitted.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"submitted_by": {
"type": "string",
"format": "email",
"description": "Defaults to the timecard's employee_email."
}
}
}
}
}
},
"responses": {
"200": {
"description": "Timecard submitted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Timecard"
}
}
}
},
"404": {
"description": "Timecard not found"
},
"422": {
"description": "Timecard is not in 'open' status"
}
}
}
POST /timecards/{id}/approve
Approve a submitted timecard (manager sign-off)
Manager action: transitions a submitted timecard to approved and records approved_by/approver_email. Emits time.timecard.approved.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Approve a submitted timecard (manager sign-off)",
"description": "Manager action: transitions a submitted timecard to approved and records approved_by/approver_email. Emits time.timecard.approved.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"approved_by": {
"type": "string",
"format": "email",
"description": "Defaults to the effective user, then the timecard's approver_email."
},
"approver_notes": {
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Timecard approved",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Timecard"
}
}
}
},
"404": {
"description": "Timecard not found"
},
"422": {
"description": "Timecard is not in 'submitted' status"
}
}
}
GET /shifts
List shifts
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List shifts",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Shift"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
POST /shifts
Create shift
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create shift",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Shift"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Shift"
}
}
}
}
}
}
GET /shifts/{id}
Get shift
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get shift",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Shift"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /shifts/{id}
Update shift
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update shift",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Shift"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Shift"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /shifts/{id}
Delete shift
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete shift",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
GET /exceptions
List exceptions
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List exceptions",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/TimeException"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
POST /exceptions
Create exception
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create exception",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TimeException"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TimeException"
}
}
}
}
}
}
GET /exceptions/{id}
Get exception
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get exception",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TimeException"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /exceptions/{id}
Update exception
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update exception",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TimeException"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TimeException"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /exceptions/{id}
Delete exception
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete exception",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
POST /exceptions/{id}/resolve
Resolve a time exception
Marks the exception resolved and records resolved_by/resolved_at. Emits time.exception.resolved. Note that recording a meal_start punch resolves meal exceptions automatically — this endpoint is for the other types (missed punches, late-ins) or for manual closure.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Resolve a time exception",
"description": "Marks the exception resolved and records resolved_by/resolved_at. Emits time.exception.resolved. Note that recording a meal_start punch resolves meal exceptions automatically — this endpoint is for the other types (missed punches, late-ins) or for manual closure.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"resolved_by": {
"type": "string",
"format": "email",
"description": "Defaults to the effective user, then the exception's employee_email."
},
"resolution_note": {
"type": "string",
"example": "Employee confirmed a 35-minute lunch."
}
}
}
}
}
},
"responses": {
"200": {
"description": "Exception resolved",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TimeException"
}
}
}
},
"404": {
"description": "Exception not found"
},
"422": {
"description": "Exception is already resolved"
}
}
}
GET /meal-rules
List meal rules
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List meal rules",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/MealRule"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
GET /meal-rules/{id}
Get meal rule
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get meal rule",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MealRule"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /meal-rules/{id}
Update meal rule
Tune a country's meal policy (thresholds must be non-negative numbers). Lowering meal_required_after_hours is the quickest way to make the detector fire during a demo.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update meal rule",
"description": "Tune a country's meal policy (thresholds must be non-negative numbers). Lowering meal_required_after_hours is the quickest way to make the detector fire during a demo.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MealRule"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MealRule"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
GET /pay-periods
List pay periods
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List pay periods",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PayPeriod"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
GET /pay-periods/{id}
Get pay period
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get pay period",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PayPeriod"
}
}
}
},
"404": {
"description": "Not found"
}
}
}