{ "openapi": "3.1.0", "info": { "title": "Clockster Company API", "version": "3.0.0", "description": "A server-to-server API for reading and writing your company's employees, structure,\nschedules, attendance, tasks and documents. It is what an HR system, a payroll system or a\nturnstile of your own talks to.\n\nA key authenticates a company. There is no per-user identity here, so everything a call can\nsee and change belongs to the company holding the key, and another company's id answers\n`404` rather than `403` — you cannot learn that it exists.\n\n## Quickstart\n\nA first integration usually goes in this order, because each step needs the one before it.\n\n**1. Confirm the key.** `GET /me` answers the company it belongs to and changes nothing.\n\n**2. Write the structure.** `POST /locations/upsert`, then `/departments/upsert` and\n`/positions/upsert` if you use them. Send each row with the `external_id` your own system\nknows it by. Read the ids back out of the answer — an employee is filed against a location\nby id.\n\n**3. Write the roster.** `POST /users/upsert`, up to 100 people a call. `first_name`, `role`\nand `location_id` are the minimum; send `external_id` on every person so the next sync\nupdates rather than duplicates.\n\n**4. Say who works when.** `POST /schedules` takes a list, each item a kind of day, the\ndates it falls on and the people it is for.\n\n**5. Read what happened.** `GET /timesheets?date_from=…&date_to=…` answers one row per\nperson per day: what was planned, and with `include=actual,variance` what was recorded and\nhow the two differ. `GET /attendance` answers the raw clock-ins instead.\n\n**6. Stop polling.** Create a webhook endpoint and subscribe to the events you care about;\nre-read on the event rather than on a timer.\n\nFrom then on a sync is `updated_since` plus the cursor: ask for what changed, page through\nit, remember the instant you asked at.\n\n## SDK\n\n| Language | Package | Install |\n| --- | --- | --- |\n| TypeScript, Node | [`@clockster/sdk`](https://www.npmjs.com/package/@clockster/sdk) | `npm install @clockster/sdk` |\n| Python | [`clockster`](https://pypi.org/project/clockster/) | `pip install clockster` |\n| Go | [`github.com/clockster/sdk-go`](https://pkg.go.dev/github.com/clockster/sdk-go) | `go get github.com/clockster/sdk-go` |\n| PHP | [`clockster/sdk`](https://packagist.org/packages/clockster/sdk) | `composer require clockster/sdk` |\n\nAll four are typed from this document and generated from it, so an operation is called the\nsame thing in each. Sources and issues:\n[sdk-typescript](https://github.com/clockster/sdk-typescript),\n[sdk-python](https://github.com/clockster/sdk-python),\n[sdk-go](https://github.com/clockster/sdk-go),\n[sdk-php](https://github.com/clockster/sdk-php).\n\nNone of them carries a dependency worth the name: TypeScript and Go have none at all,\nPython has `httpx`, PHP has curl. Each answers what its own language reads most plainly —\nobjects, dictionaries described by `TypedDict`s, structs, arrays described by array shapes\n— and each walks a cursor-paged listing the way that language walks anything. The Python\nclient mirrors every operation both blocking and awaited.\n\n**Your language is not on the list.** This document is public and machine-readable, so a\nclient for any language is one command away, and it will be as complete as the list above —\nthe published package is generated from this same file.\n\n```\nnpx @hey-api/openapi-ts -i https://api.clockster.com/openapi/v3.json -o ./clockster\n```\n\nOther generators read it as well; `openapi-generator` is the usual one outside JavaScript.\n\n**Worked examples** live beside each package, in its `examples` directory: a roster sync\nin, a timesheet export out. Google Sheets is there too, beside the\n[TypeScript one](https://github.com/clockster/sdk-typescript/tree/main/examples) — a\nspreadsheet that fills itself from your roster and timesheets, and does not use the package\nat all, since Apps Script has no npm, so it is this API over `UrlFetchApp`.\n\n**Versions.** Each SDK follows its own semver, unrelated to the version of this API and to\nthe others'. A new API version is a major release of a package rather than a second package.\n\n## AI agents (MCP)\n\nThis API is also served to AI agents as an MCP server, at\n`https://api.clockster.com/company/mcp` over Streamable HTTP. A client that can send a bearer\ntoken connects with a company API key — Claude Code, Cursor, or an agent of your own built on\nan MCP SDK. A web chat client such as claude.ai or ChatGPT signs in over OAuth instead: it\nsends an administrator to a consent screen in the web application, and approving issues it an\nAPI key with the scopes chosen there, listed and revocable beside your other keys. That key\nworks only through the MCP server: called on these paths directly, it answers `403`.\n\n```\nclaude mcp add --transport http clockster https://api.clockster.com/company/mcp \\\n --header \"Authorization: Bearer \"\n```\n\nEach operation of this document is one tool, named after it: `users_index`,\n`timesheets_index`, `users_upsert`. Its arguments are the operation's parameters, plus\n`body` carrying the request body of a write, and its result is the operation's JSON body, so\neverything this document says about cursors, filters and errors holds for the agent too.\n\n**The key's scopes decide what the agent sees.** A key holding only `:read` scopes gets no\ntool that writes. Give the agent a key of its own with the sections it needs: its calls are\nthen attributed to it in the request log, and revoking the key disconnects it alone.\n\n**Writes are real, and marked.** Upserts, dismissals and deletes are marked destructive, so\nthe client asks the person before running one. Uploading a file and changing webhooks are\nnot available to agents. A create that takes an `Idempotency-Key` gets one derived from the\ncall, so a client retrying after a timeout does not write twice. An identical call within 24\nhours is answered with the first result and says so; its `idempotency_key` argument sets a\ndeliberate repeat apart.\n\n**Differences.** A list answers 20 rows unless the agent asks for more, and the agent's calls\nhave a rate limit of their own, described under Rate limits.\n\n## Making a request\n\nEvery path below is appended to:\n\n```\nhttps://api.clockster.com/company/v3/\n```\n\nThe key is issued in the web application, under the API section, and goes on every request\nas a bearer credential:\n\n```\nAuthorization: Bearer {ACCESS-TOKEN}\n```\n\nA missing, revoked or expired key answers `401` from that request onward. Issue a new one\nin the same place; there is no call here that mints one.\n\nA key has full access, or read or write access per section, and each operation below names\nthe scope it needs; `write` includes `read`. A call outside the key's scopes answers `403`\nwith `insufficient_scope`.\n\nBodies are JSON, and `Content-Type: application/json` is required on any request that\ncarries one. The single exception is `POST /files`, which takes `multipart/form-data`\nbecause it carries bytes. Responses are JSON throughout.\n\n## Pagination\n\nListings page on a cursor rather than a page number. Read `meta.next_cursor` and send it\nback as `cursor`; a null one means you have reached the end. `per_page` sets the size, up\nto 100.\n\nA cursor encodes a position in a fixed ordering, so nothing is repeated or skipped while\nthe data is being written to underneath you — which a page number cannot promise. One\nissued under a different ordering is refused rather than silently restarting the list, and\nthe refusal names the ordering that was expected.\n\nChanging the filters mid-walk is not refused and not meaningful: start again from the\nfirst page when you change them.\n\nThere is no total count. Ask for the next page until there is no cursor.\n\n## Filtering\n\nFilters that take a list match any of the values in it, and separate filters all have to\nhold: `?locations=1,2&positions=7` means \"at location 1 or 2, and holding position 7\".\nRepeating the parameter (`locations[]=1&locations[]=2`) means the same thing.\n\nEvery list has a cap, stated on the parameter — 100 ids on most of them. A filter only\never narrows within your own company, so it cannot be used to find out what another one\nholds.\n\n**`updated_since` is how a sync stays cheap.** It takes an instant and answers only rows\nchanged at or after it. Keep the time you asked at, not the time you finished, and overlap\nby a minute on the next call.\n\nThere is no sort parameter: each listing has one fixed ordering, oldest first — by `id`,\nor by the moment of the mark on `GET /attendance`.\n\nOldest first is what lets one walk finish the job: rows written while you are paging are\nappended ahead of the cursor and come back in the same pass. It also means a first call\nlands on the oldest row there is, which on a long-lived company is years back. When you\nwant recent activity rather than all of it, that is what `updated_since` is for.\n\n## Choosing what comes back\n\nA response carries its own fields always. Related objects are loaded only when `include`\nnames them, comma-separated: `?include=location,department`. What each endpoint accepts is\nlisted on that endpoint, and a name it does not accept is refused rather than ignored.\n\n**A key that was not asked for is absent, never null.** `null` means the value is known to\nbe empty; an absent key means you did not ask. Code that treats the two the same will\neventually record somebody's department as having been cleared.\n\n## Writing\n\nEvery write takes a list, and most are keyed on ids you own: send items carrying an\n`external_id` — a key from your own system that we never generate — and each one creates or\nupdates depending on whether that key is already known here. The answer says which happened,\nper item, in the order sent. Up to 100 items a call.\n\n**A batch is all or nothing.** One unacceptable item refuses the whole request before\nanything is written, so a `422` means none of it landed and you are never left guessing\nwhich half applied.\n\n**A resend of a keyed write is safe** and is the intended way to recover from a timeout or\na failure of any kind: whatever landed the first time matches its key and is updated rather\nthan duplicated.\n\n**An omitted field keeps its stored value.** Send a field as `null` to clear it.\n\nThree writes carry no key of your own. `POST /attendance` takes up to 100 marks and drops one\nit already holds, so a resend cannot double somebody's day. `POST /schedules` takes 25 and\n`POST /payroll/single-adjustments` 100, and neither has anything to match on — read the\nanswer rather than sending it again blind.\n\n## Rate limits\n\n100 requests a minute, counted against the company and shared by all of its keys. Each\nversion of this API has a budget of its own, so reading one does not spend the other's.\nOver the limit answers `429` until the window rolls over.\n\nCalls an AI agent makes through the MCP server have a further 100 a minute of their own, so\nan agent cannot spend your integrations' budget. A tool result carries no headers: over\nthat limit the error body holds `error.retry_after`, the seconds to wait.\n\n| Header | Sent on | Meaning |\n| --- | --- | --- |\n| `X-RateLimit-Limit` | every response | Requests allowed in the window. |\n| `X-RateLimit-Remaining` | every response | Requests left. Slow down as it nears zero rather than waiting for the refusal. |\n| `Retry-After` | `429` | Seconds to wait before trying again. |\n| `X-RateLimit-Reset` | `429` | Unix timestamp at which the window rolls over. |\n\n## Retries and failures\n\n**Retry a `429` and a `5xx`; do not retry a `4xx`.** A refusal we cannot fix by being asked\nagain is a refusal — retrying it burns your rate limit and changes nothing. On a `429`,\n`Retry-After` says how long to wait.\n\nBack off between attempts rather than retrying immediately, and give up after a handful.\n\n**A request that times out with no answer is unknown, not failed** — it may have been\napplied. Send it again: a keyed batch converges on what you meant rather than doubling\nanything, and `POST /attendance` drops a mark it already holds for the same person, moment\nand direction.\n\n**Where there is no such key, send an `Idempotency-Key`.** Five writes create something with\nnothing to match a second attempt against — a rota, a one-off payroll adjustment, a webhook\nendpoint, a rotated secret, a delivery sent again. Choose a value per attempt, send it in\nthat header, and a retry carrying the same one is answered with the first result rather than\nperformed again. The answer to a replay says so with `Idempotency-Replayed: true`.\n\nA key is remembered for 24 hours and belongs to the request it was first used for: sending it\nwith a different body answers `409` and `idempotency_key_reused` rather than handing you\nsomebody else's result. While the first request is still running, a second carrying the same\nkey answers `409` and `idempotency_key_in_flight` — wait rather than retry. A refusal is not\nremembered, so a key may be reused once its body is fixed.\n\nThe header is optional, and unnecessary on everything keyed on an `external_id`.\n\n## Dates and times\n\nInstants are ISO 8601 with an offset, and that is how they are answered:\n`2026-03-02T09:00:00+05:00`. Where you write one — the `datetime` of a clock-in — the\noffset is required, because the mark's own zone is read off it.\n\nPlain dates are `YYYY-MM-DD` and carry no time or zone: a date of birth, a contract's\nexpiry, the day a shift falls on. The day somebody worked means the same thing to them\nwherever the reader is.\n\nClock times are `HH:MM:SS`, read in the timezone stated beside them. A shift that starts at\n09:00 starts at 09:00 locally, whatever the offset is that week.\n\nDurations are seconds.\n\n## Numbers\n\nMoney and other decimal amounts are answered as JSON numbers rounded to two places. JSON\nnumbers carry no scale, so a whole amount arrives as `1500` rather than `1500.00` — it is\nthe same number. Do not accumulate them in binary floating point.\n\nIds are integers. Anything that looks like a number but is a label — an employee code, a\nnational id — is a string, and a leading zero in it is part of the value.\n\nField names are `snake_case` throughout, in both directions.\n\n## Keeping the key safe\n\nA key reaches the whole company within its scopes: the roster, attendance, payslips,\ndocuments. Treat it as you would a password.\n\n**It belongs on a server you control.** Do not ship it in a mobile app, a browser page or\nanything else a user can read, and do not commit it.\n\n**Give each system a key of its own, with only the scopes it needs.** A company may hold up\nto ten. Revoking one cuts off that system alone, and the request log in the web application\nsays which key made each call.\n\n**The plaintext is shown once, when the key is issued.** To replace a key without a cutover,\nissue the new one, move the system over, then revoke the old one; a revoked key answers\n`401` from its next request.\n\n## Compatibility\n\nThe document at `https://api.clockster.com/openapi/v3.json` is this API's contract, built\nfrom the deployment that answers it. Point a client generator at it rather than writing one\nby hand.\n\n**New fields may be added to any response at any time, and are not a breaking change.**\nIgnore keys you do not recognise; do not fail on them. New optional parameters and new\nendpoints arrive the same way.\n\nAn `operationId` never changes once published, and neither does the meaning of an existing\nfield. A change that would break a caller gets a new version of the whole surface, side by\nside with this one, rather than being applied underneath you. V2 remains available and is\ndocumented beside this.\n\n## Getting help\n\nWrite to `info@clockster.com` with what you called, what you expected and what came back.\nInclude the `error.request_id` from the response — every refusal carries one, and it is how\nwe find the request in our logs.\n\nRequests made with your key are also readable in the web application, under the API\nsection: the path, the status and the body that was sent." }, "servers": [ { "url": "https://api.clockster.com", "description": "Production" } ], "security": [ { "companyToken": [] } ], "tags": [ { "name": "Token", "description": "One call that says which company a key belongs to. Worth the first request of any integration." }, { "name": "Employees", "description": "The roster, and the thing every other section refers to. Write it with the ids your own\nsystem already uses (`external_id`) and you never have to keep a map of ours.\n\nPeople who leave stay readable — ask with `status=dismissed` — so a sync can learn that\nsomeone was let go rather than watching them silently vanish from the list." }, { "name": "Locations", "description": "Where work happens. A location carries coordinates and a radius, which is what a mobile clock-in is checked against." }, { "name": "Departments", "description": "The org tree. A department may hold managers, and `include=managers` is how you read them." }, { "name": "Positions", "description": "Job titles. Flat, and the one structure that carries no managers." }, { "name": "User filters", "description": "A grouping of your own — a shift, a crew, a cost centre — orthogonal to location, department and position." }, { "name": "Schedules", "description": "Who is expected when. A roster is written as a whole and read back per person; the three kinds — work, free and leave — are separate shapes rather than one shape with unused keys." }, { "name": "Attendance", "description": "The clock-ins themselves, read or recorded. What a turnstile, a payroll run and a report all disagree about until they read the same source." }, { "name": "Timesheets", "description": "Schedule crossed with attendance, per person per day: expected, actual, and what the difference is called." }, { "name": "Requests", "description": "What people ask for — leave, a corrected clock-in, an advance — and what was decided. Read-only here: a decision belongs to whoever holds the responsibility for it." }, { "name": "Tasks", "description": "Work issued from your system and read back with its state. Written with your own key, so re-sending the same task updates it instead of duplicating it." }, { "name": "Documents", "description": "Contracts and the rest of the personnel file, with their signing state. Filed against a person, and pointed at a file uploaded first." }, { "name": "Files", "description": "One upload, answering with an id to attach. Everything that takes a file takes that id, never bytes inline." }, { "name": "Payroll", "description": "Payslips as calculated, for the months they were calculated for, and the one-off additions and deductions that go into the next calculation." }, { "name": "Webhooks", "description": "What we send you when something changes, so a sync does not have to poll for it. An\nendpoint subscribes to the events it wants, and every delivery is readable here\nafterwards — including one you ask us to send again.\n\n**What arrives** is a POST of\n`{\"id\": 123, \"event\": \"user.updated\", \"occurred_at\": \"2026-03-02T09:00:00+05:00\", \"data\": {}}`,\nwith four headers: `X-Clockster-Event`, `X-Clockster-Timestamp`, `X-Clockster-Delivery`\nand `X-Clockster-Signature`.\n\n**Check the signature.** It is `sha256=` followed by the HMAC-SHA256 of\n`.` keyed with the endpoint's secret — compare it against the raw\nbytes you received, before parsing them, with a constant-time comparison. The secret is\nshown once when the endpoint is created and once more when it is replaced.\n\n**Answer 2xx quickly and do the work afterwards.** A 5xx or a timeout is retried with a\nwidening delay; a 4xx is not, because a refusal we cannot fix by trying again is a\nrefusal. An endpoint that keeps failing is switched off and its contact told why.\n\n**A key reaches the endpoints and deliveries whose events it can read**: `user.*` needs\n`users:read`; `location.*`, `department.*` and `position.*` need `structure:read`;\n`task.*` needs `tasks:read`. Subscribing to an event beyond them, or reading or changing an\nendpoint or delivery that carries one, answers `403` with `insufficient_scope`, and the\nlistings leave those out. A `full` key reaches all of them." } ], "components": { "securitySchemes": { "companyToken": { "type": "http", "scheme": "bearer", "description": "A company API key, issued in the API section of the web application. An operation names the scope it needs; `write` includes `read`, and `full` includes every scope." } }, "schemas": { "WorkSchedule": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "work", "free", "leave" ], "x-clockster-set": "SchedulesType", "description": "Which kind of day this is, and with it what else the item requires." }, "dates": { "type": "array", "minItems": 1, "maxItems": 366, "items": { "type": "string", "format": "date" }, "description": "The days this applies to, each `YYYY-MM-DD`. No repeats." }, "users": { "type": "array", "minItems": 1, "maxItems": 200, "items": { "type": "integer" }, "description": "Who the day is for, by id. At least one, and no repeats." }, "location_id": { "type": [ "integer", "null" ], "description": "The location this is filed against, by id. Null clears it." }, "department_id": { "type": [ "integer", "null" ], "description": "The department this is filed against, by id. Null clears it." }, "position_id": { "type": [ "integer", "null" ], "description": "The position this is filed against, by id. Null clears it." }, "timezone": { "type": "string", "pattern": "^(?:Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])$", "description": "The UTC offset the clock times beside it are read in — `Z`, or `+05:00`. An offset rather than a zone name, so the day is fixed to a moment rather than to a rule that may be changed later." }, "start": { "type": [ "string", "null" ], "format": "date-time", "description": "When it starts, as a clock time `HH:MM:SS`, read in the offset beside it." }, "end": { "type": [ "string", "null" ], "format": "date-time", "description": "When it ends, as a clock time `HH:MM:SS`, read in the offset beside it." }, "break_time": { "type": [ "integer", "null" ], "minimum": 0, "maximum": 43200, "description": "Unpaid break within the day, in seconds." }, "grace_start": { "type": [ "integer", "null" ], "minimum": 0, "maximum": 3600, "description": "How late an arrival still counts as on time, in seconds." }, "grace_end": { "type": [ "integer", "null" ], "minimum": 0, "maximum": 3600, "description": "How early a departure still counts as a full day, in seconds." }, "shifts": { "type": [ "array", "null" ], "minItems": 1, "maxItems": 8, "items": { "type": "object", "properties": { "start": { "type": "string", "format": "date-time", "description": "When it starts, as a clock time `HH:MM:SS`, read in the offset beside it." }, "end": { "type": "string", "format": "date-time", "description": "When it ends, as a clock time `HH:MM:SS`, read in the offset beside it." }, "location_id": { "type": [ "integer", "null" ], "description": "The location this is filed against, by id. Null clears it." }, "department_id": { "type": [ "integer", "null" ], "description": "The department this is filed against, by id. Null clears it." }, "position_id": { "type": [ "integer", "null" ], "description": "The position this is filed against, by id. Null clears it." } }, "required": [ "start", "end" ] }, "description": "Split the day into shifts instead of one span. Each carries its own clock times and may sit somewhere other than the day does." } }, "required": [ "type", "dates", "users", "timezone" ] }, "FreeSchedule": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "work", "free", "leave" ], "x-clockster-set": "SchedulesType", "description": "Which kind of day this is, and with it what else the item requires." }, "dates": { "type": "array", "minItems": 1, "maxItems": 366, "items": { "type": "string", "format": "date" }, "description": "The days this applies to, each `YYYY-MM-DD`. No repeats." }, "users": { "type": "array", "minItems": 1, "maxItems": 200, "items": { "type": "integer" }, "description": "Who the day is for, by id. At least one, and no repeats." }, "location_id": { "type": [ "integer", "null" ], "description": "The location this is filed against, by id. Null clears it." }, "department_id": { "type": [ "integer", "null" ], "description": "The department this is filed against, by id. Null clears it." }, "position_id": { "type": [ "integer", "null" ], "description": "The position this is filed against, by id. Null clears it." }, "timezone": { "type": "string", "pattern": "^(?:Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])$", "description": "The UTC offset the clock times beside it are read in — `Z`, or `+05:00`. An offset rather than a zone name, so the day is fixed to a moment rather than to a rule that may be changed later." }, "start": { "type": "string", "format": "date-time", "description": "When it starts, as a clock time `HH:MM:SS`, read in the offset beside it." }, "end": { "type": "string", "format": "date-time", "description": "When it ends, as a clock time `HH:MM:SS`, read in the offset beside it." }, "time_planned": { "type": [ "integer", "null" ], "minimum": 0, "maximum": 86400, "description": "How long the person is expected to work that day, in seconds." } }, "required": [ "type", "dates", "users", "timezone", "start", "end" ] }, "LeaveSchedule": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "work", "free", "leave" ], "x-clockster-set": "SchedulesType", "description": "Which kind of day this is, and with it what else the item requires." }, "dates": { "type": "array", "minItems": 1, "maxItems": 366, "items": { "type": "string", "format": "date" }, "description": "The days this applies to, each `YYYY-MM-DD`. No repeats." }, "users": { "type": "array", "minItems": 1, "maxItems": 200, "items": { "type": "integer" }, "description": "Who the day is for, by id. At least one, and no repeats." }, "location_id": { "type": [ "integer", "null" ], "description": "The location this is filed against, by id. Null clears it." }, "department_id": { "type": [ "integer", "null" ], "description": "The department this is filed against, by id. Null clears it." }, "position_id": { "type": [ "integer", "null" ], "description": "The position this is filed against, by id. Null clears it." }, "leave_type": { "type": "string", "enum": [ "annual", "unpaid", "sick", "unpaid_sick", "maternity", "paternity", "special", "day_off", "compensatory", "personal", "emergency", "unexcused_absence" ], "x-clockster-set": "SchedulesLeaveType", "description": "What kind of leave the day is." } }, "required": [ "type", "dates", "users", "leave_type" ] }, "Refusal": { "type": "object", "properties": { "error": { "type": "object", "properties": { "code": { "type": "string" }, "message": { "type": "string" }, "request_id": { "type": "string" } }, "required": [ "code", "message", "request_id" ] } }, "required": [ "error" ] }, "ValidationRefusal": { "type": "object", "properties": { "error": { "type": "object", "properties": { "code": { "type": "string" }, "message": { "type": "string" }, "request_id": { "type": "string" }, "errors": { "type": "object", "additionalProperties": { "type": "array", "items": { "type": "string" } } } }, "required": [ "code", "message", "request_id", "errors" ] } }, "required": [ "error" ] }, "PageLinks": { "type": "object", "properties": { "first": { "type": "null" }, "last": { "type": "null" }, "prev": { "type": [ "null", "string" ] }, "next": { "type": [ "null", "string" ] } }, "required": [ "first", "last", "prev", "next" ] }, "PageMeta": { "type": "object", "properties": { "path": { "type": "string" }, "per_page": { "type": "integer" }, "next_cursor": { "type": [ "null", "string" ] }, "prev_cursor": { "type": [ "null", "string" ] } }, "required": [ "path", "per_page", "next_cursor", "prev_cursor" ] }, "EmployeeShort": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "code": { "type": [ "null", "string" ] }, "first_name": { "type": "string" }, "middle_name": { "type": [ "null", "string" ] }, "last_name": { "type": "string" } }, "required": [ "id", "external_id", "code", "first_name", "middle_name", "last_name" ] }, "UpsertOutcome": { "type": "object", "properties": { "external_id": { "type": [ "null", "string" ] }, "id": { "type": "integer" }, "result": { "type": "string" } }, "required": [ "external_id", "id", "result" ] }, "DeleteOutcome": { "type": "object", "properties": { "id": { "type": "integer" }, "result": { "type": "string" } }, "required": [ "id", "result" ] } } }, "paths": { "/company/v3/attendance": { "get": { "operationId": "company.v3.attendance.index", "tags": [ "Attendance" ], "summary": "List clock-ins", "parameters": [ { "name": "date_from", "in": "query", "required": true, "description": "Start of the window, inclusive (YYYY-MM-DD).", "schema": { "type": "string", "format": "date" } }, { "name": "date_to", "in": "query", "required": true, "description": "End of the window, inclusive (YYYY-MM-DD).", "schema": { "type": "string", "format": "date" } }, { "name": "per_page", "in": "query", "required": false, "description": "How many rows one page holds. Defaults to 50.", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } }, { "name": "cursor", "in": "query", "required": false, "description": "The `meta.next_cursor` of the previous page. Omit it for the first. A cursor is bound to the filters it was issued under — change them and start again.", "schema": { "type": [ "string", "null" ], "maxLength": 512 } }, { "name": "users", "in": "query", "required": false, "description": "Only rows belonging to these people, by id.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "locations", "in": "query", "required": false, "description": "Only these locations, by id.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "statuses", "in": "query", "required": false, "description": "Only rows in these states.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 3, "items": { "type": "string", "enum": [ "out", "in", "break" ], "x-clockster-set": "AttendanceStatus" } } }, { "name": "sources", "in": "query", "required": false, "description": "Only marks recorded this way — a device, the mobile app, or a person entering them by hand.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 5, "items": { "type": "string", "enum": [ "device", "mobile", "frontend", "api", "system" ], "x-clockster-set": "AttendanceSource" } } }, { "name": "include", "in": "query", "required": false, "description": "Relations to load, comma-separated. Anything not named is absent from the answer rather than null.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "enum": [ "user", "location", "attachments" ], "x-clockster-set": "AttendanceInclude" } } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "user_id": { "type": "integer" }, "location_id": { "type": [ "integer", "null" ] }, "datetime": { "type": "string" }, "status": { "type": "string" }, "source": { "type": "string" }, "latitude": { "type": [ "null", "number" ] }, "longitude": { "type": [ "null", "number" ] }, "address": { "type": [ "null", "string" ] }, "comment": { "type": [ "null", "string" ] }, "user": { "$ref": "#/components/schemas/EmployeeShort" }, "location": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "code": { "type": [ "null", "string" ] }, "title": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "latitude": { "type": [ "null", "number" ] }, "longitude": { "type": [ "null", "number" ] }, "radius": { "type": "integer" }, "created_at": { "type": "string" }, "updated_at": { "type": "string" } }, "required": [ "id", "external_id", "code", "title", "description", "latitude", "longitude", "radius", "created_at", "updated_at" ] }, "attachments": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "format": { "type": "string" }, "url": { "type": "string" }, "created_at": { "type": "string" } }, "required": [ "id", "name", "description", "format", "url", "created_at" ] } } }, "required": [ "id", "user_id", "location_id", "datetime", "status", "source", "latitude", "longitude", "address", "comment" ] } }, "links": { "$ref": "#/components/schemas/PageLinks" }, "meta": { "$ref": "#/components/schemas/PageMeta" } }, "required": [ "data", "links", "meta" ] }, "example": { "data": [ { "id": 95708, "user_id": 70753, "location_id": 83977, "datetime": "2026-03-02T09:00:00+05:00", "status": "in", "source": "device", "latitude": 43.238949, "longitude": 76.889709, "address": null, "comment": null }, { "id": 36006, "user_id": 70753, "location_id": 83977, "datetime": "2026-03-02T18:00:00+05:00", "status": "out", "source": "device", "latitude": 43.238949, "longitude": 76.889709, "address": null, "comment": null } ], "links": { "first": null, "last": null, "prev": null, "next": null }, "meta": { "path": "https://api.clockster.com/company/v3/attendance", "per_page": 50, "next_cursor": null, "prev_cursor": null } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Marks inside a window of days, oldest first.\n\n**The window is required and capped at 90 days.** This is the largest table in the\nproduct; an unbounded read of it has no plan that finishes. `date_from` and `date_to`\nare plain dates matched against the clock the mark was stamped with, not against an\ninstant — a day means the same thing here as it does to the person who worked it.\n\n**`datetime` is the moment as it was recorded**, offset included — the stored wall clock\nread in the stored zone, never shifted anywhere else. It is one field because it is one\nfact: the wall clock is the value without its offset, and the zone is the offset. On the\nrare row whose zone cannot be read it comes back with no offset at all, which is how you\nsee that the zone was not known.\n\n`status` is `in`, `out` or `break`, not the integer the column keeps.\n\nLate arrivals are the one thing to plan for: a device that was offline uploads what it\nrecorded earlier, so a mark can appear inside a window you have already read. Re-read\nthe last few days with overlap rather than paging strictly forward and never looking\nback.", "security": [ { "companyToken": [ "attendance:read" ] } ] }, "post": { "operationId": "company.v3.attendance.store", "tags": [ "Attendance" ], "summary": "Record attendance", "parameters": [], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "user_id": { "type": "integer" }, "datetime": { "type": "string" }, "status": { "type": "string" }, "id": { "type": "integer" }, "result": { "type": "string" } }, "required": [ "user_id", "datetime", "status", "id", "result" ] } } }, "required": [ "data" ] }, "example": { "data": [ { "user_id": 70753, "datetime": "2026-03-02T09:03:00+05:00", "status": "in", "id": 81597, "result": "unchanged" }, { "user_id": 70753, "datetime": "2026-03-02T16:55:00+05:00", "status": "out", "id": 77979, "result": "created" } ] } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Marks recorded by something you run — a turnstile of your own, a kiosk, your app.\n\n**`datetime` carries its own offset and is the only place time is stated** — send\n`2026-08-10T09:03:00+05:00` and `09:03:00` goes on file with `+05:00` beside it. There\nis no separate timezone field, and the offset is not optional: the wall clock and the\nzone are read off that one value, so they cannot disagree. A mark may not be dated in the\nfuture, nor more than 24 hours back: older than that is history being rewritten, and\nlateness already computed against the day would move under it.\n\n`status` is `in`, `out` or `break`, and the answer echoes it back in those words, with\nthe moment as the instant it was recorded at — the same shapes the listing answers with.\n\n**A mark already on file for the same person, moment and direction is not written\nagain**, whether it repeats across two requests or inside one. There is no key you own\non this resource, so no promise of idempotency is made in general — but a retried upload\nwill not double somebody's day. The answer says `created` or `unchanged` per item, with\nthe id either way.\n\nThe shift a mark belongs to is worked out afterwards, so `shift_id` is yours to send\nonly if you already know it.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "attendance": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "object", "properties": { "user_id": { "type": "integer", "description": "The employee this belongs to, by the id this API issued." }, "location_id": { "type": [ "integer", "null" ], "description": "Where the mark was made, by id." }, "shift_id": { "type": [ "integer", "null" ], "description": "The shift this mark belongs to, by id, where you know which one it is." }, "status": { "type": "string", "enum": [ "out", "in", "break" ], "x-clockster-set": "AttendanceStatus", "description": "What the mark is: coming in, going out, or going on a break." }, "datetime": { "type": "string", "format": "date-time", "description": "When it happened, as `2026-08-01T09:00:00+05:00`. The offset is part of it rather than optional. Not in the future, and at most 24 hours late." }, "comment": { "type": [ "string", "null" ], "maxLength": 250, "description": "A note carried alongside, for people to read." } }, "required": [ "user_id", "status", "datetime" ] }, "description": "The marks to record, up to 100 a call." } }, "required": [ "attendance" ] } } } }, "security": [ { "companyToken": [ "attendance:write" ] } ] } }, "/company/v3/departments": { "get": { "operationId": "company.v3.departments.index", "tags": [ "Departments" ], "summary": "List departments", "parameters": [ { "name": "per_page", "in": "query", "required": false, "description": "How many rows one page holds. Defaults to 50.", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } }, { "name": "cursor", "in": "query", "required": false, "description": "The `meta.next_cursor` of the previous page. Omit it for the first. A cursor is bound to the filters it was issued under — change them and start again.", "schema": { "type": [ "string", "null" ], "maxLength": 512 } }, { "name": "search", "in": "query", "required": false, "description": "Free text over the names the section lists.", "schema": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 200 } }, { "name": "updated_since", "in": "query", "required": false, "description": "Only rows changed at or after this instant (ISO 8601). The cheap way to sync: ask for what moved, not for everything.", "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "include", "in": "query", "required": false, "description": "Relations to load, comma-separated. Anything not named is absent from the answer rather than null.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "enum": [ "managers" ], "x-clockster-set": "DepartmentsInclude" } } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "title": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" }, "managers": { "type": "array", "items": { "$ref": "#/components/schemas/EmployeeShort" } } }, "required": [ "id", "external_id", "title", "description", "created_at", "updated_at" ] } }, "links": { "$ref": "#/components/schemas/PageLinks" }, "meta": { "$ref": "#/components/schemas/PageMeta" } }, "required": [ "data", "links", "meta" ] }, "example": { "data": [ { "id": 85573, "external_id": null, "title": "Administration", "description": null, "created_at": "2026-03-02T12:00:00+00:00", "updated_at": "2026-03-02T12:00:00+00:00" }, { "id": 88746, "external_id": null, "title": "Staff", "description": null, "created_at": "2026-03-02T12:00:00+00:00", "updated_at": "2026-03-02T12:00:00+00:00" }, { "id": 18620, "external_id": null, "title": "Ops", "description": null, "created_at": "2026-03-02T12:00:00+00:00", "updated_at": "2026-03-02T12:00:00+00:00" } ], "links": { "first": null, "last": null, "prev": null, "next": null }, "meta": { "path": "https://api.clockster.com/company/v3/departments", "per_page": 50, "next_cursor": null, "prev_cursor": null } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "`include=managers` adds the managers. Without it the key is absent, never null standing in for \"not asked for\".", "security": [ { "companyToken": [ "structure:read" ] } ] } }, "/company/v3/departments/upsert": { "post": { "operationId": "company.v3.departments.upsert", "tags": [ "Departments" ], "summary": "Create or update departments", "parameters": [], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/UpsertOutcome" } } }, "required": [ "data" ] }, "example": { "data": [ { "external_id": "HR-DEP-1", "id": 41132, "result": "updated" }, { "external_id": "HR-DEP-2", "id": 58294, "result": "created" } ] } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Up to 100 entries in one call, matched on `external_id`.\n\n**Not matched on the name.** Renaming an entry on your side updates ours, where matching\non `title` would have created a second and orphaned the first.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "items": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "object", "properties": { "external_id": { "type": "string", "minLength": 1, "maxLength": 64, "description": "Your own key for this row. Send it on every write and the next one updates rather than duplicates." }, "title": { "type": "string", "maxLength": 200, "description": "The name this is shown under." }, "description": { "type": [ "string", "null" ], "maxLength": 500, "description": "Free text about this row, for people rather than for your code." } }, "required": [ "external_id", "title" ] }, "description": "The rows to write. Each carries your own `external_id`, and a row already stored under that key is updated rather than added." } }, "required": [ "items" ] } } } }, "security": [ { "companyToken": [ "structure:write" ] } ] } }, "/company/v3/departments/{id}": { "get": { "operationId": "company.v3.departments.show", "tags": [ "Departments" ], "summary": "Read one department", "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } }, { "name": "include", "in": "query", "required": false, "description": "Relations to load, comma-separated. Anything not named is absent from the answer rather than null.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "enum": [ "managers" ], "x-clockster-set": "DepartmentsInclude" } } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "title": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" }, "managers": { "type": "array", "items": { "$ref": "#/components/schemas/EmployeeShort" } } }, "required": [ "id", "external_id", "title", "description", "created_at", "updated_at" ] } }, "required": [ "data" ] }, "example": { "data": { "id": 18620, "external_id": null, "title": "Ops", "description": null, "created_at": "2026-03-02T12:00:00+00:00", "updated_at": "2026-03-02T12:00:00+00:00" } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "The same keys and the same `include` vocabulary as the listing — `include=managers` adds the managers. Somebody else's id is a `404`.", "security": [ { "companyToken": [ "structure:read" ] } ] }, "delete": { "operationId": "company.v3.departments.destroy", "tags": [ "Departments" ], "summary": "Delete a department", "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/DeleteOutcome" } }, "required": [ "data" ] }, "example": { "data": { "id": 80256, "result": "deleted" } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "409": { "description": "Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "location_in_use", "message": "People still work at this location. Move them first, then delete it.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "**Refused while anybody is in it** — 409, `department_in_use`.\n\nOnce it goes, so do its managers, and there is no way to reconstruct who was in it.\n\nMove the people first, then delete. Somebody dismissed does not count as being in it.", "security": [ { "companyToken": [ "structure:write" ] } ] } }, "/company/v3/documents": { "get": { "operationId": "company.v3.documents.index", "tags": [ "Documents" ], "summary": "List documents", "parameters": [ { "name": "per_page", "in": "query", "required": false, "description": "How many rows one page holds. Defaults to 50.", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } }, { "name": "cursor", "in": "query", "required": false, "description": "The `meta.next_cursor` of the previous page. Omit it for the first. A cursor is bound to the filters it was issued under — change them and start again.", "schema": { "type": [ "string", "null" ], "maxLength": 512 } }, { "name": "party", "in": "query", "required": false, "description": "Whose side of the document to read.", "schema": { "type": [ "string", "null" ], "enum": [ "employee", "counterparty" ], "x-clockster-set": "DocumentsParty" } }, { "name": "external_ids", "in": "query", "required": false, "description": "Only rows carrying these keys of yours. The other half of an upsert: write with your key, read back with it.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "string", "maxLength": 64 } } }, { "name": "users", "in": "query", "required": false, "description": "Only rows belonging to these people, by id.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "locations", "in": "query", "required": false, "description": "Only these locations, by id.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "departments", "in": "query", "required": false, "description": "Only these departments, by id.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "positions", "in": "query", "required": false, "description": "Only these positions, by id.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "user_filters", "in": "query", "required": false, "description": "Only these user filters, by id.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "types", "in": "query", "required": false, "description": "Only rows of these types.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 49, "items": { "type": "string", "enum": [ "passport", "cv", "diploma", "medical", "photo", "other", "medical_book", "employment_agreement", "termination_of_employment_agreement", "equipment_agreement", "application", "order", "supplementary_agreement", "job_description", "nda", "non_compete_agreement", "data_processing_agreement", "act_of_service_acceptance", "health_and_safety_briefing", "shift_schedule", "letter", "vacation_schedule", "contract", "agreement", "goods_release_note", "reconciliation_act", "return_to_supplier", "driver_license", "birth_certificate", "marriage_certificate", "divorce_certificate", "change_fio_certificate", "srts", "vaccination", "social_id", "disability_certificate", "large_family_certificate", "asp_certificate", "tech_passport", "pension", "rk_passport", "student_card", "vnzh", "pcr_certificate", "lbg_card", "insurance_policy", "hunter", "oralman", "attorney" ], "x-clockster-set": "DocumentsType" } } }, { "name": "employment_types", "in": "query", "required": false, "description": "Only documents covering these employment terms.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 10, "items": { "type": "string", "enum": [ "full_time", "part_time", "irregular_hours", "contract_1", "contract_2", "apprenticeship", "traineeship", "piece_rate", "probation", "outstaffing" ], "x-clockster-set": "DocumentsEmploymentType" } } }, { "name": "search", "in": "query", "required": false, "description": "Free text over the names the section lists.", "schema": { "type": [ "string", "null" ], "maxLength": 50 } }, { "name": "updated_since", "in": "query", "required": false, "description": "Only rows changed at or after this instant (ISO 8601). The cheap way to sync: ask for what moved, not for everything.", "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "expires_after", "in": "query", "required": false, "description": "Expiring after this date (YYYY-MM-DD) — how you find what is about to lapse.", "schema": { "type": [ "string", "null" ], "format": "date" } }, { "name": "expires_before", "in": "query", "required": false, "description": "Expiring before this date (YYYY-MM-DD).", "schema": { "type": [ "string", "null" ], "format": "date" } }, { "name": "effective_from", "in": "query", "required": false, "description": "In force at or after this date (YYYY-MM-DD).", "schema": { "type": [ "string", "null" ], "format": "date" } }, { "name": "effective_to", "in": "query", "required": false, "description": "In force at or before this date (YYYY-MM-DD).", "schema": { "type": [ "string", "null" ], "format": "date" } }, { "name": "include", "in": "query", "required": false, "description": "Relations to load, comma-separated. Anything not named is absent from the answer rather than null.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "enum": [ "attachments", "signers", "labor_contract" ], "x-clockster-set": "DocumentsInclude" } } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "user_id": { "type": "integer" }, "author_id": { "type": "integer" }, "party": { "type": "string" }, "type": { "type": "string" }, "name": { "type": "string" }, "contract_number": { "type": [ "null", "string" ] }, "employment_type": { "type": [ "null", "string" ] }, "start_date": { "type": [ "null", "string" ] }, "end_date": { "type": [ "null", "string" ] }, "expiration_date": { "type": [ "null", "string" ] }, "parent_document_id": { "type": [ "integer", "null" ] }, "signature": { "type": "object", "properties": { "state": { "type": "string" }, "completed_at": { "type": [ "null", "string" ] } }, "required": [ "state", "completed_at" ] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" }, "attachments": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "format": { "type": "string" }, "url": { "type": "string" }, "created_at": { "type": "string" } }, "required": [ "id", "name", "description", "format", "url", "created_at" ] } }, "signers": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string" }, "status": { "type": "string" }, "signed_at": { "type": [ "null", "string" ] }, "signed_via": { "type": [ "null", "string" ] }, "party_id": { "type": "integer" }, "party_type": { "type": "string" } }, "required": [ "type", "status", "signed_at", "signed_via", "party_id", "party_type" ] } }, "labor_contract": { "type": [ "null", "object" ], "properties": { "id": { "type": "integer" }, "external_contract_id": { "type": "string" }, "iin": { "type": "string" }, "contract_number": { "type": [ "null", "string" ] }, "contract_date": { "type": [ "null", "string" ] }, "begin_date": { "type": [ "null", "string" ] }, "end_date": { "type": [ "null", "string" ] }, "termination_date": { "type": [ "null", "string" ] }, "established_post": { "type": "string" }, "has_contract_file": { "type": "boolean" } }, "required": [ "id", "external_contract_id", "iin", "contract_number", "contract_date", "begin_date", "end_date", "termination_date", "established_post", "has_contract_file" ] } }, "required": [ "id", "external_id", "user_id", "author_id", "party", "type", "name", "contract_number", "employment_type", "start_date", "end_date", "expiration_date", "parent_document_id", "signature", "created_at", "updated_at" ] } }, "links": { "$ref": "#/components/schemas/PageLinks" }, "meta": { "$ref": "#/components/schemas/PageMeta" } }, "required": [ "data", "links", "meta" ] }, "example": { "data": [ { "id": 86663, "external_id": "HR-DOC-4471", "user_id": 70753, "author_id": 70753, "party": "employee", "type": "employment_agreement", "name": "Employment agreement", "contract_number": "EA-2026-0114", "employment_type": "full_time", "start_date": "2026-01-14", "end_date": null, "expiration_date": "2027-01-13", "parent_document_id": null, "signature": { "state": "pending", "completed_at": null }, "created_at": "2026-03-02T09:00:00+00:00", "updated_at": "2026-03-02T12:30:00+00:00" }, { "id": 86663, "external_id": "HR-DOC-4472", "user_id": 70753, "author_id": 70753, "party": "employee", "type": "nda", "name": "NDA", "contract_number": null, "employment_type": null, "start_date": null, "end_date": null, "expiration_date": null, "parent_document_id": null, "signature": { "state": "none", "completed_at": null }, "created_at": "2026-03-02T09:00:00+00:00", "updated_at": "2026-03-02T12:30:00+00:00" } ], "links": { "first": null, "last": null, "prev": null, "next": null }, "meta": { "path": "https://api.clockster.com/company/v3/documents", "per_page": 50, "next_cursor": null, "prev_cursor": null } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "The company's paperwork, oldest first, paged on a cursor.\n\n**Both kinds of document.** `party` is `employee` for a document about one of your\npeople and `counterparty` for one a counterparty signs. Filter with `party=employee` or\n`party=counterparty`; leave it out for both.\n\n**`signature.state` is derived, because there is no column for it.** `none` when nobody\nhas to sign, then `rejected`, `revoked`, `pending` in that order of precedence, and\n`signed` only when every signer has. Refusal outranks an outstanding signature, so a\ndocument one person refused reads `rejected` even while others are still pending.\n`completed_at` is set only for `signed`, and is when the last signer signed.\n\n**Dates are three shapes in one payload.** `start_date`, `end_date` and\n`expiration_date` are plain `YYYY-MM-DD` — they are date columns and carry no time or\nzone. `created_at` is an instant with an offset.\n\n`expires_after` and `expires_before` are a plain range of dates.\n\n`effective_from` and `effective_to` select documents valid during a window: a document\nstarts on or before your `effective_to` and either has no end date or ends on or after\nyour `effective_from`. Both are required together. **A document with no `start_date`\nnever matches** — a row with no interval cannot overlap one.\n\n`locations`, `departments`, `positions` and `user_filters` all reach the document\nthrough the person it is about, so **a document with no `user_id` matches none of\nthem**. The web app files company-level documents that way.\n\n**`updated_since` reads only what changed**, as an instant rather than a date so a\ncaller polling every few minutes can say which minute. Two things it will not show you,\nand both are ours rather than yours: a document changed by a path inside the product\nthat writes the row directly keeps its old timestamp, and so does one the back office\nmodifies. Signature progress is the common case of the second — a signature completed\nthrough the web app does not move `updated_at`. Re-read with overlap if you depend on\ncatching those.\n\nDocuments the product files for its own machinery are never listed.", "security": [ { "companyToken": [ "documents:read" ] } ] } }, "/company/v3/documents/upsert": { "post": { "operationId": "company.v3.documents.upsert", "tags": [ "Documents" ], "summary": "File documents", "parameters": [], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/UpsertOutcome" } } }, "required": [ "data" ] }, "example": { "data": [ { "external_id": "HR-DOC-4471", "id": 86663, "result": "created" }, { "external_id": "HR-DOC-4472", "id": 86663, "result": "created" } ] } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Up to 100 documents in one call, matched on `external_id`, which is required here.\n\n**The bytes go up separately.** `POST /company/v3/files` takes one file and answers an\nid; send that as `file_id`. A file may be claimed once: one already hanging off a\ndocument is refused rather than moved.\n\n**An uploaded file waits to be claimed and is not cleaned up.** A batch refused at the\nhundredth document leaves all hundred files staged and still valid, so a retry should\nsend the same `file_id`s rather than uploading them again.\n\n`parent_external_id` links a supplementary agreement to what it amends, by your key\nrather than ours. It resolves against documents already filed — not against another item\nof the same batch — and a key matching nothing clears the link rather than failing.\n\n`user_id` is required and may name somebody who has left: termination paperwork is filed\nafter a dismissal, which is exactly when it is needed.\n\n**No signers.** Signing runs conversions and outbound calls that do not belong under a\nversioned contract, so documents filed here are always `party: employee`. Signature\nstate is readable; creating a signing request is not offered yet.\n\n`author_id` is set to the subject: this token authenticates a company, not a person.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "documents": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "object", "properties": { "external_id": { "type": "string", "minLength": 1, "maxLength": 64, "description": "Your own key for this row. Send it on every write and the next one updates rather than duplicates." }, "type": { "type": "string", "enum": [ "passport", "cv", "diploma", "medical", "photo", "other", "medical_book", "employment_agreement", "termination_of_employment_agreement", "equipment_agreement", "application", "order", "supplementary_agreement", "job_description", "nda", "non_compete_agreement", "data_processing_agreement", "act_of_service_acceptance", "health_and_safety_briefing", "shift_schedule", "letter", "vacation_schedule", "contract", "agreement", "goods_release_note", "reconciliation_act", "return_to_supplier", "driver_license", "birth_certificate", "marriage_certificate", "divorce_certificate", "change_fio_certificate", "srts", "vaccination", "social_id", "disability_certificate", "large_family_certificate", "asp_certificate", "tech_passport", "pension", "rk_passport", "student_card", "vnzh", "pcr_certificate", "lbg_card", "insurance_policy", "hunter", "oralman", "attorney" ], "x-clockster-set": "DocumentsType", "description": "Which kind of document this is." }, "user_id": { "type": "integer", "description": "The employee this belongs to, by the id this API issued." }, "name": { "type": [ "string", "null" ], "maxLength": 255, "description": "What to call this document." }, "contract_number": { "type": [ "string", "null" ], "maxLength": 255, "description": "The number written on the contract." }, "employment_type": { "type": [ "string", "null" ], "enum": [ "full_time", "part_time", "irregular_hours", "contract_1", "contract_2", "apprenticeship", "traineeship", "piece_rate", "probation", "outstaffing" ], "x-clockster-set": "DocumentsEmploymentType", "description": "The terms the contract is on." }, "start_date": { "type": [ "string", "null" ], "format": "date", "description": "The day it begins, `YYYY-MM-DD`." }, "end_date": { "type": [ "string", "null" ], "format": "date", "description": "The day it ends, `YYYY-MM-DD`." }, "expiration_date": { "type": [ "string", "null" ], "format": "date", "description": "The day it stops being valid, `YYYY-MM-DD`." }, "parent_external_id": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 64, "description": "The document this one hangs under, by your key for that one." }, "file_id": { "type": [ "integer", "null" ], "description": "The stored file this points at, from `POST /files`. Upload the bytes first and name the id it answered with." } }, "required": [ "external_id", "type", "user_id" ] }, "description": "The documents to write, up to 100 a call." } }, "required": [ "documents" ] } } } }, "security": [ { "companyToken": [ "documents:write" ] } ] } }, "/company/v3/documents/{id}": { "get": { "operationId": "company.v3.documents.show", "tags": [ "Documents" ], "summary": "Read one document", "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } }, { "name": "include", "in": "query", "required": false, "description": "Relations to load, comma-separated. Anything not named is absent from the answer rather than null.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "enum": [ "attachments", "signers", "labor_contract" ], "x-clockster-set": "DocumentsInclude" } } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "user_id": { "type": "integer" }, "author_id": { "type": "integer" }, "party": { "type": "string" }, "type": { "type": "string" }, "name": { "type": "string" }, "contract_number": { "type": [ "null", "string" ] }, "employment_type": { "type": [ "null", "string" ] }, "start_date": { "type": [ "null", "string" ] }, "end_date": { "type": [ "null", "string" ] }, "expiration_date": { "type": [ "null", "string" ] }, "parent_document_id": { "type": [ "integer", "null" ] }, "signature": { "type": "object", "properties": { "state": { "type": "string" }, "completed_at": { "type": [ "null", "string" ] } }, "required": [ "state", "completed_at" ] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" }, "attachments": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "format": { "type": "string" }, "url": { "type": "string" }, "created_at": { "type": "string" } }, "required": [ "id", "name", "description", "format", "url", "created_at" ] } }, "signers": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string" }, "status": { "type": "string" }, "signed_at": { "type": [ "null", "string" ] }, "signed_via": { "type": [ "null", "string" ] }, "party_id": { "type": "integer" }, "party_type": { "type": "string" } }, "required": [ "type", "status", "signed_at", "signed_via", "party_id", "party_type" ] } }, "labor_contract": { "type": [ "null", "object" ], "properties": { "id": { "type": "integer" }, "external_contract_id": { "type": "string" }, "iin": { "type": "string" }, "contract_number": { "type": [ "null", "string" ] }, "contract_date": { "type": [ "null", "string" ] }, "begin_date": { "type": [ "null", "string" ] }, "end_date": { "type": [ "null", "string" ] }, "termination_date": { "type": [ "null", "string" ] }, "established_post": { "type": "string" }, "has_contract_file": { "type": "boolean" } }, "required": [ "id", "external_contract_id", "iin", "contract_number", "contract_date", "begin_date", "end_date", "termination_date", "established_post", "has_contract_file" ] } }, "required": [ "id", "external_id", "user_id", "author_id", "party", "type", "name", "contract_number", "employment_type", "start_date", "end_date", "expiration_date", "parent_document_id", "signature", "created_at", "updated_at" ] } }, "required": [ "data" ] }, "example": { "data": { "id": 86663, "external_id": "HR-DOC-4471", "user_id": 70753, "author_id": 70753, "party": "employee", "type": "employment_agreement", "name": "Employment agreement", "contract_number": "EA-2026-0114", "employment_type": "full_time", "start_date": "2026-01-14", "end_date": null, "expiration_date": "2027-01-13", "parent_document_id": null, "signature": { "state": "pending", "completed_at": null }, "created_at": "2026-03-02T09:00:00+00:00", "updated_at": "2026-03-02T12:30:00+00:00" } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "The same keys the listing answers with, and the same `include` vocabulary. Another company's id is a `404`.", "security": [ { "companyToken": [ "documents:read" ] } ] }, "delete": { "operationId": "company.v3.documents.destroy", "tags": [ "Documents" ], "summary": "Delete a document", "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/DeleteOutcome" } }, "required": [ "data" ] }, "example": { "data": { "id": 86663, "result": "deleted" } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Deletes the document, its file records and the stored objects behind them.\n\n**A document everyone has signed cannot be deleted** — `409`, code `document_signed`.\n\nAnother company's id is a `404`.", "security": [ { "companyToken": [ "documents:write" ] } ] } }, "/company/v3/files": { "post": { "operationId": "company.v3.files.store", "tags": [ "Files" ], "summary": "Upload a file", "parameters": [], "responses": { "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "format": { "type": "string" }, "url": { "type": "string" }, "created_at": { "type": "string" } }, "required": [ "id", "name", "format", "url", "created_at" ] } }, "required": [ "data" ] }, "example": { "data": { "id": 95644, "name": "agreement", "format": "pdf", "url": "https://storage.clockster.com/files/9f21c0b4.pdf?X-Amz-Signature=abc123", "created_at": "2026-03-02T09:00:00+00:00" } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "One file, `multipart/form-data`, field name `file`. The only route on this surface that\nis not JSON.\n\nIt answers an `id`. That id is what `POST /company/v3/documents/upsert` takes as\n`file_id`; until a document claims it the file belongs to nothing.\n\nUp to 10 MB. `pdf`, `doc`, `docx`, `xls`, `xlsx`, `jpg`, `jpeg`, `png` — the content is\nchecked, not just the extension. `name` is optional and defaults to the uploaded\nfilename without its extension.\n\n`url` is signed and short-lived: read it, do not store it. Read the document again to\nget a fresh one.", "requestBody": { "required": true, "content": { "multipart/form-data": { "schema": { "type": "object", "properties": { "file": { "type": "string", "format": "binary" }, "name": { "type": [ "string", "null" ], "maxLength": 100, "description": "What to call this document." } }, "required": [ "file" ] } } } }, "security": [ { "companyToken": [ "documents:write" ] } ] } }, "/company/v3/locations": { "get": { "operationId": "company.v3.locations.index", "tags": [ "Locations" ], "summary": "List locations", "parameters": [ { "name": "per_page", "in": "query", "required": false, "description": "How many rows one page holds. Defaults to 50.", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } }, { "name": "cursor", "in": "query", "required": false, "description": "The `meta.next_cursor` of the previous page. Omit it for the first. A cursor is bound to the filters it was issued under — change them and start again.", "schema": { "type": [ "string", "null" ], "maxLength": 512 } }, { "name": "search", "in": "query", "required": false, "description": "Free text over the names the section lists.", "schema": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 200 } }, { "name": "include", "in": "query", "required": false, "description": "Relations to load, comma-separated. Anything not named is absent from the answer rather than null.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "enum": [ "managers" ], "x-clockster-set": "LocationsInclude" } } }, { "name": "codes", "in": "query", "required": false, "description": "Only these employee codes.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 200, "items": { "type": "string", "maxLength": 50 } } }, { "name": "updated_since", "in": "query", "required": false, "description": "Only rows changed at or after this instant (ISO 8601). The cheap way to sync: ask for what moved, not for everything.", "schema": { "type": [ "string", "null" ], "format": "date-time" } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "code": { "type": [ "null", "string" ] }, "title": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "latitude": { "type": [ "null", "number" ] }, "longitude": { "type": [ "null", "number" ] }, "radius": { "type": "integer" }, "created_at": { "type": "string" }, "updated_at": { "type": "string" }, "managers": { "type": "array", "items": { "$ref": "#/components/schemas/EmployeeShort" } } }, "required": [ "id", "external_id", "code", "title", "description", "latitude", "longitude", "radius", "created_at", "updated_at" ] } }, "links": { "$ref": "#/components/schemas/PageLinks" }, "meta": { "$ref": "#/components/schemas/PageMeta" } }, "required": [ "data", "links", "meta" ] }, "example": { "data": [ { "id": 94678, "external_id": null, "code": null, "title": "Office", "description": null, "latitude": null, "longitude": null, "radius": 100, "created_at": "2026-03-02T12:00:00+00:00", "updated_at": "2026-03-02T12:00:00+00:00" }, { "id": 83977, "external_id": null, "code": "WH-01", "title": "Warehouse", "description": "Ground floor", "latitude": 43.238949, "longitude": 76.889709, "radius": 150, "created_at": "2026-03-02T12:00:00+00:00", "updated_at": "2026-03-02T12:00:00+00:00" } ], "links": { "first": null, "last": null, "prev": null, "next": null }, "meta": { "path": "https://api.clockster.com/company/v3/locations", "per_page": 50, "next_cursor": null, "prev_cursor": null } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Ordered by `id` and paged on a cursor: no page number, no total, and nothing repeated\nor skipped while the list is written to. A cursor issued for another ordering is\nrefused rather than silently restarting the list.\n\nCoordinates are numbers, and a latitude of exactly 0 is a coordinate rather than a\nmissing one.\n\n`include=managers` adds the employees who manage the location, as on departments and\nuser filters.", "security": [ { "companyToken": [ "structure:read" ] } ] } }, "/company/v3/locations/upsert": { "post": { "operationId": "company.v3.locations.upsert", "tags": [ "Locations" ], "summary": "Create or update locations", "parameters": [], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/UpsertOutcome" } } }, "required": [ "data" ] }, "example": { "data": [ { "external_id": "HR-DEP-1", "id": 41132, "result": "updated" }, { "external_id": "HR-DEP-2", "id": 58294, "result": "created" } ] } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Up to 100 entries in one call, matched on `external_id`.\n\n**Not matched on the name.** Renaming an entry on your side updates ours, where matching\non `title` would have created a second and orphaned the first.\n\n`code` and `external_id` are different things and both are kept: `code` is a label you\nfill in and we never validate, `external_id` is what the match runs on. Coordinates and\nradius are set here too.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "items": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "object", "properties": { "external_id": { "type": "string", "minLength": 1, "maxLength": 64, "description": "Your own key for this row. Send it on every write and the next one updates rather than duplicates." }, "title": { "type": "string", "maxLength": 200, "description": "The name this is shown under." }, "description": { "type": [ "string", "null" ], "maxLength": 500, "description": "Free text about this row, for people rather than for your code." }, "code": { "type": [ "string", "null" ], "maxLength": 50, "description": "A short code people read, yours to choose. The listing beside this write can filter on it." }, "latitude": { "type": [ "number", "null" ], "minimum": -90, "maximum": 90, "description": "Where the location is. A mobile clock-in is checked against this and `radius`." }, "longitude": { "type": [ "number", "null" ], "minimum": -180, "maximum": 180, "description": "Where the location is. A mobile clock-in is checked against this and `radius`." }, "radius": { "type": "integer", "minimum": 50, "maximum": 700, "description": "How far from those coordinates a mobile clock-in still counts, in metres. A location written without one gets 100." } }, "required": [ "external_id", "title" ] }, "description": "The rows to write. Each carries your own `external_id`, and a row already stored under that key is updated rather than added." } }, "required": [ "items" ] } } } }, "security": [ { "companyToken": [ "structure:write" ] } ] } }, "/company/v3/locations/{id}": { "get": { "operationId": "company.v3.locations.show", "tags": [ "Locations" ], "summary": "Read one location", "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } }, { "name": "include", "in": "query", "required": false, "description": "Relations to load, comma-separated. Anything not named is absent from the answer rather than null.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "enum": [ "managers" ], "x-clockster-set": "LocationsInclude" } } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "code": { "type": [ "null", "string" ] }, "title": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "latitude": { "type": [ "null", "number" ] }, "longitude": { "type": [ "null", "number" ] }, "radius": { "type": "integer" }, "created_at": { "type": "string" }, "updated_at": { "type": "string" }, "managers": { "type": "array", "items": { "$ref": "#/components/schemas/EmployeeShort" } } }, "required": [ "id", "external_id", "code", "title", "description", "latitude", "longitude", "radius", "created_at", "updated_at" ] } }, "required": [ "data" ] }, "example": { "data": { "id": 83977, "external_id": null, "code": "WH-01", "title": "Warehouse", "description": "Ground floor", "latitude": 43.238949, "longitude": 76.889709, "radius": 150, "created_at": "2026-03-02T12:00:00+00:00", "updated_at": "2026-03-02T12:00:00+00:00" } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "The same keys the listing answers with. Somebody else's id is a `404`, where asking the listing for it answers `200` with an empty array and leaves you counting.", "security": [ { "companyToken": [ "structure:read" ] } ] }, "delete": { "operationId": "company.v3.locations.destroy", "tags": [ "Locations" ], "summary": "Delete a location", "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/DeleteOutcome" } }, "required": [ "data" ] }, "example": { "data": { "id": 80256, "result": "deleted" } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "409": { "description": "Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "location_in_use", "message": "People still work at this location. Move them first, then delete it.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "**Refused while anybody works there** — 409, `location_in_use` — counting both the\nlocation on a person's record and the several they may also be assigned to.\n\n**Refused while anything sits beneath it** — 409, `location_has_children`. Deleting a\nparent detaches its whole subtree and destroys the rows describing the ancestry, and\nthis API cannot express a hierarchy at all, so you would not be able to see what you\nhad taken apart.\n\nOnce it goes, so do its managers, its device assignments and any auto-scheduler\nconfigured for it; devices, schedules, tasks and approval routes keep working with the\nlocation set to null. None of that is recoverable and none of it is logged.\n\nMove the people first, then delete.", "security": [ { "companyToken": [ "structure:write" ] } ] } }, "/company/v3/me": { "get": { "operationId": "company.v3.me", "tags": [ "Token" ], "summary": "Whose token this is", "parameters": [], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "integer" }, "title": { "type": "string" } }, "required": [ "id", "title" ] } }, "required": [ "data" ] }, "example": { "data": { "id": 79823, "title": "Golden Co" } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Confirms which company a key belongs to.\n\nAnswers the id and the name, and nothing else: everything a key opens is reachable from\nthe endpoints themselves, and a company attribute this surface does not act on would\nonly read as one it does." } }, "/company/v3/payroll/payslips": { "get": { "operationId": "company.v3.payroll.payslips", "tags": [ "Payroll" ], "summary": "List payslips", "parameters": [ { "name": "per_page", "in": "query", "required": false, "description": "How many rows one page holds. Defaults to 50.", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } }, { "name": "cursor", "in": "query", "required": false, "description": "The `meta.next_cursor` of the previous page. Omit it for the first. A cursor is bound to the filters it was issued under — change them and start again.", "schema": { "type": [ "string", "null" ], "maxLength": 512 } }, { "name": "users", "in": "query", "required": false, "description": "Only rows belonging to these people, by id.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "statuses", "in": "query", "required": false, "description": "Only rows in these states.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 3, "items": { "type": "string", "enum": [ "draft", "approved", "paid" ], "x-clockster-set": "PayrollPayslipsStatus" } } }, { "name": "months", "in": "query", "required": false, "description": "Only these months, as YYYY-MM.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 36, "items": { "type": "string", "format": "date-time" } } }, { "name": "updated_since", "in": "query", "required": false, "description": "Only rows changed at or after this instant (ISO 8601). The cheap way to sync: ask for what moved, not for everything.", "schema": { "type": [ "string", "null" ], "format": "date-time" } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "user": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] } }, "required": [ "id", "external_id" ] }, "author_id": { "type": "integer" }, "period": { "type": "object", "properties": { "from": { "type": "string" }, "to": { "type": "string" }, "month": { "type": "string" } }, "required": [ "from", "to", "month" ] }, "status": { "type": "string" }, "currency": { "type": [ "null", "string" ] }, "take_home": { "type": "number" }, "ctc": { "type": "integer" }, "salary": { "type": "object", "properties": { "basic_rate": { "type": [ "null", "number" ] }, "basic_type": { "type": [ "null", "string" ] }, "work_days": { "type": [ "integer", "null" ] }, "worked_days": { "type": [ "integer", "null" ] } }, "required": [ "basic_rate", "basic_type", "work_days", "worked_days" ] }, "additions": { "type": "array", "items": { "type": "object", "properties": { "title": { "type": "string" }, "type": { "type": "string" }, "value": { "type": "integer" }, "pre_tax": { "type": "boolean" }, "comment": { "type": [ "null", "string" ] } }, "required": [ "title", "type", "value", "pre_tax", "comment" ] } }, "deductions": { "type": "array", "items": { "type": "object", "properties": { "title": { "type": "string" }, "type": { "type": "string" }, "value": { "type": "number" }, "pre_tax": { "type": "boolean" }, "comment": { "type": [ "null", "string" ] } }, "required": [ "title", "type", "value", "pre_tax", "comment" ] } }, "allowances": { "type": "array", "items": { "type": "object", "properties": { "title": { "type": "string" }, "type": { "type": "string" }, "value": { "type": "integer" }, "pre_tax": { "type": "boolean" }, "comment": { "type": [ "null", "string" ] } }, "required": [ "title", "type", "value", "pre_tax", "comment" ] } }, "loan_repaid": { "type": "integer" }, "updated_at": { "type": "string" } }, "required": [ "id", "user", "author_id", "period", "status", "currency", "take_home", "ctc", "salary", "additions", "deductions", "allowances", "loan_repaid", "updated_at" ] } }, "meta": { "type": "object", "properties": { "per_page": { "type": "integer" }, "next_cursor": { "type": [ "null", "string" ] }, "prev_cursor": { "type": [ "null", "string" ] } }, "required": [ "per_page", "next_cursor", "prev_cursor" ] } }, "required": [ "data", "meta" ] }, "example": { "data": [ { "id": 14583, "user": { "id": 11251, "external_id": null }, "author_id": 11251, "period": { "from": "2026-03-01", "to": "2026-03-31", "month": "2026-03" }, "status": "approved", "currency": "KZT", "take_home": 1250.5, "ctc": 1500, "salary": { "basic_rate": 1000.5, "basic_type": "fixed", "work_days": 21, "worked_days": 20 }, "additions": [ { "title": "Performance bonus", "type": "annual_bonus", "value": 300, "pre_tax": true, "comment": "Q1" } ], "deductions": [ { "title": "Late penalty", "type": "absence", "value": 49.5, "pre_tax": false, "comment": null } ], "allowances": [ { "title": "Meal allowance", "type": "allowance", "value": 100, "pre_tax": false, "comment": null } ], "loan_repaid": 200, "updated_at": "2026-03-02T12:30:00+00:00" } ], "meta": { "per_page": 50, "next_cursor": null, "prev_cursor": null } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Reading only. Nothing on this surface creates or changes a payslip.\n\n**Amounts carry their currency.** A payslip that carries none — one built from a salary\nfiled before the field was required — answers the company's own currency rather than\nnull, so you do not have to invent that fallback yourself.\n\n**The line items are here**: `additions`, `deductions` and `allowances`, each with its\n`title`, `value` and `pre_tax`, which is what decides whether an amount lands before tax\nis worked out.\n\n**`loan_repaid` is what was taken back against an advance.** An advance is not a\ndeduction: it becomes a loan and is repaid on scheduled days, so it never appears in\n`deductions` and a caller subtracting the line items from the total will be out by\nexactly this amount. The figure folds together loan repayments and one-off loan\nadjustments, because that is how the calculation records them.\n\n**The parts do not add up to `take_home`, and are not meant to.** Taxes and the gross\nfigure are not published here, so what you get is what was added, taken off and repaid —\nnot a derivation of the total.\n\n**`updated_since` matters more here than anywhere**: a payslip is recalculated and moves\nfrom `draft` to `approved` to `paid`, so without it a caller re-reads every month\nforever. `months` is `YYYY-MM`, and takes a list, so a quarter is one request.", "security": [ { "companyToken": [ "payroll:read" ] } ] } }, "/company/v3/payroll/single-adjustments": { "get": { "operationId": "company.v3.payroll.single-adjustments.index", "tags": [ "Payroll" ], "summary": "List single adjustments", "parameters": [ { "name": "per_page", "in": "query", "required": false, "description": "How many rows one page holds. Defaults to 50.", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } }, { "name": "cursor", "in": "query", "required": false, "description": "The `meta.next_cursor` of the previous page. Omit it for the first. A cursor is bound to the filters it was issued under — change them and start again.", "schema": { "type": [ "string", "null" ], "maxLength": 512 } }, { "name": "users", "in": "query", "required": false, "description": "Only rows belonging to these people, by id.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "types", "in": "query", "required": false, "description": "Only rows of these types.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 6, "items": { "type": "string", "enum": [ "service_charge", "single_addition_pre_tax", "single_addition_post_tax", "single_loan", "single_deduction_pre_tax", "single_deduction_post_tax" ], "x-clockster-set": "PayrollSingleAdjustmentsType" } } }, { "name": "date_from", "in": "query", "required": false, "description": "Start of the window, inclusive (YYYY-MM-DD).", "schema": { "type": [ "string", "null" ], "format": "date" } }, { "name": "date_to", "in": "query", "required": false, "description": "End of the window, inclusive (YYYY-MM-DD).", "schema": { "type": [ "string", "null" ], "format": "date" } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "user": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] } }, "required": [ "id", "external_id" ] }, "type": { "type": "string" }, "title": { "type": [ "null", "string" ] }, "amount": { "type": [ "null", "number" ] }, "date": { "type": "string" }, "created_at": { "type": "string" } }, "required": [ "id", "user", "type", "title", "amount", "date", "created_at" ] } }, "links": { "$ref": "#/components/schemas/PageLinks" }, "meta": { "$ref": "#/components/schemas/PageMeta" } }, "required": [ "data", "links", "meta" ] }, "example": { "data": [ { "id": 47809, "user": { "id": 11251, "external_id": "HR-1001" }, "type": "single_addition_pre_tax", "title": "Sales bonus", "amount": 300.5, "date": "2026-03-15", "created_at": "2026-03-02T12:00:00+00:00" }, { "id": 47809, "user": { "id": 11251, "external_id": "HR-1001" }, "type": "single_deduction_post_tax", "title": null, "amount": 50.25, "date": "2026-03-20", "created_at": "2026-03-02T12:00:00+00:00" } ], "links": { "first": null, "last": null, "prev": null, "next": null }, "meta": { "path": "https://api.clockster.com/company/v3/payroll/single-adjustments", "per_page": 50, "next_cursor": null, "prev_cursor": null } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "One-off additions and deductions, oldest first, with who they are for and the day they\nare dated.\n\n`amount` is never negative: `type` says whether it is added or taken off, and whether\nbefore or after tax. `date_from` and `date_to` bound the day, inclusive.\n\nRows filed in the web application are listed too, and may carry `13th_pay`, which is\ncomputed there rather than filed here.", "security": [ { "companyToken": [ "payroll:read" ] } ] }, "post": { "operationId": "company.v3.payroll.single-adjustments.store", "tags": [ "Payroll" ], "summary": "Create single adjustments", "parameters": [ { "name": "Idempotency-Key", "in": "header", "required": false, "description": "A value you choose, unique per attempt. Send the same one when retrying and the first answer is replayed instead of the write happening twice. Optional, and only on the writes that carry no key of your own.", "schema": { "type": "string", "maxLength": 255 } } ], "responses": { "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "user": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] } }, "required": [ "id", "external_id" ] }, "type": { "type": "string" }, "title": { "type": [ "null", "string" ] }, "amount": { "type": [ "null", "number" ] }, "date": { "type": "string" }, "created_at": { "type": "string" } }, "required": [ "id", "user", "type", "title", "amount", "date", "created_at" ] } } }, "required": [ "data" ] }, "example": { "data": [ { "id": 47809, "user": { "id": 11251, "external_id": "HR-1001" }, "type": "single_addition_pre_tax", "title": "Sales bonus", "amount": 300.5, "date": "2026-03-15", "created_at": "2026-03-02T12:00:00+00:00" }, { "id": 47809, "user": { "id": 11251, "external_id": "HR-1001" }, "type": "single_deduction_post_tax", "title": null, "amount": 50.25, "date": "2026-03-20", "created_at": "2026-03-02T12:00:00+00:00" } ] } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Up to 100 one-off amounts — a bonus, a service charge, a penalty — each for one person\nand one day. All or nothing: a `422` means none of the batch landed.\n\n**An adjustment is read when a payslip is calculated.** A `draft` payslip whose period\nholds `date` takes it in on its next calculation. An `approved` or `paid` one does not:\nit is recalculated only by hand in the web application, and nothing here tells you\nwhether that happened. Check the payslip's status for the month before filing into it.\n\n**Send an `Idempotency-Key`.** An adjustment carries no key of yours, so a retry after a\ntimeout files it a second time unless the header says it is the same attempt. The\nanswer lists what was created, in the order sent.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "adjustments": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "object", "properties": { "user_id": { "type": "integer", "description": "The employee this belongs to, by the id this API issued." }, "type": { "type": "string", "enum": [ "service_charge", "single_addition_pre_tax", "single_addition_post_tax", "single_loan", "single_deduction_pre_tax", "single_deduction_post_tax" ], "x-clockster-set": "PayrollSingleAdjustmentsType", "description": "What the amount does: an addition or a deduction, before or after tax, a service charge or a one-off loan." }, "amount": { "type": "number", "minimum": 0, "maximum": 999999999, "description": "How much, never negative — `type` says which way it goes. At most two decimal places." }, "date": { "type": "string", "format": "date", "description": "The day it is dated, `YYYY-MM-DD`. The payslip whose period holds this day takes it in." }, "title": { "type": [ "string", "null" ], "maxLength": 250, "description": "The name this is shown under." } }, "required": [ "user_id", "type", "amount", "date" ] }, "description": "The adjustments to file, up to 100 a call." } }, "required": [ "adjustments" ] } } } }, "security": [ { "companyToken": [ "payroll:write" ] } ] } }, "/company/v3/payroll/single-adjustments/{id}": { "delete": { "operationId": "company.v3.payroll.single-adjustments.destroy", "tags": [ "Payroll" ], "summary": "Delete a single adjustment", "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/DeleteOutcome" } }, "required": [ "data" ] }, "example": { "data": { "id": 47809, "result": "deleted" } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Deletes the adjustment. A payslip already calculated with it keeps the amount until it\nis calculated again — for an `approved` or `paid` one, only by hand in the web\napplication.\n\nAnother company's id is a `404`.", "security": [ { "companyToken": [ "payroll:write" ] } ] } }, "/company/v3/positions": { "get": { "operationId": "company.v3.positions.index", "tags": [ "Positions" ], "summary": "List positions", "parameters": [ { "name": "per_page", "in": "query", "required": false, "description": "How many rows one page holds. Defaults to 50.", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } }, { "name": "cursor", "in": "query", "required": false, "description": "The `meta.next_cursor` of the previous page. Omit it for the first. A cursor is bound to the filters it was issued under — change them and start again.", "schema": { "type": [ "string", "null" ], "maxLength": 512 } }, { "name": "search", "in": "query", "required": false, "description": "Free text over the names the section lists.", "schema": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 200 } }, { "name": "updated_since", "in": "query", "required": false, "description": "Only rows changed at or after this instant (ISO 8601). The cheap way to sync: ask for what moved, not for everything.", "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "include", "in": "query", "required": false, "description": "Relations to load, comma-separated. Anything not named is absent from the answer rather than null.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "title": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" } }, "required": [ "id", "external_id", "title", "description", "created_at", "updated_at" ] } }, "links": { "$ref": "#/components/schemas/PageLinks" }, "meta": { "$ref": "#/components/schemas/PageMeta" } }, "required": [ "data", "links", "meta" ] }, "example": { "data": [ { "id": 48898, "external_id": null, "title": "CEO", "description": null, "created_at": "2026-03-02T12:00:00+00:00", "updated_at": "2026-03-02T12:00:00+00:00" }, { "id": 27147, "external_id": null, "title": "Staff", "description": null, "created_at": "2026-03-02T12:00:00+00:00", "updated_at": "2026-03-02T12:00:00+00:00" }, { "id": 32684, "external_id": null, "title": "Baker", "description": null, "created_at": "2026-03-02T12:00:00+00:00", "updated_at": "2026-03-02T12:00:00+00:00" } ], "links": { "first": null, "last": null, "prev": null, "next": null }, "meta": { "path": "https://api.clockster.com/company/v3/positions", "per_page": 50, "next_cursor": null, "prev_cursor": null } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "A position carries no managers, so asking to include them is refused rather than answered with an empty list.", "security": [ { "companyToken": [ "structure:read" ] } ] } }, "/company/v3/positions/upsert": { "post": { "operationId": "company.v3.positions.upsert", "tags": [ "Positions" ], "summary": "Create or update positions", "parameters": [], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/UpsertOutcome" } } }, "required": [ "data" ] }, "example": { "data": [ { "external_id": "HR-DEP-1", "id": 41132, "result": "updated" }, { "external_id": "HR-DEP-2", "id": 58294, "result": "created" } ] } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Up to 100 entries in one call, matched on `external_id`.\n\n**Not matched on the name.** Renaming an entry on your side updates ours, where matching\non `title` would have created a second and orphaned the first.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "items": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "object", "properties": { "external_id": { "type": "string", "minLength": 1, "maxLength": 64, "description": "Your own key for this row. Send it on every write and the next one updates rather than duplicates." }, "title": { "type": "string", "maxLength": 200, "description": "The name this is shown under." }, "description": { "type": [ "string", "null" ], "maxLength": 500, "description": "Free text about this row, for people rather than for your code." } }, "required": [ "external_id", "title" ] }, "description": "The rows to write. Each carries your own `external_id`, and a row already stored under that key is updated rather than added." } }, "required": [ "items" ] } } } }, "security": [ { "companyToken": [ "structure:write" ] } ] } }, "/company/v3/positions/{id}": { "get": { "operationId": "company.v3.positions.show", "tags": [ "Positions" ], "summary": "Read one position", "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } }, { "name": "include", "in": "query", "required": false, "description": "Relations to load, comma-separated. Anything not named is absent from the answer rather than null.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "title": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" } }, "required": [ "id", "external_id", "title", "description", "created_at", "updated_at" ] } }, "required": [ "data" ] }, "example": { "data": { "id": 32684, "external_id": null, "title": "Baker", "description": null, "created_at": "2026-03-02T12:00:00+00:00", "updated_at": "2026-03-02T12:00:00+00:00" } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "The same keys the listing answers with. A position carries no managers, so asking to include them is refused rather than answered with an empty list.", "security": [ { "companyToken": [ "structure:read" ] } ] }, "delete": { "operationId": "company.v3.positions.destroy", "tags": [ "Positions" ], "summary": "Delete a position", "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/DeleteOutcome" } }, "required": [ "data" ] }, "example": { "data": { "id": 80256, "result": "deleted" } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "409": { "description": "Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "location_in_use", "message": "People still work at this location. Move them first, then delete it.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "**Refused while anybody holds it** — 409, `position_in_use`.\n\nBeyond the people: deleting a position destroys its auto-scheduler staffing\nconfiguration outright, so a rota that says \"two bakers on nights\" stops saying it.\n\nMove the people first, then delete.", "security": [ { "companyToken": [ "structure:write" ] } ] } }, "/company/v3/schedules": { "post": { "operationId": "company.v3.schedules.store", "tags": [ "Schedules" ], "summary": "Create schedules", "parameters": [ { "name": "Idempotency-Key", "in": "header", "required": false, "description": "A value you choose, unique per attempt. Send the same one when retrying and the first answer is replayed instead of the write happening twice. Optional, and only on the writes that carry no key of your own.", "schema": { "type": "string", "maxLength": 255 } } ], "responses": { "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "type": { "type": "string" }, "leave_type": { "type": [ "null", "string" ] }, "is_split": { "type": "boolean" }, "dates": { "type": "array", "items": { "type": "string" } }, "timezone": { "type": [ "null", "string" ] }, "start": { "type": [ "null", "string" ] }, "end": { "type": [ "null", "string" ] }, "time_planned": { "type": "integer" }, "break_time": { "type": "integer" }, "grace_start": { "type": "integer" }, "grace_end": { "type": "integer" }, "location_id": { "type": [ "integer", "null" ] }, "department_id": { "type": [ "integer", "null" ] }, "position_id": { "type": [ "integer", "null" ] }, "shifts": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "start": { "type": "string" }, "end": { "type": "string" }, "time_planned": { "type": "integer" }, "location_id": { "type": [ "integer", "null" ] }, "department_id": { "type": [ "integer", "null" ] }, "position_id": { "type": [ "integer", "null" ] } }, "required": [ "id", "start", "end", "time_planned", "location_id", "department_id", "position_id" ] } }, "users": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] } }, "required": [ "id", "external_id" ] } } }, "required": [ "id", "type", "leave_type", "is_split", "dates", "timezone", "start", "end", "time_planned", "break_time", "grace_start", "grace_end", "location_id", "department_id", "position_id", "shifts", "users" ] } } }, "required": [ "data" ] }, "example": { "data": [ { "id": 44197, "type": "work", "leave_type": null, "is_split": true, "dates": [ "2026-04-06", "2026-04-07" ], "timezone": "+05:00", "start": "08:00:00", "end": "20:00:00", "time_planned": 28800, "break_time": 0, "grace_start": 900, "grace_end": 300, "location_id": 83977, "department_id": 18620, "position_id": 32684, "shifts": [ { "id": 47566, "start": "08:00:00", "end": "12:00:00", "time_planned": 14400, "location_id": 83977, "department_id": 18620, "position_id": 32684 }, { "id": 47566, "start": "16:00:00", "end": "20:00:00", "time_planned": 14400, "location_id": null, "department_id": null, "position_id": null } ], "users": [ { "id": 70753, "external_id": "HR-1001" } ] }, { "id": 44197, "type": "leave", "leave_type": "annual", "is_split": false, "dates": [ "2026-04-08" ], "timezone": null, "start": null, "end": null, "time_planned": 0, "break_time": 0, "grace_start": 0, "grace_end": 0, "location_id": null, "department_id": null, "position_id": null, "shifts": [], "users": [ { "id": 70753, "external_id": "HR-1001" } ] } ] } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Up to 25 schedules in one call, each a kind of day, the days it falls on, and the people\nit is for. `type` is per item, so a rota and the absences inside it go together.\n\nThere are no repeat patterns: send the dates. If you want every Monday, say which\nMondays.\n\n**Send the rota as one call, not as twenty.** Everyone named is notified, once per call —\nso the same twenty schedules sent one at a time buzz in somebody's pocket twenty times.\n\n**This is the write to send an `Idempotency-Key` with.** A schedule carries no key of\nyours, so a retry after a timeout files the rota a second time unless the header tells us\nit is the same attempt.\n\n**Schedules carry no key you own**, so a resend duplicates rather than converges. A\n`422` means none of the batch landed and is safe to fix and send again; a timeout is\nnot — read back before retrying. The answer lists what was created, in the order sent.\n\n**`type` decides what else is required.** `work` needs `timezone` and either\n`start`/`end` or `shifts`. `free` — a day with hours to make up rather than hours to\nkeep — needs `timezone`, `start`, `end`, and takes `time_planned`. `leave` needs only\n`leave_type`.\n\n`start` and `end` are clock times, `HH:MM:SS`, read in `timezone` — not instants, whatever\na generated client calls the field. `timezone` is a fixed offset, `+05:00` or `Z`.\n\n**The answer is not an echo of the request, so read it.** A day with two or more `shifts`\ntakes its `start`, `end` and `time_planned` from them and comes back with `is_split`\ntrue. A day with exactly one shift is not a split day: the hours move onto the day itself\nand `shifts` comes back empty. `time_planned` for a worked day is always computed —\nthe hours less the break — never taken from what you sent.\n\n**Every span is seconds**, `break_time` and `grace_start`/`grace_end` included. Grace is\nstored to the minute, so send a multiple of 60; the maximum is 3600.\n\n**Grace is not the same as a boundary.** It is how far past the start a person may arrive\nand still be credited from the shift boundary. How far outside the shift a punch is\ncollected at all is a company setting and is not on this endpoint.\n\nThere is no `title` — one is generated and it means nothing to you. One schedule takes up\nto 366 dates, 200 people and 8 shifts.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "schedules": { "type": "array", "minItems": 1, "items": { "oneOf": [ { "$ref": "#/components/schemas/WorkSchedule" }, { "$ref": "#/components/schemas/FreeSchedule" }, { "$ref": "#/components/schemas/LeaveSchedule" } ], "discriminator": { "propertyName": "type", "mapping": { "work": "#/components/schemas/WorkSchedule", "free": "#/components/schemas/FreeSchedule", "leave": "#/components/schemas/LeaveSchedule" } } }, "maxItems": 25, "description": "The days to write, up to 25 a call. Each is one of three shapes, and `type` says which." } }, "required": [ "schedules" ] } } } }, "security": [ { "companyToken": [ "schedules:write" ] } ] } }, "/company/v3/schedules/{id}": { "get": { "operationId": "company.v3.schedules.show", "tags": [ "Schedules" ], "summary": "Read one schedule", "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "integer" }, "type": { "type": "string" }, "leave_type": { "type": [ "null", "string" ] }, "is_split": { "type": "boolean" }, "dates": { "type": "array", "items": { "type": "string" } }, "timezone": { "type": "string" }, "start": { "type": "string" }, "end": { "type": "string" }, "time_planned": { "type": "integer" }, "break_time": { "type": "integer" }, "grace_start": { "type": "integer" }, "grace_end": { "type": "integer" }, "location_id": { "type": [ "integer", "null" ] }, "department_id": { "type": [ "integer", "null" ] }, "position_id": { "type": [ "integer", "null" ] }, "shifts": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "start": { "type": "string" }, "end": { "type": "string" }, "time_planned": { "type": "integer" }, "location_id": { "type": [ "integer", "null" ] }, "department_id": { "type": [ "integer", "null" ] }, "position_id": { "type": [ "integer", "null" ] } }, "required": [ "id", "start", "end", "time_planned", "location_id", "department_id", "position_id" ] } }, "users": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] } }, "required": [ "id", "external_id" ] } } }, "required": [ "id", "type", "leave_type", "is_split", "dates", "timezone", "start", "end", "time_planned", "break_time", "grace_start", "grace_end", "location_id", "department_id", "position_id", "shifts", "users" ] } }, "required": [ "data" ] }, "example": { "data": { "id": 44197, "type": "work", "leave_type": null, "is_split": false, "dates": [ "2026-04-06" ], "timezone": "+05:00", "start": "09:00:00", "end": "18:00:00", "time_planned": 28800, "break_time": 3600, "grace_start": 900, "grace_end": 300, "location_id": null, "department_id": null, "position_id": null, "shifts": [], "users": [ { "id": 11251, "external_id": null } ] } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Exactly what creating it answered with — the same keys, the shifts and the people\nincluded, since those are the schedule rather than an optional extra.\n\nWorth reading back after a create: a day with two or more shifts takes its `start`,\n`end` and `time_planned` from them, and a day with exactly one has the shift folded into\nit and comes back with `shifts` empty.\n\nThere is no listing of schedules. `GET /company/v3/timesheets` answers what a person is\nscheduled for on a day, which is the question a rota is usually asked.", "security": [ { "companyToken": [ "schedules:read" ] } ] }, "delete": { "operationId": "company.v3.schedules.destroy", "tags": [ "Schedules" ], "summary": "Delete a schedule", "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/DeleteOutcome" } }, "required": [ "data" ] }, "example": { "data": { "id": 80256, "result": "deleted" } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "409": { "description": "Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "location_in_use", "message": "People still work at this location. Move them first, then delete it.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Everyone who was on it is notified, the same way a change to it would notify them.\n\n**A default schedule is refused** — 409, `schedule_is_default`. It is what the company\nfalls back to.\n\n**An open shift is refused** — 409, `schedule_is_open`. Deleting one also removes its\nsiblings and recomputes their days, which this API can neither create nor show you. Use\nthe web application.", "security": [ { "companyToken": [ "schedules:write" ] } ] } }, "/company/v3/tasks": { "get": { "operationId": "company.v3.tasks.index", "tags": [ "Tasks" ], "summary": "List tasks", "parameters": [ { "name": "per_page", "in": "query", "required": false, "description": "How many rows one page holds. Defaults to 50.", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } }, { "name": "cursor", "in": "query", "required": false, "description": "The `meta.next_cursor` of the previous page. Omit it for the first. A cursor is bound to the filters it was issued under — change them and start again.", "schema": { "type": [ "string", "null" ], "maxLength": 512 } }, { "name": "external_ids", "in": "query", "required": false, "description": "Only rows carrying these keys of yours. The other half of an upsert: write with your key, read back with it.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "string", "maxLength": 64 } } }, { "name": "users", "in": "query", "required": false, "description": "Only rows belonging to these people, by id.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "categories", "in": "query", "required": false, "description": "Only rows in these categories.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "statuses", "in": "query", "required": false, "description": "Only rows in these states.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 6, "items": { "type": "string", "enum": [ "created", "started", "paused", "completed", "incompleted", "pastdue" ], "x-clockster-set": "TasksStatus" } } }, { "name": "active", "in": "query", "required": false, "description": "Only rows switched on (`true`) or off (`false`). Omit for both.", "schema": { "type": "boolean" } }, { "name": "search", "in": "query", "required": false, "description": "Free text over the names the section lists.", "schema": { "type": [ "string", "null" ], "maxLength": 50 } }, { "name": "due_from", "in": "query", "required": false, "description": "Due at or after this date (YYYY-MM-DD).", "schema": { "type": [ "string", "null" ], "format": "date" } }, { "name": "due_to", "in": "query", "required": false, "description": "Due at or before this date (YYYY-MM-DD).", "schema": { "type": [ "string", "null" ], "format": "date" } }, { "name": "updated_since", "in": "query", "required": false, "description": "Only rows changed at or after this instant (ISO 8601). The cheap way to sync: ask for what moved, not for everything.", "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "include", "in": "query", "required": false, "description": "Relations to load, comma-separated. Anything not named is absent from the answer rather than null.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "enum": [ "items", "managers", "user", "author" ], "x-clockster-set": "TasksInclude" } } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "title": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "status": { "type": "string" }, "active": { "type": "boolean" }, "priority": { "type": "integer" }, "user_id": { "type": "integer" }, "author_id": { "type": "integer" }, "category_id": { "type": [ "integer", "null" ] }, "location_id": { "type": [ "integer", "null" ] }, "department_id": { "type": [ "integer", "null" ] }, "position_id": { "type": [ "integer", "null" ] }, "due_date": { "type": "string" }, "time_start": { "type": "string" }, "time_end": { "type": "string" }, "timezone": { "type": "string" }, "kpi_plan": { "type": "integer" }, "kpi_fact": { "type": "integer" }, "time_worked": { "type": "integer" }, "started_at": { "type": [ "null", "string" ] }, "finished_at": { "type": [ "null", "string" ] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" }, "items": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "title": { "type": "string" }, "order": { "type": "integer" }, "is_completed": { "type": "boolean" } }, "required": [ "id", "title", "order", "is_completed" ] } }, "managers": { "type": "array", "items": { "$ref": "#/components/schemas/EmployeeShort" } }, "user": { "$ref": "#/components/schemas/EmployeeShort" }, "author": { "$ref": "#/components/schemas/EmployeeShort" } }, "required": [ "id", "external_id", "title", "description", "status", "active", "priority", "user_id", "author_id", "category_id", "location_id", "department_id", "position_id", "due_date", "time_start", "time_end", "timezone", "kpi_plan", "kpi_fact", "time_worked", "started_at", "finished_at", "created_at", "updated_at" ] } }, "meta": { "type": "object", "properties": { "per_page": { "type": "integer" }, "next_cursor": { "type": [ "null", "string" ] }, "prev_cursor": { "type": [ "null", "string" ] } }, "required": [ "per_page", "next_cursor", "prev_cursor" ] } }, "required": [ "data", "meta" ] }, "example": { "data": [ { "id": 85398, "external_id": "WO-4471", "title": "Replace the filter", "description": "Unit 3, second floor", "status": "created", "active": true, "priority": 1, "user_id": 70753, "author_id": 70753, "category_id": null, "location_id": 83977, "department_id": 18620, "position_id": null, "due_date": "2026-04-06", "time_start": "09:00:00", "time_end": "18:00:00", "timezone": "+05:00", "kpi_plan": 10, "kpi_fact": 0, "time_worked": 0, "started_at": null, "finished_at": null, "created_at": "2026-03-02T12:00:00+00:00", "updated_at": "2026-03-02T12:00:00+00:00" } ], "meta": { "per_page": 50, "next_cursor": null, "prev_cursor": null } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Work as we hold it: what was issued, what became of it, and how it measured up.\n\n`kpi_fact` against `kpi_plan`, plus `time_worked`, are the point of reading a task back\n— what was asked for, what was achieved, how long it took. `status` says where it got\nto.\n\n**Twenty-four fields, not the forty the table has.** Eight of the rest configure how the\nmobile application behaves while the job is done — whether it demands a photo, records a\nlocation, keeps the steps in order. That is a task template's business, and neither\nuseful nor settable here.\n\n`include=items` adds the steps, `include=managers` the people who approve or are\nnotified. Without them the keys are absent, never null standing in for \"not asked for\".\n\n**`updated_since` is what an export should page on**, as an instant: a task moves\nthrough its statuses inside a working day, so a caller polling for completions needs to\nsay which minute.\n\n`statuses` takes `created`, `started`, `paused`, `completed`, `incompleted` and\n`pastdue`. Three more exist in the database and none is offered: `finished` and\n`unfinished` are deprecated spellings, and `pending` is reached only through approval.\nA value that cannot be explained is worse than one that is absent.\n\n**Oldest first.** A first call lands on the earliest task this company ever issued,\nwhich for a long-standing one is years back. That order is what lets a full export\nfinish in one walk, and it is not what you want for \"what happened lately\": ask with\n`updated_since`, or narrow with `due_from` and `due_to`.", "security": [ { "companyToken": [ "tasks:read" ] } ] } }, "/company/v3/tasks/upsert": { "post": { "operationId": "company.v3.tasks.upsert", "tags": [ "Tasks" ], "summary": "Issue tasks", "parameters": [], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/UpsertOutcome" } } }, "required": [ "data" ] }, "example": { "data": [ { "external_id": "WO-4471", "id": 85398, "result": "created" } ] } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Up to 100 pieces of work in one call, matched on `external_id`, which is required here\n— a task has no natural key of its own, since the same round is issued every week under\nthe same title.\n\n**`status` is not accepted, and neither are the timestamps around it.** The product\nmoves a task through its lifecycle with events — completing notifies, approval routes,\nreopening makes it pastdue again — and a status written straight onto the row fires none\nof that. Issue the work here; read where it got to with `GET /company/v3/tasks`.\n\n**Eight fields are not accepted either** — `req_photo`, `req_sequence`, `gallery`,\n`get_location`, `get_timing`, `is_keep_status`, `req_approve`, `req_notify`. They\nconfigure the mobile application, not the job.\n\n**Where the work sits is taken from whoever it is for.** Omit `location_id`,\n`department_id` and `position_id` and they come from the assignee — your system knows\nthe person, not our org chart. Send them to override.\n\n**`items` is an exception to the omitted-field rule**: sending it replaces the steps outright, because a step carries no key\nto match an incoming one against — and replacing them discards the completion the person\ndoing the work recorded. Omit the key to leave them alone. `managers` likewise states\nwho approves now rather than adding to them.\n\n`kpi_plan` has no \"unset\" — the column is NOT NULL with a default of 0, so an omitted\nplan is a plan of zero.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "tasks": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "object", "properties": { "external_id": { "type": "string", "minLength": 1, "maxLength": 64, "description": "Your own key for this row. Send it on every write and the next one updates rather than duplicates." }, "title": { "type": "string", "maxLength": 255, "description": "The name this is shown under." }, "description": { "type": [ "string", "null" ], "maxLength": 1000, "description": "Free text about this row, for people rather than for your code." }, "user_id": { "type": "integer", "description": "The employee this belongs to, by the id this API issued." }, "category_id": { "type": [ "integer", "null" ], "description": "The category it belongs to, by id." }, "location_id": { "type": [ "integer", "null" ], "description": "The location this is filed against, by id. Null clears it." }, "department_id": { "type": [ "integer", "null" ], "description": "The department this is filed against, by id. Null clears it." }, "position_id": { "type": [ "integer", "null" ], "description": "The position this is filed against, by id. Null clears it." }, "due_date": { "type": [ "string", "null" ], "format": "date", "description": "The day it is due, `YYYY-MM-DD`." }, "time_start": { "type": [ "string", "null" ], "format": "date-time", "description": "When in the day it starts, as a clock time `HH:MM:SS`." }, "time_end": { "type": [ "string", "null" ], "format": "date-time", "description": "When in the day it ends, as a clock time `HH:MM:SS`." }, "timezone": { "type": [ "string", "null" ], "pattern": "^(?:Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])$", "description": "The UTC offset the clock times beside it are read in — `Z`, or `+05:00`. An offset rather than a zone name, so the day is fixed to a moment rather than to a rule that may be changed later." }, "priority": { "type": "integer", "enum": [ 0, 1 ], "x-clockster-set": "TasksPriority", "description": "Whether the task is flagged as a priority: `1` if it is, `0` if not." }, "active": { "type": "boolean", "description": "Whether the task is active." }, "kpi_plan": { "type": [ "number", "null" ], "minimum": 0, "description": "The planned figure this task is measured against." }, "managers": { "type": [ "array", "null" ], "maxItems": 10, "items": { "type": "integer" }, "description": "Who may decide on this task, by id. Up to ten." }, "items": { "type": [ "array", "null" ], "maxItems": 200, "items": { "type": "object", "properties": { "title": { "type": "string", "maxLength": 500, "description": "The name this is shown under." }, "order": { "type": [ "integer", "null" ], "minimum": 0, "description": "Where this item sits in the checklist, counting from zero." } }, "required": [ "title" ] }, "description": "The checklist inside this task, in the order given." } }, "required": [ "external_id", "title", "user_id" ] }, "description": "The tasks to write, up to 100 a call." } }, "required": [ "tasks" ] } } } }, "security": [ { "companyToken": [ "tasks:write" ] } ] } }, "/company/v3/tasks/{id}": { "get": { "operationId": "company.v3.tasks.show", "tags": [ "Tasks" ], "summary": "Read one task", "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } }, { "name": "include", "in": "query", "required": false, "description": "Relations to load, comma-separated. Anything not named is absent from the answer rather than null.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "enum": [ "items", "managers", "user", "author" ], "x-clockster-set": "TasksInclude" } } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "title": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "status": { "type": "string" }, "active": { "type": "boolean" }, "priority": { "type": "integer" }, "user_id": { "type": "integer" }, "author_id": { "type": "integer" }, "category_id": { "type": [ "integer", "null" ] }, "location_id": { "type": [ "integer", "null" ] }, "department_id": { "type": [ "integer", "null" ] }, "position_id": { "type": [ "integer", "null" ] }, "due_date": { "type": "string" }, "time_start": { "type": "string" }, "time_end": { "type": "string" }, "timezone": { "type": "string" }, "kpi_plan": { "type": "integer" }, "kpi_fact": { "type": "integer" }, "time_worked": { "type": "integer" }, "started_at": { "type": [ "null", "string" ] }, "finished_at": { "type": [ "null", "string" ] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" }, "items": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "title": { "type": "string" }, "order": { "type": "integer" }, "is_completed": { "type": "boolean" } }, "required": [ "id", "title", "order", "is_completed" ] } }, "managers": { "type": "array", "items": { "$ref": "#/components/schemas/EmployeeShort" } }, "user": { "$ref": "#/components/schemas/EmployeeShort" }, "author": { "$ref": "#/components/schemas/EmployeeShort" } }, "required": [ "id", "external_id", "title", "description", "status", "active", "priority", "user_id", "author_id", "category_id", "location_id", "department_id", "position_id", "due_date", "time_start", "time_end", "timezone", "kpi_plan", "kpi_fact", "time_worked", "started_at", "finished_at", "created_at", "updated_at" ] } }, "required": [ "data" ] }, "example": { "data": { "id": 85398, "external_id": "WO-4471", "title": "Replace the filter", "description": "Unit 3, second floor", "status": "created", "active": true, "priority": 1, "user_id": 70753, "author_id": 70753, "category_id": null, "location_id": 83977, "department_id": 18620, "position_id": null, "due_date": "2026-04-06", "time_start": "09:00:00", "time_end": "18:00:00", "timezone": "+05:00", "kpi_plan": 10, "kpi_fact": 0, "time_worked": 0, "started_at": null, "finished_at": null, "created_at": "2026-03-02T12:00:00+00:00", "updated_at": "2026-03-02T12:00:00+00:00" } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "The same keys the listing answers with, and the same `include` vocabulary. Another company's id is a `404`.", "security": [ { "companyToken": [ "tasks:read" ] } ] } }, "/company/v3/timesheets": { "get": { "operationId": "company.v3.timesheets.index", "tags": [ "Timesheets" ], "summary": "Timesheets", "parameters": [ { "name": "date_from", "in": "query", "required": true, "description": "Start of the window, inclusive (YYYY-MM-DD).", "schema": { "type": "string", "format": "date" } }, { "name": "date_to", "in": "query", "required": true, "description": "End of the window, inclusive (YYYY-MM-DD).", "schema": { "type": "string", "format": "date" } }, { "name": "cursor", "in": "query", "required": false, "description": "The `meta.next_cursor` of the previous page. Omit it for the first. A cursor is bound to the filters it was issued under — change them and start again.", "schema": { "type": [ "string", "null" ], "maxLength": 512 } }, { "name": "users", "in": "query", "required": false, "description": "Only rows belonging to these people, by id.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "locations", "in": "query", "required": false, "description": "Only these locations, by id.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "departments", "in": "query", "required": false, "description": "Only these departments, by id.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "positions", "in": "query", "required": false, "description": "Only these positions, by id.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "employment", "in": "query", "required": false, "description": "Only people on these employment terms.", "schema": { "type": [ "string", "null" ], "maxLength": 50 } }, { "name": "include", "in": "query", "required": false, "description": "Relations to load, comma-separated. Anything not named is absent from the answer rather than null.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "enum": [ "actual", "variance", "user", "location", "department", "position" ], "x-clockster-set": "TimesheetsInclude" } } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "date": { "type": "string" }, "user": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "code": { "type": [ "null", "string" ] }, "first_name": { "type": "string" }, "middle_name": { "type": [ "null", "string" ] }, "last_name": { "type": "string" } }, "required": [ "id", "external_id" ] }, "planned": { "type": [ "null", "object" ], "properties": { "schedule_id": { "type": "integer" }, "type": { "type": "string" }, "leave_type": { "type": [ "null", "string" ] }, "is_split": { "type": "boolean" }, "timezone": { "type": "string" }, "start": { "type": "string" }, "end": { "type": "string" }, "time_planned": { "type": "integer" }, "break_time": { "type": "integer" }, "grace_start": { "type": "integer" }, "grace_end": { "type": "integer" }, "shifts": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "code": { "type": [ "null", "string" ] }, "start": { "type": "string" }, "end": { "type": "string" }, "time_planned": { "type": "integer" }, "location_id": { "type": [ "integer", "null" ] }, "department_id": { "type": [ "integer", "null" ] }, "position_id": { "type": [ "integer", "null" ] }, "location": { "type": [ "null", "object" ], "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "title": { "type": "string" } }, "required": [ "id", "external_id", "title" ] }, "department": { "type": [ "null", "object" ], "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "title": { "type": "string" } }, "required": [ "id", "external_id", "title" ] }, "position": { "type": [ "null", "object" ], "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "title": { "type": "string" } }, "required": [ "id", "external_id", "title" ] } }, "required": [ "id", "code", "start", "end", "time_planned", "location_id", "department_id", "position_id" ] } }, "location_id": { "type": [ "integer", "null" ] }, "department_id": { "type": [ "integer", "null" ] }, "position_id": { "type": [ "integer", "null" ] }, "location": { "type": [ "null", "object" ], "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "title": { "type": "string" } }, "required": [ "id", "external_id", "title" ] }, "department": { "type": [ "null", "object" ], "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "title": { "type": "string" } }, "required": [ "id", "external_id", "title" ] }, "position": { "type": [ "null", "object" ], "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "title": { "type": "string" } }, "required": [ "id", "external_id", "title" ] } }, "required": [ "schedule_id", "type", "leave_type", "is_split", "timezone", "start", "end", "time_planned", "break_time", "grace_start", "grace_end", "shifts", "location_id", "department_id", "position_id" ] }, "actual": { "type": "object", "properties": { "in": { "type": [ "null", "string" ] }, "out": { "type": [ "null", "string" ] }, "time_worked": { "type": "integer" }, "time_break": { "type": "integer" }, "time_worked_day_off": { "type": "integer" }, "time_night": { "type": "integer" }, "shifts": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "in": { "type": "string" }, "out": { "type": "string" }, "time_worked": { "type": "integer" } }, "required": [ "id", "in", "out", "time_worked" ] } } }, "required": [ "in", "out", "time_worked", "time_break", "time_worked_day_off", "time_night", "shifts" ] }, "variance": { "type": "object", "properties": { "time_late": { "type": "integer" }, "time_early_left": { "type": "integer" }, "time_overworked": { "type": "integer" }, "time_underworked": { "type": "integer" } }, "required": [ "time_late", "time_early_left", "time_overworked", "time_underworked" ] } }, "required": [ "date", "user", "planned" ] } }, "meta": { "type": "object", "properties": { "users_per_page": { "type": "integer" }, "next_cursor": { "type": [ "null", "string" ] }, "prev_cursor": { "type": [ "null", "string" ] } }, "required": [ "users_per_page", "next_cursor", "prev_cursor" ] } }, "required": [ "data", "meta" ] }, "example": { "data": [ { "date": "2026-04-06", "user": { "id": 70753, "external_id": "HR-1001" }, "planned": { "schedule_id": 30457, "type": "work", "leave_type": null, "is_split": false, "timezone": "+05:00", "start": "09:00:00", "end": "18:00:00", "time_planned": 28800, "break_time": 3600, "grace_start": 0, "grace_end": 0, "shifts": [], "location_id": 83977, "department_id": 35628, "position_id": 54888 } }, { "date": "2026-04-07", "user": { "id": 70753, "external_id": "HR-1001" }, "planned": { "schedule_id": 92132, "type": "work", "leave_type": null, "is_split": false, "timezone": "+00:00", "start": "09:00:00", "end": "18:00:00", "time_planned": 28800, "break_time": 3600, "grace_start": 0, "grace_end": 0, "shifts": [], "location_id": null, "department_id": null, "position_id": null } }, { "date": "2026-04-08", "user": { "id": 70753, "external_id": "HR-1001" }, "planned": { "schedule_id": 40941, "type": "work", "leave_type": null, "is_split": true, "timezone": "+05:00", "start": "08:00:00", "end": "20:00:00", "time_planned": 28800, "break_time": 0, "grace_start": 0, "grace_end": 0, "shifts": [ { "id": 47566, "code": null, "start": "08:00:00", "end": "12:00:00", "time_planned": 14400, "location_id": 83977, "department_id": 35628, "position_id": 54888 }, { "id": 47566, "code": null, "start": "16:00:00", "end": "20:00:00", "time_planned": 14400, "location_id": null, "department_id": null, "position_id": null } ], "location_id": 83977, "department_id": 35628, "position_id": 54888 } } ], "meta": { "users_per_page": 100, "next_cursor": null, "prev_cursor": null } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "One row per person per calendar day of the window: what was planned, and — when asked\nfor — what happened and how the two differ.\n\n**`planned` alone is the timesheet grid** — who was meant to work, when, and what kind\nof day it was. `include=actual` adds what was recorded, `include=variance` adds the\ndifference. Either one is what makes the request expensive, because both require\nmatching the clock-ins.\n\n**The four variance numbers are not additive.** Arriving three minutes late produces\n`time_late` 180 and `time_underworked` 180 — the same minutes, counted once as lateness\nand once as unfilled plan. Summing them double-counts.\n\n**`planned: null` means no schedule at all for that day.** It is rarer than it sounds: a\ncompany created with default settings carries a work and a leave schedule covering four\nyears, so ordinary days off arrive as `type: leave` rather than as an absent plan. A day\nthat was scheduled and not worked is the other case — a plan, an empty `actual`, and\n`time_underworked` equal to the whole planned time.\n\n**There is no `per_page`.** How many rows fifty people produce depends on the window and\non who is scheduled, which the caller cannot predict and we can: the page is sized to a\nrow budget instead, and `meta.users_per_page` reports what that came to. Paging walks\npeople, so one person's whole period always arrives on a single page and a monthly total\nnever has to be assembled across two.\n\nHolidays are not marked: take them from your own calendar. Drafts are never returned.", "security": [ { "companyToken": [ "attendance:read" ] } ] } }, "/company/v3/user-filters": { "get": { "operationId": "company.v3.user-filters.index", "tags": [ "User filters" ], "summary": "List user filters", "parameters": [ { "name": "per_page", "in": "query", "required": false, "description": "How many rows one page holds. Defaults to 50.", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } }, { "name": "cursor", "in": "query", "required": false, "description": "The `meta.next_cursor` of the previous page. Omit it for the first. A cursor is bound to the filters it was issued under — change them and start again.", "schema": { "type": [ "string", "null" ], "maxLength": 512 } }, { "name": "search", "in": "query", "required": false, "description": "Free text over the names the section lists.", "schema": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 200 } }, { "name": "updated_since", "in": "query", "required": false, "description": "Only rows changed at or after this instant (ISO 8601). The cheap way to sync: ask for what moved, not for everything.", "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "include", "in": "query", "required": false, "description": "Relations to load, comma-separated. Anything not named is absent from the answer rather than null.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "enum": [ "managers" ], "x-clockster-set": "UserFiltersInclude" } } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "title": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" }, "managers": { "type": "array", "items": { "$ref": "#/components/schemas/EmployeeShort" } } }, "required": [ "id", "external_id", "title", "description", "created_at", "updated_at" ] } }, "links": { "$ref": "#/components/schemas/PageLinks" }, "meta": { "$ref": "#/components/schemas/PageMeta" } }, "required": [ "data", "links", "meta" ] }, "example": { "data": [ { "id": 72430, "external_id": null, "title": "Night crew", "description": "After 22:00", "created_at": "2026-03-02T12:00:00+00:00", "updated_at": "2026-03-02T12:00:00+00:00" } ], "links": { "first": null, "last": null, "prev": null, "next": null }, "meta": { "path": "https://api.clockster.com/company/v3/user-filters", "per_page": 50, "next_cursor": null, "prev_cursor": null } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "`include=managers` adds the managers, as on departments.", "security": [ { "companyToken": [ "structure:read" ] } ] } }, "/company/v3/user-filters/upsert": { "post": { "operationId": "company.v3.user-filters.upsert", "tags": [ "User filters" ], "summary": "Create or update user filters", "parameters": [], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/UpsertOutcome" } } }, "required": [ "data" ] }, "example": { "data": [ { "external_id": "HR-DEP-1", "id": 41132, "result": "updated" }, { "external_id": "HR-DEP-2", "id": 58294, "result": "created" } ] } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Up to 100 entries in one call, matched on `external_id`.\n\n**Not matched on the name.** Renaming an entry on your side updates ours, where matching\non `title` would have created a second and orphaned the first.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "items": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "object", "properties": { "external_id": { "type": "string", "minLength": 1, "maxLength": 64, "description": "Your own key for this row. Send it on every write and the next one updates rather than duplicates." }, "title": { "type": "string", "maxLength": 200, "description": "The name this is shown under." }, "description": { "type": [ "string", "null" ], "maxLength": 500, "description": "Free text about this row, for people rather than for your code." } }, "required": [ "external_id", "title" ] }, "description": "The rows to write. Each carries your own `external_id`, and a row already stored under that key is updated rather than added." } }, "required": [ "items" ] } } } }, "security": [ { "companyToken": [ "structure:write" ] } ] } }, "/company/v3/user-filters/{id}": { "get": { "operationId": "company.v3.user-filters.show", "tags": [ "User filters" ], "summary": "Read one user filter", "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } }, { "name": "include", "in": "query", "required": false, "description": "Relations to load, comma-separated. Anything not named is absent from the answer rather than null.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "enum": [ "managers" ], "x-clockster-set": "UserFiltersInclude" } } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "title": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" }, "managers": { "type": "array", "items": { "$ref": "#/components/schemas/EmployeeShort" } } }, "required": [ "id", "external_id", "title", "description", "created_at", "updated_at" ] } }, "required": [ "data" ] }, "example": { "data": { "id": 72430, "external_id": null, "title": "Night crew", "description": "After 22:00", "created_at": "2026-03-02T12:00:00+00:00", "updated_at": "2026-03-02T12:00:00+00:00" } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "The same keys and the same `include` vocabulary as the listing. Somebody else's id is a `404`.", "security": [ { "companyToken": [ "structure:read" ] } ] }, "delete": { "operationId": "company.v3.user-filters.destroy", "tags": [ "User filters" ], "summary": "Delete a user filter", "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/DeleteOutcome" } }, "required": [ "data" ] }, "example": { "data": { "id": 80256, "result": "deleted" } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "409": { "description": "Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "location_in_use", "message": "People still work at this location. Move them first, then delete it.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Members do not block it — a filter is a label, and a caller who keeps their filters in\nstep re-creates it on the next sync.\n\n**Refused while an approval route points at it** — 409, `user_filter_in_use`. The route\nwould survive with nobody to approve through it and quietly stop routing. Change the\nroute first.", "security": [ { "companyToken": [ "structure:write" ] } ] } }, "/company/v3/user-requests": { "get": { "operationId": "company.v3.user-requests.index", "tags": [ "Requests" ], "summary": "List requests", "parameters": [ { "name": "per_page", "in": "query", "required": false, "description": "How many rows one page holds. Defaults to 50.", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } }, { "name": "cursor", "in": "query", "required": false, "description": "The `meta.next_cursor` of the previous page. Omit it for the first. A cursor is bound to the filters it was issued under — change them and start again.", "schema": { "type": [ "string", "null" ], "maxLength": 512 } }, { "name": "types", "in": "query", "required": false, "description": "Only rows of these types.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 4, "items": { "type": "string", "enum": [ "leave", "work", "general", "finance" ], "x-clockster-set": "UserRequestsType" } } }, { "name": "statuses", "in": "query", "required": false, "description": "Only rows in these states.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 7, "items": { "type": "string", "enum": [ "pending", "accepted", "rejected", "cancelled", "approval", "execution", "signing" ], "x-clockster-set": "UserRequestsStatus" } } }, { "name": "subtypes", "in": "query", "required": false, "description": "Only rows of these subtypes, which narrow a type further.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "string", "maxLength": 64 } } }, { "name": "users", "in": "query", "required": false, "description": "Only rows belonging to these people, by id.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "updated_since", "in": "query", "required": false, "description": "Only rows changed at or after this instant (ISO 8601). The cheap way to sync: ask for what moved, not for everything.", "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "include", "in": "query", "required": false, "description": "Relations to load, comma-separated. Anything not named is absent from the answer rather than null.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "enum": [ "content", "user", "author" ], "x-clockster-set": "UserRequestsInclude" } } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "type": { "type": "string" }, "subtype": { "type": [ "null", "string" ] }, "status": { "type": "string" }, "user_id": { "type": "integer" }, "author_id": { "type": "integer" }, "period": { "type": "object", "properties": { "date_start": { "type": [ "null", "string" ] }, "date_end": { "type": [ "null", "string" ] } }, "required": [ "date_start", "date_end" ] }, "comment": { "type": [ "null", "string" ] }, "amount": { "type": [ "null", "number" ] }, "currency": { "type": [ "null", "string" ] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" }, "user": { "$ref": "#/components/schemas/EmployeeShort" }, "author": { "$ref": "#/components/schemas/EmployeeShort" }, "content": { "type": "object", "properties": { "type": { "type": "string" }, "clockins": { "type": "array", "items": { "type": "object", "properties": { "status": { "type": "string" }, "datetime": { "type": "string" } }, "required": [ "status", "datetime" ] } }, "amount": { "type": [ "null", "number" ] }, "currency_id": { "type": "integer" } }, "required": [ "type" ] } }, "required": [ "id", "type", "subtype", "status", "user_id", "author_id", "period", "comment", "amount", "currency", "created_at", "updated_at" ] } }, "links": { "$ref": "#/components/schemas/PageLinks" }, "meta": { "$ref": "#/components/schemas/PageMeta" } }, "required": [ "data", "links", "meta" ] }, "example": { "data": [ { "id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "type": "general", "subtype": "clockins", "status": "accepted", "user_id": 70753, "author_id": 70753, "period": { "date_start": "2026-02-24", "date_end": "2026-02-26" }, "comment": "Forgot to badge in, the terminal was down", "amount": null, "currency": null, "created_at": "2026-03-02T12:00:00+00:00", "updated_at": "2026-03-02T12:00:00+00:00" }, { "id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "type": "finance", "subtype": "advance-payment", "status": "pending", "user_id": 70753, "author_id": 70753, "period": { "date_start": null, "date_end": null }, "comment": null, "amount": 1500.5, "currency": "KZT", "created_at": "2026-03-02T12:00:00+00:00", "updated_at": "2026-03-02T12:00:00+00:00" } ], "links": { "first": null, "last": null, "prev": null, "next": null }, "meta": { "path": "http://api.clockster.local/company/v3/user-requests", "per_page": 50, "next_cursor": null, "prev_cursor": null } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "What people asked for, and what became of it — leave, schedule changes, corrections and\nmoney.\n\n**Read this to learn that a timesheet moved.** Half of everything here is a batch\nclock-in correction: somebody forgot to punch, a manager approved the fix, and the\nattendance for those days changed after the fact. Only 48 per cent of those are approved\nwithin three days of the day they correct, and a third reach back more than a week — so\na caller that pulled a timesheet last week cannot assume it still holds. Attendance\ncarries no timestamps of its own, which makes this listing paged on `updated_since` the\nonly signal that anything has moved.\n\n`period` is the field that makes that usable: the span of days a request concerns,\nwherever its kind happens to keep them. A clock-in correction keeps them inside the\npunches, a leave request as a period or a list, a request for a certificate not at all —\nboth ends are null there rather than invented.\n\n`subtype` is the second half of `type`, and the product keeps it in two different places\n— `content.type` for most kinds, `content.leave_type` for leave. It is answered as one\nfield, and `subtypes` filters on both.\n\n`comment` is what the author wrote when filing it, ordinarily the reason. Comments the\nworkflow writes itself — on acknowledgement, or when a spawned task closes — are not\nanswered here.\n\n**Oldest first.** A first call lands on the earliest request this company ever filed,\nwhich for a long-standing one is years back. That order is what lets a full export\nfinish in one walk, and it is not what you want for \"what changed lately\": ask with\n`updated_since`.\n\n`content` is behind `include=content`: its shape depends on the kind, and one schema\ndescribes one shape everywhere else on this surface.\n\n**Reading only.** Creating a request enters a workflow — approval routes resolve,\napprovers are notified, tasks are spawned — and approving one is a person's decision that\na dismissal application or a sick note gives weight to.", "security": [ { "companyToken": [ "user-requests:read" ] } ] } }, "/company/v3/user-requests/{id}": { "get": { "operationId": "company.v3.user-requests.show", "tags": [ "Requests" ], "summary": "Read one request", "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "string" }, "type": { "type": "string" }, "subtype": { "type": [ "null", "string" ] }, "status": { "type": "string" }, "user_id": { "type": "integer" }, "author_id": { "type": "integer" }, "period": { "type": "object", "properties": { "date_start": { "type": "string" }, "date_end": { "type": "string" } }, "required": [ "date_start", "date_end" ] }, "comment": { "type": [ "null", "string" ] }, "amount": { "type": [ "null", "number" ] }, "currency": { "type": [ "null", "string" ] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" }, "content": { "type": "object", "properties": { "type": { "type": "string" }, "clockins": { "type": "array", "items": { "type": "object", "properties": { "status": { "type": "string" }, "datetime": { "type": "string" } }, "required": [ "status", "datetime" ] } } }, "required": [ "type", "clockins" ] } }, "required": [ "id", "type", "subtype", "status", "user_id", "author_id", "period", "comment", "amount", "currency", "created_at", "updated_at", "content" ] } }, "required": [ "data" ] }, "example": { "data": { "id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "type": "general", "subtype": "clockins", "status": "accepted", "user_id": 70753, "author_id": 70753, "period": { "date_start": "2026-02-24", "date_end": "2026-02-26" }, "comment": "Forgot to badge in, the terminal was down", "amount": null, "currency": null, "created_at": "2026-03-02T12:00:00+00:00", "updated_at": "2026-03-02T12:00:00+00:00", "content": { "type": "clockins", "clockins": [ { "status": "1", "datetime": "2026-02-24T08:10:00+05:00" }, { "status": "0", "datetime": "2026-02-26T17:00:00+05:00" } ] } } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Answers with `content`, unlike the listing: one row is one shape to make sense of, not a hundred.", "security": [ { "companyToken": [ "user-requests:read" ] } ] } }, "/company/v3/users": { "get": { "operationId": "company.v3.users.index", "tags": [ "Employees" ], "summary": "List employees", "parameters": [ { "name": "per_page", "in": "query", "required": false, "description": "How many rows one page holds. Defaults to 50.", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } }, { "name": "cursor", "in": "query", "required": false, "description": "The `meta.next_cursor` of the previous page. Omit it for the first. A cursor is bound to the filters it was issued under — change them and start again.", "schema": { "type": [ "string", "null" ], "maxLength": 512 } }, { "name": "search", "in": "query", "required": false, "description": "Free text over the names the section lists.", "schema": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 200 } }, { "name": "updated_since", "in": "query", "required": false, "description": "Only rows changed at or after this instant (ISO 8601). The cheap way to sync: ask for what moved, not for everything.", "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "status", "in": "query", "required": false, "description": "Whether the people who left are in the answer: `active`, `dismissed`, or `all`.", "schema": { "type": "string", "enum": [ "active", "dismissed", "all" ], "default": "active", "x-clockster-set": "UsersStatus" } }, { "name": "ids", "in": "query", "required": false, "description": "Only these ids.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "codes", "in": "query", "required": false, "description": "Only these employee codes.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "string", "maxLength": 40 } } }, { "name": "external_ids", "in": "query", "required": false, "description": "Only rows carrying these keys of yours. The other half of an upsert: write with your key, read back with it.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "string", "maxLength": 64 } } }, { "name": "locations", "in": "query", "required": false, "description": "Only these locations, by id.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "departments", "in": "query", "required": false, "description": "Only these departments, by id.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "positions", "in": "query", "required": false, "description": "Only these positions, by id.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "user_filters", "in": "query", "required": false, "description": "Only these user filters, by id.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "employment", "in": "query", "required": false, "description": "Only people on these employment terms.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "string", "enum": [ "full_time", "part_time", "irregular_hours", "contract_1", "contract_2", "apprenticeship", "traineeship", "piece_rate", "probation", "outstaffing" ], "x-clockster-set": "UsersEmployment" } } }, { "name": "include", "in": "query", "required": false, "description": "Relations to load, comma-separated. Anything not named is absent from the answer rather than null.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "enum": [ "location", "locations", "department", "position", "user_filters", "dismissal", "meta" ], "x-clockster-set": "UsersInclude" } } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "code": { "type": [ "null", "string" ] }, "first_name": { "type": "string" }, "middle_name": { "type": [ "null", "string" ] }, "last_name": { "type": "string" }, "email": { "type": "string" }, "phone": { "type": [ "null", "string" ] }, "extra_phone": { "type": [ "null", "string" ] }, "role": { "type": [ "null", "string" ] }, "gender": { "type": "string" }, "national_id": { "type": "string" }, "tax_id": { "type": [ "null", "string" ] }, "insurance_id": { "type": [ "null", "string" ] }, "employment": { "type": "string" }, "locale": { "type": "string" }, "timezone": { "type": "string" }, "date_birth": { "type": "string" }, "date_hire": { "type": "string" }, "date_leave": { "type": [ "null", "string" ] }, "photo": { "type": [ "null", "string" ] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" }, "dismissed_at": { "type": [ "null", "string" ] }, "dismissal": { "type": "object", "properties": { "id": { "type": "integer" }, "title": { "type": "string" } }, "required": [ "id", "title" ] }, "location": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "code": { "type": [ "null", "string" ] }, "title": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "latitude": { "type": [ "null", "number" ] }, "longitude": { "type": [ "null", "number" ] }, "radius": { "type": "integer" }, "created_at": { "type": "string" }, "updated_at": { "type": "string" } }, "required": [ "id", "external_id", "code", "title", "description", "latitude", "longitude", "radius", "created_at", "updated_at" ] }, "locations": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "code": { "type": [ "null", "string" ] }, "title": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "latitude": { "type": [ "null", "number" ] }, "longitude": { "type": [ "null", "number" ] }, "radius": { "type": "integer" }, "created_at": { "type": "string" }, "updated_at": { "type": "string" } }, "required": [ "id", "external_id", "code", "title", "description", "latitude", "longitude", "radius", "created_at", "updated_at" ] } }, "department": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "title": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" } }, "required": [ "id", "external_id", "title", "description", "created_at", "updated_at" ] }, "position": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "title": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" } }, "required": [ "id", "external_id", "title", "description", "created_at", "updated_at" ] }, "user_filters": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "title": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" } }, "required": [ "id", "external_id", "title", "description", "created_at", "updated_at" ] } }, "meta": { "type": "object", "properties": { "birth_place": { "type": "string" }, "marital_status": { "type": "string" }, "religion": { "type": [ "null", "string" ] }, "blood_type": { "type": [ "null", "string" ] }, "children": { "type": [ "integer", "null" ] }, "contact_name": { "type": [ "null", "string" ] }, "relationship": { "type": [ "null", "string" ] }, "phone": { "type": [ "null", "string" ] }, "domicile_address": { "type": [ "null", "string" ] }, "domicile_city": { "type": [ "null", "string" ] }, "domicile_province": { "type": [ "null", "string" ] }, "domicile_district": { "type": [ "null", "string" ] }, "domicile_postal_code": { "type": [ "null", "string" ] }, "document_address": { "type": [ "null", "string" ] }, "document_city": { "type": [ "null", "string" ] }, "document_province": { "type": [ "null", "string" ] }, "document_district": { "type": [ "null", "string" ] }, "document_postal_code": { "type": [ "null", "string" ] }, "education_level": { "type": [ "null", "string" ] }, "education_institution": { "type": [ "null", "string" ] }, "education_major": { "type": [ "null", "string" ] }, "graduation_year": { "type": [ "integer", "null" ] }, "education_gpa": { "type": [ "null", "number" ] }, "full_name": { "type": [ "null", "string" ] }, "alias_name": { "type": [ "null", "string" ] }, "local_name": { "type": [ "null", "string" ] }, "nationality": { "type": [ "null", "string" ] }, "marriage_date": { "type": [ "null", "string" ] }, "retire_age": { "type": [ "integer", "null" ] }, "retire_date": { "type": [ "null", "string" ] }, "ethnic_origin": { "type": [ "null", "string" ] }, "contract_number": { "type": [ "null", "string" ] }, "health_insurance_id": { "type": [ "null", "string" ] }, "gov_savings_id": { "type": [ "null", "string" ] }, "gov_savings_acc": { "type": [ "null", "string" ] } }, "required": [ "birth_place", "marital_status", "religion", "blood_type", "children", "contact_name", "relationship", "phone", "domicile_address", "domicile_city", "domicile_province", "domicile_district", "domicile_postal_code", "document_address", "document_city", "document_province", "document_district", "document_postal_code", "education_level", "education_institution", "education_major", "graduation_year", "education_gpa", "full_name", "alias_name", "local_name", "nationality", "marriage_date", "retire_age", "retire_date", "ethnic_origin", "contract_number", "health_insurance_id", "gov_savings_id", "gov_savings_acc" ] } }, "required": [ "id", "external_id", "code", "first_name", "middle_name", "last_name", "email", "phone", "extra_phone", "role", "gender", "national_id", "tax_id", "insurance_id", "employment", "locale", "timezone", "date_birth", "date_hire", "date_leave", "photo", "created_at", "updated_at", "dismissed_at" ] } }, "links": { "$ref": "#/components/schemas/PageLinks" }, "meta": { "$ref": "#/components/schemas/PageMeta" } }, "required": [ "data", "links", "meta" ] }, "example": { "data": [ { "id": 70753, "external_id": "HR-1001", "code": "EMP-2", "first_name": "Bolat", "middle_name": null, "last_name": "Nurlan", "email": "bolat@example.test", "phone": "+77010000002", "extra_phone": null, "role": null, "gender": "male", "national_id": "900101300123", "tax_id": null, "insurance_id": null, "employment": "full_time", "locale": "en", "timezone": "Asia/Almaty", "date_birth": "1990-01-01", "date_hire": "2026-01-15", "date_leave": null, "photo": null, "created_at": "2026-03-02T12:00:00+00:00", "updated_at": "2026-03-02T12:00:00+00:00", "dismissed_at": null } ], "links": { "first": null, "last": null, "prev": null, "next": null }, "meta": { "path": "https://api.clockster.com/company/v3/users", "per_page": 50, "next_cursor": null, "prev_cursor": null } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "The roster as we hold it. Every scalar is always present; a relation appears only when\n`include` names it.\n\n**`status` decides whether the people who left are in the answer** — `active` by\ndefault, `dismissed` for only them, `all` for both. A dismissed employee keeps their\nrecord: `date_leave` says when they were let go and `dismissed_at` when the record was\nclosed. A sync that never asks for them cannot learn that anyone left, so ask\nperiodically even if your day-to-day reads are `active`.\n\n**`external_ids` is the other half of the roster write.** Ask with the same keys you\nsent to `/users/upsert` and reconcile without keeping a map of our ids.\n\n`updated_since` reads only what changed, against the `updated_at` every row carries.", "security": [ { "companyToken": [ "users:read" ] } ] } }, "/company/v3/users/dismiss": { "post": { "operationId": "company.v3.users.dismiss", "tags": [ "Employees" ], "summary": "Dismiss employees", "parameters": [], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/UpsertOutcome" } } }, "required": [ "data" ] }, "example": { "data": [ { "external_id": "HR-1001", "id": 70753, "result": "already_dismissed" } ] } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Up to 100 people in one call, each named by `external_id` or by `id`, exactly one per\nitem.\n\n**This is dismissal, not erasure.** The record stays and stays readable: the person\nappears under `status=dismissed` with `dismissed_at` set, and the seat is freed for\nsomebody else. It is the shape leaving actually has — a nightly sync noticing that\ntwelve people are no longer on the roster.\n\n**There is no hard delete on this API.** Erasing a person takes their attendance,\npayroll, documents and bank details with them, with no way to undo it, and one ability\ngrants this whole API — an integrator that syncs your roster cannot be given that\nwithout also being given erasure. If a retention obligation needs it, ask us.\n\n**Somebody already gone answers `already_dismissed`** rather than failing, which matters\nhere because dismissing frees the key for a new hire.\n\n**If a key is held by two people** — one who left and one hired since — the living one\nis the one dismissed.\n\nWhat happens that you cannot see, and cannot undo:\n\n- Their sessions end immediately and their phone is unpaired.\n- Every terminal at their locations is told to forget their face. That is sent once and\n not retried, so a terminal that is offline at the time keeps admitting them.\n- **Requests still waiting on them to approve are cancelled**, not just their own. Dismiss\n a manager and their team's pending vacation requests are cancelled with them.\n- An offboarding process starts, if the company has one configured.\n- `date_leave` is filled in with today's date if it was empty.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "users": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "object", "properties": { "external_id": { "type": "string", "maxLength": 64, "description": "Who to dismiss, by your key for them. This or `id`, exactly one per item." }, "id": { "type": "integer", "description": "Who to dismiss, by the id this API issued. This or `external_id`, exactly one per item." } } }, "description": "The people to dismiss, up to 100 a call." } }, "required": [ "users" ] } } } }, "security": [ { "companyToken": [ "users:write" ] } ] } }, "/company/v3/users/upsert": { "post": { "operationId": "company.v3.users.upsert", "tags": [ "Employees" ], "summary": "Create or update employees", "parameters": [], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/UpsertOutcome" } } }, "required": [ "data" ] }, "example": { "data": [ { "external_id": "HR-1", "id": 47580, "result": "updated" }, { "external_id": "HR-2", "id": 38630, "result": "created" } ] } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "The roster, written in batches of up to 100. `external_id` is optional here and behaves\nas it does everywhere: an item carrying one updates the person it names, an item without\none always creates. `first_name`, `role` and `location_id` are the minimum.\n\nThe field set is what an HR system holds about an employee.\n\nA person created here reaches the turnstiles of their location, and every location's\ndevices are told once for the whole batch rather than once per person.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "users": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "object", "properties": { "external_id": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 64, "description": "Your own key for this row. Send it on every write and the next one updates rather than duplicates." }, "first_name": { "type": "string", "maxLength": 20, "description": "Given name. The one field every person must have." }, "middle_name": { "type": [ "string", "null" ], "maxLength": 40, "description": "Middle name, where the place they live uses one." }, "last_name": { "type": [ "string", "null" ], "maxLength": 40, "description": "Family name." }, "code": { "type": [ "string", "null" ], "minLength": 1, "maxLength": 40, "description": "A short code people read, yours to choose. The listing beside this write can filter on it." }, "email": { "type": [ "string", "null" ], "format": "email", "maxLength": 255, "description": "Their email address." }, "phone": { "type": [ "string", "null" ], "minLength": 10, "maxLength": 21, "description": "Their phone number." }, "extra_phone": { "type": [ "string", "null" ], "minLength": 10, "maxLength": 21, "description": "A second phone number." }, "role": { "type": "string", "enum": [ "admin", "employee" ], "x-clockster-set": "UsersRole", "description": "Whether the person administers the company or is an employee in it." }, "gender": { "type": [ "string", "null" ], "enum": [ "male", "female", "other" ], "x-clockster-set": "UsersGender", "description": "Their gender, as the personnel file records it." }, "locale": { "type": [ "string", "null" ], "enum": [ "en", "ru", "kk", "uk", "id", "uz", "az", "fr", "vi", "zh" ], "x-clockster-set": "UsersLocale", "description": "Which language the application speaks to them in." }, "timezone": { "type": [ "string", "null" ], "maxLength": 64, "description": "The zone they work in, as a name — `Asia/Almaty`. A name rather than an offset, unlike a schedule or a task, because a person's zone follows the rules of the place they are in." }, "date_hire": { "type": [ "string", "null" ], "format": "date", "description": "The day they started, `YYYY-MM-DD`." }, "date_leave": { "type": [ "string", "null" ], "format": "date", "description": "The day they leave or left, `YYYY-MM-DD`. Filled in for you on a dismissal that does not carry one." }, "date_birth": { "type": [ "string", "null" ], "format": "date", "description": "Their date of birth, `YYYY-MM-DD`." }, "national_id": { "type": [ "string", "null" ], "maxLength": 20, "description": "Their national identifier." }, "tax_id": { "type": [ "string", "null" ], "maxLength": 20, "description": "Their tax identifier." }, "insurance_id": { "type": [ "string", "null" ], "maxLength": 20, "description": "Their insurance identifier." }, "employment": { "type": [ "string", "null" ], "enum": [ "full_time", "part_time", "irregular_hours", "contract_1", "contract_2", "apprenticeship", "traineeship", "piece_rate", "probation", "outstaffing" ], "x-clockster-set": "UsersEmployment", "description": "The terms they are employed on." }, "responsibility": { "type": [ "string", "null" ], "maxLength": 5000, "description": "Free text about what this person is responsible for." }, "location_id": { "type": "integer", "description": "The location they are filed under, by id. Required — everybody belongs somewhere." }, "locations": { "type": [ "array", "null" ], "maxItems": 50, "items": { "type": "integer" }, "description": "Every other location they may work at, by id, beside the one they are filed under." }, "department_id": { "type": [ "integer", "null" ], "description": "The department this is filed against, by id. Null clears it." }, "position_id": { "type": [ "integer", "null" ], "description": "The position this is filed against, by id. Null clears it." }, "user_filters": { "type": [ "array", "null" ], "maxItems": 50, "items": { "type": "integer" }, "description": "The groupings they belong to, by id." } }, "required": [ "first_name", "role", "location_id" ] }, "description": "The people to write, up to 100 a call." } }, "required": [ "users" ] } } } }, "security": [ { "companyToken": [ "users:write" ] } ] } }, "/company/v3/users/{id}": { "get": { "operationId": "company.v3.users.show", "tags": [ "Employees" ], "summary": "Read one employee", "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } }, { "name": "include", "in": "query", "required": false, "description": "Relations to load, comma-separated. Anything not named is absent from the answer rather than null.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "enum": [ "location", "locations", "department", "position", "user_filters", "dismissal", "meta" ], "x-clockster-set": "UsersInclude" } } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "code": { "type": [ "null", "string" ] }, "first_name": { "type": "string" }, "middle_name": { "type": [ "null", "string" ] }, "last_name": { "type": "string" }, "email": { "type": "string" }, "phone": { "type": [ "null", "string" ] }, "extra_phone": { "type": [ "null", "string" ] }, "role": { "type": [ "null", "string" ] }, "gender": { "type": "string" }, "national_id": { "type": "string" }, "tax_id": { "type": [ "null", "string" ] }, "insurance_id": { "type": [ "null", "string" ] }, "employment": { "type": "string" }, "locale": { "type": "string" }, "timezone": { "type": "string" }, "date_birth": { "type": "string" }, "date_hire": { "type": "string" }, "date_leave": { "type": [ "null", "string" ] }, "photo": { "type": [ "null", "string" ] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" }, "dismissed_at": { "type": [ "null", "string" ] }, "location": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "code": { "type": [ "null", "string" ] }, "title": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "latitude": { "type": [ "null", "number" ] }, "longitude": { "type": [ "null", "number" ] }, "radius": { "type": "integer" }, "created_at": { "type": "string" }, "updated_at": { "type": "string" } }, "required": [ "id", "external_id", "code", "title", "description", "latitude", "longitude", "radius", "created_at", "updated_at" ] }, "locations": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "code": { "type": [ "null", "string" ] }, "title": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "latitude": { "type": [ "null", "number" ] }, "longitude": { "type": [ "null", "number" ] }, "radius": { "type": "integer" }, "created_at": { "type": "string" }, "updated_at": { "type": "string" } }, "required": [ "id", "external_id", "code", "title", "description", "latitude", "longitude", "radius", "created_at", "updated_at" ] } }, "department": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "title": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" } }, "required": [ "id", "external_id", "title", "description", "created_at", "updated_at" ] }, "position": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "title": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" } }, "required": [ "id", "external_id", "title", "description", "created_at", "updated_at" ] }, "user_filters": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "external_id": { "type": [ "null", "string" ] }, "title": { "type": "string" }, "description": { "type": [ "null", "string" ] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" } }, "required": [ "id", "external_id", "title", "description", "created_at", "updated_at" ] } }, "dismissal": { "type": "object", "properties": { "id": { "type": "integer" }, "title": { "type": "string" } }, "required": [ "id", "title" ] }, "meta": { "type": "object", "properties": { "birth_place": { "type": "string" }, "marital_status": { "type": "string" }, "religion": { "type": [ "null", "string" ] }, "blood_type": { "type": [ "null", "string" ] }, "children": { "type": [ "integer", "null" ] }, "contact_name": { "type": [ "null", "string" ] }, "relationship": { "type": [ "null", "string" ] }, "phone": { "type": [ "null", "string" ] }, "domicile_address": { "type": [ "null", "string" ] }, "domicile_city": { "type": [ "null", "string" ] }, "domicile_province": { "type": [ "null", "string" ] }, "domicile_district": { "type": [ "null", "string" ] }, "domicile_postal_code": { "type": [ "null", "string" ] }, "document_address": { "type": [ "null", "string" ] }, "document_city": { "type": [ "null", "string" ] }, "document_province": { "type": [ "null", "string" ] }, "document_district": { "type": [ "null", "string" ] }, "document_postal_code": { "type": [ "null", "string" ] }, "education_level": { "type": [ "null", "string" ] }, "education_institution": { "type": [ "null", "string" ] }, "education_major": { "type": [ "null", "string" ] }, "graduation_year": { "type": [ "integer", "null" ] }, "education_gpa": { "type": [ "null", "number" ] }, "full_name": { "type": [ "null", "string" ] }, "alias_name": { "type": [ "null", "string" ] }, "local_name": { "type": [ "null", "string" ] }, "nationality": { "type": [ "null", "string" ] }, "marriage_date": { "type": [ "null", "string" ] }, "retire_age": { "type": [ "integer", "null" ] }, "retire_date": { "type": [ "null", "string" ] }, "ethnic_origin": { "type": [ "null", "string" ] }, "contract_number": { "type": [ "null", "string" ] }, "health_insurance_id": { "type": [ "null", "string" ] }, "gov_savings_id": { "type": [ "null", "string" ] }, "gov_savings_acc": { "type": [ "null", "string" ] } }, "required": [ "birth_place", "marital_status", "religion", "blood_type", "children", "contact_name", "relationship", "phone", "domicile_address", "domicile_city", "domicile_province", "domicile_district", "domicile_postal_code", "document_address", "document_city", "document_province", "document_district", "document_postal_code", "education_level", "education_institution", "education_major", "graduation_year", "education_gpa", "full_name", "alias_name", "local_name", "nationality", "marriage_date", "retire_age", "retire_date", "ethnic_origin", "contract_number", "health_insurance_id", "gov_savings_id", "gov_savings_acc" ] } }, "required": [ "id", "external_id", "code", "first_name", "middle_name", "last_name", "email", "phone", "extra_phone", "role", "gender", "national_id", "tax_id", "insurance_id", "employment", "locale", "timezone", "date_birth", "date_hire", "date_leave", "photo", "created_at", "updated_at", "dismissed_at" ] } }, "required": [ "data" ] }, "example": { "data": { "id": 70753, "external_id": "HR-1001", "code": "EMP-2", "first_name": "Bolat", "middle_name": null, "last_name": "Nurlan", "email": "bolat@example.test", "phone": "+77010000002", "extra_phone": null, "role": null, "gender": "male", "national_id": "900101300123", "tax_id": null, "insurance_id": null, "employment": "full_time", "locale": "en", "timezone": "Asia/Almaty", "date_birth": "1990-01-01", "date_hire": "2026-01-15", "date_leave": null, "photo": null, "created_at": "2026-03-02T12:00:00+00:00", "updated_at": "2026-03-02T12:00:00+00:00", "dismissed_at": null } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "One employee by our id. Prefer it over `?ids=` when you expect exactly one: someone who\nis not yours answers `404`, where the listing answers `200` with an empty array, so a\nstatus can be branched on without counting.\n\nReachable for a dismissed person too.", "security": [ { "companyToken": [ "users:read" ] } ] } }, "/company/v3/webhooks": { "get": { "operationId": "company.v3.webhooks.index", "tags": [ "Webhooks" ], "summary": "List webhook endpoints", "parameters": [ { "name": "per_page", "in": "query", "required": false, "description": "How many rows one page holds. Defaults to 50.", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } }, { "name": "cursor", "in": "query", "required": false, "description": "The `meta.next_cursor` of the previous page. Omit it for the first. A cursor is bound to the filters it was issued under — change them and start again.", "schema": { "type": [ "string", "null" ], "maxLength": 512 } }, { "name": "active", "in": "query", "required": false, "description": "Only rows switched on (`true`) or off (`false`). Omit for both.", "schema": { "type": "boolean" } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "title": { "type": "string" }, "url": { "type": "string" }, "contact_email": { "type": "string" }, "secret": { "type": "string" }, "events": { "type": "array", "items": { "type": "string" } }, "auth": { "type": "object", "properties": { "type": { "type": "string" }, "username": { "type": [ "null", "string" ] } }, "required": [ "type", "username" ] }, "active": { "type": "boolean" }, "health": { "type": "object", "properties": { "consecutive_failures": { "type": "integer" }, "last_success_at": { "type": [ "null", "string" ] }, "last_failure_at": { "type": [ "null", "string" ] }, "last_failure_status": { "type": [ "integer", "null" ] }, "disabled_at": { "type": [ "null", "string" ] }, "disabled_reason": { "type": [ "null", "string" ] } }, "required": [ "consecutive_failures", "last_success_at", "last_failure_at", "last_failure_status", "disabled_at", "disabled_reason" ] } }, "required": [ "id", "title", "url", "contact_email", "secret", "events", "auth", "active", "health" ] } }, "links": { "$ref": "#/components/schemas/PageLinks" }, "meta": { "$ref": "#/components/schemas/PageMeta" } }, "required": [ "data", "links", "meta" ] }, "example": { "data": [ { "id": 44459, "title": "Task tracker", "url": "https://partner.test/hook", "contact_email": "integrations@partner.test", "secret": "0000000000000000000000000000000000000000000000000000000000000000", "events": [ "task.completed", "task.approved" ], "auth": { "type": "none", "username": null }, "active": true, "health": { "consecutive_failures": 0, "last_success_at": null, "last_failure_at": null, "last_failure_status": null, "disabled_at": null, "disabled_reason": null } } ], "links": { "first": null, "last": null, "prev": null, "next": null }, "meta": { "path": "http://api.clockster.local/company/v3/webhooks", "per_page": 50, "next_cursor": null, "prev_cursor": null } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "The endpoints this company has connected, and the health of each.\n\n`health` answers what `active` cannot: whether a person switched an endpoint off or a\nrun of failures did. Five consecutive permanent failures — a wrong address, refused\ncredentials — switch it off, as do twenty transient ones. `disabled_reason` says which.\n\n`secret` is answered in full because verifying a signature is impossible without it.\nThe credential we authenticate to the receiver *with* is not: `auth` names the scheme\nand, for basic, the username, and never the password or bearer token.", "security": [ { "companyToken": [ "webhooks:read" ] } ] }, "post": { "operationId": "company.v3.webhooks.store", "tags": [ "Webhooks" ], "summary": "Create a webhook endpoint", "parameters": [ { "name": "Idempotency-Key", "in": "header", "required": false, "description": "A value you choose, unique per attempt. Send the same one when retrying and the first answer is replayed instead of the write happening twice. Optional, and only on the writes that carry no key of your own.", "schema": { "type": "string", "maxLength": 255 } } ], "responses": { "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "integer" }, "title": { "type": "string" }, "url": { "type": "string" }, "contact_email": { "type": "string" }, "secret": { "type": "string" }, "events": { "type": "array", "items": { "type": "string" } }, "auth": { "type": "object", "properties": { "type": { "type": "string" }, "username": { "type": [ "null", "string" ] } }, "required": [ "type", "username" ] }, "active": { "type": "boolean" }, "health": { "type": "object", "properties": { "consecutive_failures": { "type": "integer" }, "last_success_at": { "type": [ "null", "string" ] }, "last_failure_at": { "type": [ "null", "string" ] }, "last_failure_status": { "type": [ "integer", "null" ] }, "disabled_at": { "type": [ "null", "string" ] }, "disabled_reason": { "type": [ "null", "string" ] } }, "required": [ "consecutive_failures", "last_success_at", "last_failure_at", "last_failure_status", "disabled_at", "disabled_reason" ] } }, "required": [ "id", "title", "url", "contact_email", "secret", "events", "auth", "active", "health" ] } }, "required": [ "data" ] }, "example": { "data": { "id": 44459, "title": "Roster sync", "url": "https://partner.test/roster", "contact_email": "integrations@partner.test", "secret": "0000000000000000000000000000000000000000000000000000000000000000", "events": [ "user.created", "user.updated" ], "auth": { "type": "none", "username": null }, "active": true, "health": { "consecutive_failures": 0, "last_success_at": null, "last_failure_at": null, "last_failure_status": null, "disabled_at": null, "disabled_reason": null } } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Connect an endpoint. Answers 201 with the signing secret it will use.\n\n**No `external_id`, and no upsert**, unlike every other write on this surface. Those\nmirror something your system already holds and must match on its own key; an endpoint is\ncreated here and its identity is ours, so there is nothing to match against.\n\nThe secret is generated, never accepted: a caller-chosen signing key is a caller-chosen\nweakness. Replace it with `POST /company/v3/webhooks/{id}/secret`.\n\nDeliveries carry `X-Clockster-Event`, `X-Clockster-Delivery` (constant across retries,\nso a repeat can be recognised), `X-Clockster-Timestamp` and `X-Clockster-Signature` —\n`sha256=` HMAC-SHA256 of `timestamp + \".\" + rawBody` under the secret. The body is\n`{\"id\", \"event\", \"occurred_at\", \"data\"}`.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "title": { "type": [ "string", "null" ], "maxLength": 255, "description": "What to call this endpoint, so a list of them reads." }, "url": { "type": "string", "maxLength": 2048, "description": "Where deliveries are posted. `http` or `https`." }, "contact_email": { "type": [ "string", "null" ], "format": "email", "maxLength": 255, "description": "An address to reach you about this endpoint." }, "events": { "type": "array", "minItems": 1, "maxItems": 50, "items": { "type": "string", "enum": [ "user.created", "user.updated", "user.deleted", "user.restored", "user.purged", "location.created", "location.updated", "location.deleted", "department.created", "department.updated", "department.deleted", "position.created", "position.updated", "position.deleted", "task.created", "task.completed", "task.approved", "task.rejected", "task.deleted" ], "x-clockster-set": "WebhooksEvent" }, "description": "Which events this endpoint receives. At least one, and no repeats." }, "auth_basic": { "type": [ "object", "null" ], "properties": { "username": { "type": "string", "maxLength": 255, "description": "The user for `auth_basic`." }, "password": { "type": "string", "maxLength": 255, "description": "The password for `auth_basic`." } }, "description": "Send deliveries with HTTP basic authentication. Not alongside `auth_token`." }, "auth_token": { "type": [ "string", "null" ], "maxLength": 2048, "description": "Send deliveries carrying this bearer token. Not alongside `auth_basic`." }, "active": { "type": "boolean", "description": "Whether the endpoint receives deliveries. False stops them without deleting it." } }, "required": [ "url", "events", "active" ] } } } }, "security": [ { "companyToken": [ "webhooks:write" ] } ] } }, "/company/v3/webhooks/deliveries": { "get": { "operationId": "company.v3.webhooks.deliveries.index", "tags": [ "Webhooks" ], "summary": "List webhook deliveries", "parameters": [ { "name": "per_page", "in": "query", "required": false, "description": "How many rows one page holds. Defaults to 50.", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } }, { "name": "cursor", "in": "query", "required": false, "description": "The `meta.next_cursor` of the previous page. Omit it for the first. A cursor is bound to the filters it was issued under — change them and start again.", "schema": { "type": [ "string", "null" ], "maxLength": 512 } }, { "name": "webhooks", "in": "query", "required": false, "description": "Only deliveries to these endpoints, by id.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 100, "items": { "type": "integer" } } }, { "name": "events", "in": "query", "required": false, "description": "Only deliveries of these events.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "maxItems": 19, "items": { "type": "string", "enum": [ "user.created", "user.updated", "user.deleted", "user.restored", "user.purged", "location.created", "location.updated", "location.deleted", "department.created", "department.updated", "department.deleted", "position.created", "position.updated", "position.deleted", "task.created", "task.completed", "task.approved", "task.rejected", "task.deleted" ], "x-clockster-set": "WebhooksEvent" } } }, { "name": "successful", "in": "query", "required": false, "description": "Only deliveries that were accepted (`true`) or that were not (`false`).", "schema": { "type": "boolean" } }, { "name": "pending", "in": "query", "required": false, "description": "Only deliveries still waiting on a retry (`true`), or only those finished with (`false`).", "schema": { "type": "boolean" } }, { "name": "since", "in": "query", "required": false, "description": "Only rows from this instant onward (ISO 8601).", "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "include", "in": "query", "required": false, "description": "Relations to load, comma-separated. Anything not named is absent from the answer rather than null.", "style": "form", "explode": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "enum": [ "payload" ], "x-clockster-set": "WebhooksDeliveriesInclude" } } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "webhook_id": { "type": "integer" }, "event": { "type": "string" }, "state": { "type": "string" }, "attempts": { "type": "integer" }, "response_status": { "type": "integer" }, "failure_reason": { "type": [ "null", "string" ] }, "occurred_at": { "type": "string" }, "next_attempt_at": { "type": [ "null", "string" ] }, "processed_at": { "type": [ "null", "string" ] }, "payload": { "type": "object", "properties": { "id": { "type": "integer" } }, "required": [ "id" ] } }, "required": [ "id", "webhook_id", "event", "state", "attempts", "response_status", "failure_reason", "occurred_at", "next_attempt_at", "processed_at" ] } }, "links": { "$ref": "#/components/schemas/PageLinks" }, "meta": { "$ref": "#/components/schemas/PageMeta" } }, "required": [ "data", "links", "meta" ] }, "example": { "data": [ { "id": 61985, "webhook_id": 44459, "event": "task.approved", "state": "pending", "attempts": 1, "response_status": 500, "failure_reason": null, "occurred_at": "2026-03-02T09:00:00+00:00", "next_attempt_at": "2026-03-02T09:05:00+00:00", "processed_at": null }, { "id": 61985, "webhook_id": 44459, "event": "task.completed", "state": "delivered", "attempts": 0, "response_status": 200, "failure_reason": null, "occurred_at": "2026-03-02T09:00:00+00:00", "next_attempt_at": null, "processed_at": "2026-03-02T09:00:01+00:00" } ], "links": { "first": null, "last": null, "prev": null, "next": null }, "meta": { "path": "http://api.clockster.local/company/v3/webhooks/deliveries", "per_page": 50, "next_cursor": null, "prev_cursor": null } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "What was sent, and what came of it. **Newest first**, unlike every other listing here: a\ndelivery log is read to learn what just happened.\n\n`state` is the field to branch on. `delivered` and `failed` are final; `pending` is\nneither — the event is waiting out its backoff and will be tried again. Reading\n`is_successful: false` as failure is the mistake the two stored flags invite.\n\n`payload` is behind `include=payload` and off by default: a page of a hundred deliveries\nis a hundred employee records, and a caller watching its integration's health has no use\nfor them. Reading one delivery answers with it either way.\n\n`webhook_id` is null where the endpoint has since been removed.", "security": [ { "companyToken": [ "webhooks:read" ] } ] } }, "/company/v3/webhooks/deliveries/{id}": { "get": { "operationId": "company.v3.webhooks.deliveries.show", "tags": [ "Webhooks" ], "summary": "Read one webhook delivery", "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "integer" }, "webhook_id": { "type": "integer" }, "event": { "type": "string" }, "state": { "type": "string" }, "attempts": { "type": "integer" }, "response_status": { "type": "integer" }, "failure_reason": { "type": [ "null", "string" ] }, "occurred_at": { "type": "string" }, "next_attempt_at": { "type": [ "null", "string" ] }, "processed_at": { "type": "string" }, "payload": { "type": "object", "properties": { "id": { "type": "integer" } }, "required": [ "id" ] } }, "required": [ "id", "webhook_id", "event", "state", "attempts", "response_status", "failure_reason", "occurred_at", "next_attempt_at", "processed_at", "payload" ] } }, "required": [ "data" ] }, "example": { "data": { "id": 61985, "webhook_id": 44459, "event": "task.completed", "state": "delivered", "attempts": 0, "response_status": 200, "failure_reason": null, "occurred_at": "2026-03-02T09:00:00+00:00", "next_attempt_at": null, "processed_at": "2026-03-02T09:00:01+00:00", "payload": { "id": 78197 } } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Answers with the payload, unlike the listing: one row is not a hundred employee records.", "security": [ { "companyToken": [ "webhooks:read" ] } ] } }, "/company/v3/webhooks/deliveries/{id}/redeliver": { "post": { "operationId": "company.v3.webhooks.deliveries.redeliver", "tags": [ "Webhooks" ], "summary": "Send a delivery again", "parameters": [ { "name": "Idempotency-Key", "in": "header", "required": false, "description": "A value you choose, unique per attempt. Send the same one when retrying and the first answer is replayed instead of the write happening twice. Optional, and only on the writes that carry no key of your own.", "schema": { "type": "string", "maxLength": 255 } }, { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/DeleteOutcome" } }, "required": [ "data" ] }, "example": { "data": { "id": 61985, "result": "queued" } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Queues the recorded event for another attempt, and answers once queued rather than once\ndelivered.\n\nA repair tool, now that transient failures retry themselves: this is for the case where\nthe receiver was fixed after we had already given up. What goes out is the event as it\nwas recorded, so `occurred_at` may be long past — which is why the envelope states it.", "security": [ { "companyToken": [ "webhooks:write" ] } ] } }, "/company/v3/webhooks/events": { "get": { "operationId": "company.v3.webhooks.events", "tags": [ "Webhooks" ], "summary": "List subscribable events", "parameters": [], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "string" } } }, "required": [ "data" ] }, "example": { "data": [ "user.created", "user.updated", "user.deleted", "user.restored", "user.purged", "location.created", "location.updated", "location.deleted", "department.created", "department.updated", "department.deleted", "position.created", "position.updated", "position.deleted", "task.created", "task.completed", "task.approved", "task.rejected", "task.deleted" ] } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Every event name an endpoint can subscribe to.\n\nServed as well as specified, so a caller can check at runtime that a name it stored is\nstill one we send rather than discovering it on the next save.", "security": [ { "companyToken": [ "webhooks:read" ] } ] } }, "/company/v3/webhooks/{id}": { "get": { "operationId": "company.v3.webhooks.show", "tags": [ "Webhooks" ], "summary": "Read one webhook endpoint", "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "integer" }, "title": { "type": "string" }, "url": { "type": "string" }, "contact_email": { "type": "string" }, "secret": { "type": "string" }, "events": { "type": "array", "items": { "type": "string" } }, "auth": { "type": "object", "properties": { "type": { "type": "string" }, "username": { "type": [ "null", "string" ] } }, "required": [ "type", "username" ] }, "active": { "type": "boolean" }, "health": { "type": "object", "properties": { "consecutive_failures": { "type": "integer" }, "last_success_at": { "type": [ "null", "string" ] }, "last_failure_at": { "type": [ "null", "string" ] }, "last_failure_status": { "type": [ "integer", "null" ] }, "disabled_at": { "type": [ "null", "string" ] }, "disabled_reason": { "type": [ "null", "string" ] } }, "required": [ "consecutive_failures", "last_success_at", "last_failure_at", "last_failure_status", "disabled_at", "disabled_reason" ] } }, "required": [ "id", "title", "url", "contact_email", "secret", "events", "auth", "active", "health" ] } }, "required": [ "data" ] }, "example": { "data": { "id": 44459, "title": "Task tracker", "url": "https://partner.test/hook", "contact_email": "integrations@partner.test", "secret": "0000000000000000000000000000000000000000000000000000000000000000", "events": [ "task.completed", "task.approved" ], "auth": { "type": "none", "username": null }, "active": true, "health": { "consecutive_failures": 0, "last_success_at": null, "last_failure_at": null, "last_failure_status": null, "disabled_at": null, "disabled_reason": null } } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "security": [ { "companyToken": [ "webhooks:read" ] } ] }, "put": { "operationId": "company.v3.webhooks.update", "tags": [ "Webhooks" ], "summary": "Replace a webhook endpoint", "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "integer" }, "title": { "type": "string" }, "url": { "type": "string" }, "contact_email": { "type": "string" }, "secret": { "type": "string" }, "events": { "type": "array", "items": { "type": "string" } }, "auth": { "type": "object", "properties": { "type": { "type": "string" }, "username": { "type": [ "null", "string" ] } }, "required": [ "type", "username" ] }, "active": { "type": "boolean" }, "health": { "type": "object", "properties": { "consecutive_failures": { "type": "integer" }, "last_success_at": { "type": [ "null", "string" ] }, "last_failure_at": { "type": [ "null", "string" ] }, "last_failure_status": { "type": [ "integer", "null" ] }, "disabled_at": { "type": [ "null", "string" ] }, "disabled_reason": { "type": [ "null", "string" ] } }, "required": [ "consecutive_failures", "last_success_at", "last_failure_at", "last_failure_status", "disabled_at", "disabled_reason" ] } }, "required": [ "id", "title", "url", "contact_email", "secret", "events", "auth", "active", "health" ] } }, "required": [ "data" ] }, "example": { "data": { "id": 44459, "title": "Task tracker", "url": "https://partner.test/hook", "contact_email": "ops@partner.test", "secret": "0000000000000000000000000000000000000000000000000000000000000000", "events": [ "task.completed" ], "auth": { "type": "none", "username": null }, "active": false, "health": { "consecutive_failures": 0, "last_success_at": null, "last_failure_at": null, "last_failure_status": null, "disabled_at": null, "disabled_reason": null } } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Replaces rather than patches: half a subscription is not a state worth reaching by\naccident.\n\nSaving clears the failure tally and any automatic switch-off — you are saying something\nchanged, so what the history counted no longer describes what is there. This is how an\nendpoint switched off by repeated failures is put back into service.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "title": { "type": [ "string", "null" ], "maxLength": 255, "description": "What to call this endpoint, so a list of them reads." }, "url": { "type": "string", "maxLength": 2048, "description": "Where deliveries are posted. `http` or `https`." }, "contact_email": { "type": [ "string", "null" ], "format": "email", "maxLength": 255, "description": "An address to reach you about this endpoint." }, "events": { "type": "array", "minItems": 1, "maxItems": 50, "items": { "type": "string", "enum": [ "user.created", "user.updated", "user.deleted", "user.restored", "user.purged", "location.created", "location.updated", "location.deleted", "department.created", "department.updated", "department.deleted", "position.created", "position.updated", "position.deleted", "task.created", "task.completed", "task.approved", "task.rejected", "task.deleted" ], "x-clockster-set": "WebhooksEvent" }, "description": "Which events this endpoint receives. At least one, and no repeats." }, "auth_basic": { "type": [ "object", "null" ], "properties": { "username": { "type": "string", "maxLength": 255, "description": "The user for `auth_basic`." }, "password": { "type": "string", "maxLength": 255, "description": "The password for `auth_basic`." } }, "description": "Send deliveries with HTTP basic authentication. Not alongside `auth_token`." }, "auth_token": { "type": [ "string", "null" ], "maxLength": 2048, "description": "Send deliveries carrying this bearer token. Not alongside `auth_basic`." }, "active": { "type": "boolean", "description": "Whether the endpoint receives deliveries. False stops them without deleting it." } }, "required": [ "url", "events", "active" ] } } } }, "security": [ { "companyToken": [ "webhooks:write" ] } ] }, "delete": { "operationId": "company.v3.webhooks.destroy", "tags": [ "Webhooks" ], "summary": "Delete a webhook endpoint", "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/DeleteOutcome" } }, "required": [ "data" ] }, "example": { "data": { "id": 80256, "result": "deleted" } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Removes the endpoint. What was delivered to it stays readable: the delivery's link is\nnulled rather than cascaded, because the record of what was sent is the company's.", "security": [ { "companyToken": [ "webhooks:write" ] } ] } }, "/company/v3/webhooks/{id}/secret": { "post": { "operationId": "company.v3.webhooks.secret", "tags": [ "Webhooks" ], "summary": "Replace the signing secret", "parameters": [ { "name": "Idempotency-Key", "in": "header", "required": false, "description": "A value you choose, unique per attempt. Send the same one when retrying and the first answer is replayed instead of the write happening twice. Optional, and only on the writes that carry no key of your own.", "schema": { "type": "string", "maxLength": 255 } }, { "name": "id", "in": "path", "required": true, "description": "The id of the row, as this API issued it.", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "integer" }, "title": { "type": "string" }, "url": { "type": "string" }, "contact_email": { "type": "string" }, "secret": { "type": "string" }, "events": { "type": "array", "items": { "type": "string" } }, "auth": { "type": "object", "properties": { "type": { "type": "string" }, "username": { "type": [ "null", "string" ] } }, "required": [ "type", "username" ] }, "active": { "type": "boolean" }, "health": { "type": "object", "properties": { "consecutive_failures": { "type": "integer" }, "last_success_at": { "type": [ "null", "string" ] }, "last_failure_at": { "type": [ "null", "string" ] }, "last_failure_status": { "type": [ "integer", "null" ] }, "disabled_at": { "type": [ "null", "string" ] }, "disabled_reason": { "type": [ "null", "string" ] } }, "required": [ "consecutive_failures", "last_success_at", "last_failure_at", "last_failure_status", "disabled_at", "disabled_reason" ] } }, "required": [ "id", "title", "url", "contact_email", "secret", "events", "auth", "active", "health" ] } }, "required": [ "data" ] }, "example": { "data": { "id": 44459, "title": "Task tracker", "url": "https://partner.test/hook", "contact_email": "ops@partner.test", "secret": "0000000000000000000000000000000000000000000000000000000000000000", "events": [ "task.completed" ], "auth": { "type": "none", "username": null }, "active": false, "health": { "consecutive_failures": 0, "last_success_at": null, "last_failure_at": null, "last_failure_status": null, "disabled_at": null, "disabled_reason": null } } } } } }, "401": { "description": "No token, or one this surface does not accept — code `unauthenticated`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "unauthenticated", "message": "Unauthenticated.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "403": { "description": "A token without the company ability — code `forbidden`; a key without the scope the operation needs — code `insufficient_scope`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "forbidden", "message": "Forbidden.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "404": { "description": "No such row in the calling company — code `not_found`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "not_found", "message": "No such resource in this company.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } }, "422": { "description": "Refused; `error.code` says why and `error.errors` names the fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationRefusal" }, "example": { "error": { "code": "validation_failed", "message": "The per page must be at least 1.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90", "errors": { "per_page": [ "The per page must be at least 1." ] } } } } } }, "429": { "description": "Rate limited; `Retry-After` says when — code `rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refusal" }, "example": { "error": { "code": "rate_limited", "message": "Too Many Attempts.", "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90" } } } } } }, "description": "Answers the endpoint with a new secret.\n\n**Send an `Idempotency-Key`.** The secret is shown once, so a retry after a lost\nresponse would rotate a second time and leave you holding one that signs nothing — with\nthe header, the retry is answered with the secret the first call minted.\n\nDeliveries already queued are signed with whichever secret is current when they are\nactually sent, so accept both for as long as your backlog can be deep — up to about a\nday where transient failures are being retried.", "security": [ { "companyToken": [ "webhooks:write" ] } ] } } } }