clockster 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. {clockster-0.3.0 → clockster-0.4.0}/CHANGELOG.md +22 -0
  2. {clockster-0.3.0 → clockster-0.4.0}/PKG-INFO +2 -1
  3. {clockster-0.3.0 → clockster-0.4.0}/README.md +1 -0
  4. {clockster-0.3.0 → clockster-0.4.0}/openapi/company-v3.json +743 -2
  5. {clockster-0.3.0 → clockster-0.4.0}/pyproject.toml +1 -1
  6. {clockster-0.3.0 → clockster-0.4.0}/src/clockster/_generated/api.py +136 -0
  7. {clockster-0.3.0 → clockster-0.4.0}/src/clockster/_generated/models.py +66 -0
  8. {clockster-0.3.0 → clockster-0.4.0}/.github/workflows/ci.yml +0 -0
  9. {clockster-0.3.0 → clockster-0.4.0}/.github/workflows/release.yml +0 -0
  10. {clockster-0.3.0 → clockster-0.4.0}/.github/workflows/spec-drift.yml +0 -0
  11. {clockster-0.3.0 → clockster-0.4.0}/.gitignore +0 -0
  12. {clockster-0.3.0 → clockster-0.4.0}/LICENSE +0 -0
  13. {clockster-0.3.0 → clockster-0.4.0}/Makefile +0 -0
  14. {clockster-0.3.0 → clockster-0.4.0}/examples/README.md +0 -0
  15. {clockster-0.3.0 → clockster-0.4.0}/examples/roster_sync.py +0 -0
  16. {clockster-0.3.0 → clockster-0.4.0}/examples/timesheet_export.py +0 -0
  17. {clockster-0.3.0 → clockster-0.4.0}/scripts/check_operations.py +0 -0
  18. {clockster-0.3.0 → clockster-0.4.0}/scripts/generate.py +0 -0
  19. {clockster-0.3.0 → clockster-0.4.0}/scripts/sync_spec.py +0 -0
  20. {clockster-0.3.0 → clockster-0.4.0}/src/clockster/__init__.py +0 -0
  21. {clockster-0.3.0 → clockster-0.4.0}/src/clockster/_client.py +0 -0
  22. {clockster-0.3.0 → clockster-0.4.0}/src/clockster/_generated/__init__.py +0 -0
  23. {clockster-0.3.0 → clockster-0.4.0}/src/clockster/_transport.py +0 -0
  24. {clockster-0.3.0 → clockster-0.4.0}/src/clockster/_version.py +0 -0
  25. {clockster-0.3.0 → clockster-0.4.0}/src/clockster/errors.py +0 -0
  26. {clockster-0.3.0 → clockster-0.4.0}/src/clockster/models.py +0 -0
  27. {clockster-0.3.0 → clockster-0.4.0}/src/clockster/pagination.py +0 -0
  28. {clockster-0.3.0 → clockster-0.4.0}/src/clockster/py.typed +0 -0
  29. {clockster-0.3.0 → clockster-0.4.0}/src/clockster/webhooks.py +0 -0
  30. {clockster-0.3.0 → clockster-0.4.0}/tests/conftest.py +0 -0
  31. {clockster-0.3.0 → clockster-0.4.0}/tests/test_client.py +0 -0
  32. {clockster-0.3.0 → clockster-0.4.0}/tests/test_examples.py +0 -0
  33. {clockster-0.3.0 → clockster-0.4.0}/tests/test_pagination.py +0 -0
  34. {clockster-0.3.0 → clockster-0.4.0}/tests/test_sets.py +0 -0
  35. {clockster-0.3.0 → clockster-0.4.0}/tests/test_webhooks.py +0 -0
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### May break your build (types only)
6
+
7
+ Nothing. Three operations and one set are new.
8
+
9
+ ### Changed in the API
10
+
11
+ Reaches you whether or not you update this package.
12
+
13
+ - `GET /payroll/payslips` answers the `external_id` of a dismissed employee. It was `None` on their
14
+ payslips.
15
+
16
+ ### New
17
+
18
+ - `clockster.payroll.single_adjustments`: `list`, `create` and `delete`, blocking and awaited, over
19
+ `/payroll/single-adjustments` — one-off additions and deductions that the next calculation of a
20
+ payslip takes in. `create` files up to 100 at a time, all or nothing; pass `idempotency_key=` so a
21
+ retry does not file them twice.
22
+ - `PayrollSingleAdjustmentsType` in `clockster.models`, a `Literal` of the six types, for `type` on
23
+ `create` and the `types` filter.
24
+
3
25
  ## 0.3.0
4
26
 
5
27
  ### May break your build (types only)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: clockster
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Official Python SDK for the Clockster Company API
5
5
  Project-URL: Homepage, https://github.com/clockster/sdk-python#readme
6
6
  Project-URL: Source, https://github.com/clockster/sdk-python
@@ -126,6 +126,7 @@ everything.
126
126
  | `documents` | `list` `get` `upsert` `delete` |
127
127
  | `files` | `upload` |
128
128
  | `payroll.payslips` | `list` |
129
+ | `payroll.single_adjustments` | `list` `create` `delete` |
129
130
  | `user_requests` | `list` `get` |
130
131
  | `webhooks` | `list` `get` `create` `update` `delete` `rotate_secret` |
131
132
  | `webhooks.deliveries` | `list` `get` `redeliver` |
@@ -97,6 +97,7 @@ everything.
97
97
  | `documents` | `list` `get` `upsert` `delete` |
98
98
  | `files` | `upload` |
99
99
  | `payroll.payslips` | `list` |
100
+ | `payroll.single_adjustments` | `list` `create` `delete` |
100
101
  | `user_requests` | `list` `get` |
101
102
  | `webhooks` | `list` `get` `create` `update` `delete` `rotate_secret` |
102
103
  | `webhooks.deliveries` | `list` `get` `redeliver` |
@@ -3,7 +3,7 @@
3
3
  "info": {
4
4
  "title": "Clockster Company API",
5
5
  "version": "3.0.0",
6
- "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\nOne token authenticates a company. There is no per-user identity here, so everything a call\ncan see and change belongs to the company holding the token, and another company's id\nanswers `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## 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, deleted 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\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\nTwo 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\nhas nothing to match on — read its answer rather than sending it again blind.\n\n## Rate limits\n\n100 requests a minute, counted against the token and shared by every process using it. 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\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`.** Four writes create something with\nnothing to match a second attempt against — a rota, a webhook endpoint, a rotated secret, a\ndelivery sent again. Choose a value per attempt, send it in that header, and a retry carrying\nthe same one is answered with the first result rather than performed again. The answer to a\nreplay 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\nThe token grants everything on this API for the whole company: the roster, attendance,\npayslips, documents. 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**A company has one key, and replacing it is immediate.** Issuing a new one in the web\napplication revokes the old one at once — there is no window where both answer, so a\nreplacement is a cutover rather than a migration. Plan it for a moment when a failed call\ncosts nothing, and expect the next request on the old key to answer `401`.\n\nThat also means several systems sharing a company's key stand or fall together: replacing it\nfor one cuts off the rest, and nothing in the request log tells which of them made a call.\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."
6
+ "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\nOne token authenticates a company. There is no per-user identity here, so everything a call\ncan see and change belongs to the company holding the token, and another company's id\nanswers `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## 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, deleted 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\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 token and shared by every process using it. 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\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\nThe token grants everything on this API for the whole company: the roster, attendance,\npayslips, documents. 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**A company has one key, and replacing it is immediate.** Issuing a new one in the web\napplication revokes the old one at once — there is no window where both answer, so a\nreplacement is a cutover rather than a migration. Plan it for a moment when a failed call\ncosts nothing, and expect the next request on the old key to answer `401`.\n\nThat also means several systems sharing a company's key stand or fall together: replacing it\nfor one cuts off the rest, and nothing in the request log tells which of them made a call.\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."
7
7
  },
8
8
  "servers": [
9
9
  {
@@ -71,7 +71,7 @@
71
71
  },
72
72
  {
73
73
  "name": "Payroll",
74
- "description": "Payslips as calculated, for the months they were calculated for."
74
+ "description": "Payslips as calculated, for the months they were calculated for, and the one-off additions and deductions that go into the next calculation."
75
75
  },
76
76
  {
77
77
  "name": "Webhooks",
@@ -5569,6 +5569,747 @@
5569
5569
  "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."
5570
5570
  }
5571
5571
  },
5572
+ "/company/v3/payroll/single-adjustments": {
5573
+ "get": {
5574
+ "operationId": "company.v3.payroll.single-adjustments.index",
5575
+ "tags": [
5576
+ "Payroll"
5577
+ ],
5578
+ "summary": "List single adjustments",
5579
+ "parameters": [
5580
+ {
5581
+ "name": "per_page",
5582
+ "in": "query",
5583
+ "required": false,
5584
+ "description": "How many rows one page holds. Defaults to 50.",
5585
+ "schema": {
5586
+ "type": "integer",
5587
+ "minimum": 1,
5588
+ "maximum": 100,
5589
+ "default": 50
5590
+ }
5591
+ },
5592
+ {
5593
+ "name": "cursor",
5594
+ "in": "query",
5595
+ "required": false,
5596
+ "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.",
5597
+ "schema": {
5598
+ "type": [
5599
+ "string",
5600
+ "null"
5601
+ ],
5602
+ "maxLength": 512
5603
+ }
5604
+ },
5605
+ {
5606
+ "name": "users",
5607
+ "in": "query",
5608
+ "required": false,
5609
+ "description": "Only rows belonging to these people, by id.",
5610
+ "style": "form",
5611
+ "explode": false,
5612
+ "schema": {
5613
+ "type": [
5614
+ "array",
5615
+ "null"
5616
+ ],
5617
+ "maxItems": 100,
5618
+ "items": {
5619
+ "type": "integer"
5620
+ }
5621
+ }
5622
+ },
5623
+ {
5624
+ "name": "types",
5625
+ "in": "query",
5626
+ "required": false,
5627
+ "description": "Only rows of these types.",
5628
+ "style": "form",
5629
+ "explode": false,
5630
+ "schema": {
5631
+ "type": [
5632
+ "array",
5633
+ "null"
5634
+ ],
5635
+ "maxItems": 6,
5636
+ "items": {
5637
+ "type": "string",
5638
+ "enum": [
5639
+ "service_charge",
5640
+ "single_addition_pre_tax",
5641
+ "single_addition_post_tax",
5642
+ "single_loan",
5643
+ "single_deduction_pre_tax",
5644
+ "single_deduction_post_tax"
5645
+ ],
5646
+ "x-clockster-set": "PayrollSingleAdjustmentsType"
5647
+ }
5648
+ }
5649
+ },
5650
+ {
5651
+ "name": "date_from",
5652
+ "in": "query",
5653
+ "required": false,
5654
+ "description": "Start of the window, inclusive (YYYY-MM-DD).",
5655
+ "schema": {
5656
+ "type": [
5657
+ "string",
5658
+ "null"
5659
+ ],
5660
+ "format": "date"
5661
+ }
5662
+ },
5663
+ {
5664
+ "name": "date_to",
5665
+ "in": "query",
5666
+ "required": false,
5667
+ "description": "End of the window, inclusive (YYYY-MM-DD).",
5668
+ "schema": {
5669
+ "type": [
5670
+ "string",
5671
+ "null"
5672
+ ],
5673
+ "format": "date"
5674
+ }
5675
+ }
5676
+ ],
5677
+ "responses": {
5678
+ "200": {
5679
+ "description": "Success",
5680
+ "content": {
5681
+ "application/json": {
5682
+ "schema": {
5683
+ "type": "object",
5684
+ "properties": {
5685
+ "data": {
5686
+ "type": "array",
5687
+ "items": {
5688
+ "type": "object",
5689
+ "properties": {
5690
+ "id": {
5691
+ "type": "integer"
5692
+ },
5693
+ "user": {
5694
+ "type": "object",
5695
+ "properties": {
5696
+ "id": {
5697
+ "type": "integer"
5698
+ },
5699
+ "external_id": {
5700
+ "type": [
5701
+ "null",
5702
+ "string"
5703
+ ]
5704
+ }
5705
+ },
5706
+ "required": [
5707
+ "id",
5708
+ "external_id"
5709
+ ]
5710
+ },
5711
+ "type": {
5712
+ "type": "string"
5713
+ },
5714
+ "title": {
5715
+ "type": [
5716
+ "null",
5717
+ "string"
5718
+ ]
5719
+ },
5720
+ "amount": {
5721
+ "type": [
5722
+ "null",
5723
+ "number"
5724
+ ]
5725
+ },
5726
+ "date": {
5727
+ "type": "string"
5728
+ },
5729
+ "created_at": {
5730
+ "type": "string"
5731
+ }
5732
+ },
5733
+ "required": [
5734
+ "id",
5735
+ "user",
5736
+ "type",
5737
+ "title",
5738
+ "amount",
5739
+ "date",
5740
+ "created_at"
5741
+ ]
5742
+ }
5743
+ },
5744
+ "links": {
5745
+ "$ref": "#/components/schemas/PageLinks"
5746
+ },
5747
+ "meta": {
5748
+ "$ref": "#/components/schemas/PageMeta"
5749
+ }
5750
+ },
5751
+ "required": [
5752
+ "data",
5753
+ "links",
5754
+ "meta"
5755
+ ]
5756
+ },
5757
+ "example": {
5758
+ "data": [
5759
+ {
5760
+ "id": 47809,
5761
+ "user": {
5762
+ "id": 11251,
5763
+ "external_id": "HR-1001"
5764
+ },
5765
+ "type": "single_addition_pre_tax",
5766
+ "title": "Sales bonus",
5767
+ "amount": 300.5,
5768
+ "date": "2026-03-15",
5769
+ "created_at": "2026-03-02T12:00:00+00:00"
5770
+ },
5771
+ {
5772
+ "id": 47809,
5773
+ "user": {
5774
+ "id": 11251,
5775
+ "external_id": "HR-1001"
5776
+ },
5777
+ "type": "single_deduction_post_tax",
5778
+ "title": null,
5779
+ "amount": 50.25,
5780
+ "date": "2026-03-20",
5781
+ "created_at": "2026-03-02T12:00:00+00:00"
5782
+ }
5783
+ ],
5784
+ "links": {
5785
+ "first": null,
5786
+ "last": null,
5787
+ "prev": null,
5788
+ "next": null
5789
+ },
5790
+ "meta": {
5791
+ "path": "https://api.clockster.com/company/v3/payroll/single-adjustments",
5792
+ "per_page": 50,
5793
+ "next_cursor": null,
5794
+ "prev_cursor": null
5795
+ }
5796
+ }
5797
+ }
5798
+ }
5799
+ },
5800
+ "401": {
5801
+ "description": "No token, or one this surface does not accept — code `unauthenticated`",
5802
+ "content": {
5803
+ "application/json": {
5804
+ "schema": {
5805
+ "$ref": "#/components/schemas/Refusal"
5806
+ },
5807
+ "example": {
5808
+ "error": {
5809
+ "code": "unauthenticated",
5810
+ "message": "Unauthenticated.",
5811
+ "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90"
5812
+ }
5813
+ }
5814
+ }
5815
+ }
5816
+ },
5817
+ "403": {
5818
+ "description": "A token without the company ability — code `forbidden`",
5819
+ "content": {
5820
+ "application/json": {
5821
+ "schema": {
5822
+ "$ref": "#/components/schemas/Refusal"
5823
+ },
5824
+ "example": {
5825
+ "error": {
5826
+ "code": "forbidden",
5827
+ "message": "Forbidden.",
5828
+ "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90"
5829
+ }
5830
+ }
5831
+ }
5832
+ }
5833
+ },
5834
+ "404": {
5835
+ "description": "No such row in the calling company — code `not_found`",
5836
+ "content": {
5837
+ "application/json": {
5838
+ "schema": {
5839
+ "$ref": "#/components/schemas/Refusal"
5840
+ },
5841
+ "example": {
5842
+ "error": {
5843
+ "code": "not_found",
5844
+ "message": "No such resource in this company.",
5845
+ "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90"
5846
+ }
5847
+ }
5848
+ }
5849
+ }
5850
+ },
5851
+ "422": {
5852
+ "description": "Refused; `error.code` says why and `error.errors` names the fields",
5853
+ "content": {
5854
+ "application/json": {
5855
+ "schema": {
5856
+ "$ref": "#/components/schemas/ValidationRefusal"
5857
+ },
5858
+ "example": {
5859
+ "error": {
5860
+ "code": "validation_failed",
5861
+ "message": "The per page must be at least 1.",
5862
+ "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90",
5863
+ "errors": {
5864
+ "per_page": [
5865
+ "The per page must be at least 1."
5866
+ ]
5867
+ }
5868
+ }
5869
+ }
5870
+ }
5871
+ }
5872
+ },
5873
+ "429": {
5874
+ "description": "Rate limited; `Retry-After` says when — code `rate_limited`",
5875
+ "content": {
5876
+ "application/json": {
5877
+ "schema": {
5878
+ "$ref": "#/components/schemas/Refusal"
5879
+ },
5880
+ "example": {
5881
+ "error": {
5882
+ "code": "rate_limited",
5883
+ "message": "Too Many Attempts.",
5884
+ "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90"
5885
+ }
5886
+ }
5887
+ }
5888
+ }
5889
+ }
5890
+ },
5891
+ "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."
5892
+ },
5893
+ "post": {
5894
+ "operationId": "company.v3.payroll.single-adjustments.store",
5895
+ "tags": [
5896
+ "Payroll"
5897
+ ],
5898
+ "summary": "Create single adjustments",
5899
+ "parameters": [
5900
+ {
5901
+ "name": "Idempotency-Key",
5902
+ "in": "header",
5903
+ "required": false,
5904
+ "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.",
5905
+ "schema": {
5906
+ "type": "string",
5907
+ "maxLength": 255
5908
+ }
5909
+ }
5910
+ ],
5911
+ "responses": {
5912
+ "201": {
5913
+ "description": "Created",
5914
+ "content": {
5915
+ "application/json": {
5916
+ "schema": {
5917
+ "type": "object",
5918
+ "properties": {
5919
+ "data": {
5920
+ "type": "array",
5921
+ "items": {
5922
+ "type": "object",
5923
+ "properties": {
5924
+ "id": {
5925
+ "type": "integer"
5926
+ },
5927
+ "user": {
5928
+ "type": "object",
5929
+ "properties": {
5930
+ "id": {
5931
+ "type": "integer"
5932
+ },
5933
+ "external_id": {
5934
+ "type": [
5935
+ "null",
5936
+ "string"
5937
+ ]
5938
+ }
5939
+ },
5940
+ "required": [
5941
+ "id",
5942
+ "external_id"
5943
+ ]
5944
+ },
5945
+ "type": {
5946
+ "type": "string"
5947
+ },
5948
+ "title": {
5949
+ "type": [
5950
+ "null",
5951
+ "string"
5952
+ ]
5953
+ },
5954
+ "amount": {
5955
+ "type": [
5956
+ "null",
5957
+ "number"
5958
+ ]
5959
+ },
5960
+ "date": {
5961
+ "type": "string"
5962
+ },
5963
+ "created_at": {
5964
+ "type": "string"
5965
+ }
5966
+ },
5967
+ "required": [
5968
+ "id",
5969
+ "user",
5970
+ "type",
5971
+ "title",
5972
+ "amount",
5973
+ "date",
5974
+ "created_at"
5975
+ ]
5976
+ }
5977
+ }
5978
+ },
5979
+ "required": [
5980
+ "data"
5981
+ ]
5982
+ },
5983
+ "example": {
5984
+ "data": [
5985
+ {
5986
+ "id": 47809,
5987
+ "user": {
5988
+ "id": 11251,
5989
+ "external_id": "HR-1001"
5990
+ },
5991
+ "type": "single_addition_pre_tax",
5992
+ "title": "Sales bonus",
5993
+ "amount": 300.5,
5994
+ "date": "2026-03-15",
5995
+ "created_at": "2026-03-02T12:00:00+00:00"
5996
+ },
5997
+ {
5998
+ "id": 47809,
5999
+ "user": {
6000
+ "id": 11251,
6001
+ "external_id": "HR-1001"
6002
+ },
6003
+ "type": "single_deduction_post_tax",
6004
+ "title": null,
6005
+ "amount": 50.25,
6006
+ "date": "2026-03-20",
6007
+ "created_at": "2026-03-02T12:00:00+00:00"
6008
+ }
6009
+ ]
6010
+ }
6011
+ }
6012
+ }
6013
+ },
6014
+ "401": {
6015
+ "description": "No token, or one this surface does not accept — code `unauthenticated`",
6016
+ "content": {
6017
+ "application/json": {
6018
+ "schema": {
6019
+ "$ref": "#/components/schemas/Refusal"
6020
+ },
6021
+ "example": {
6022
+ "error": {
6023
+ "code": "unauthenticated",
6024
+ "message": "Unauthenticated.",
6025
+ "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90"
6026
+ }
6027
+ }
6028
+ }
6029
+ }
6030
+ },
6031
+ "403": {
6032
+ "description": "A token without the company ability — code `forbidden`",
6033
+ "content": {
6034
+ "application/json": {
6035
+ "schema": {
6036
+ "$ref": "#/components/schemas/Refusal"
6037
+ },
6038
+ "example": {
6039
+ "error": {
6040
+ "code": "forbidden",
6041
+ "message": "Forbidden.",
6042
+ "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90"
6043
+ }
6044
+ }
6045
+ }
6046
+ }
6047
+ },
6048
+ "404": {
6049
+ "description": "No such row in the calling company — code `not_found`",
6050
+ "content": {
6051
+ "application/json": {
6052
+ "schema": {
6053
+ "$ref": "#/components/schemas/Refusal"
6054
+ },
6055
+ "example": {
6056
+ "error": {
6057
+ "code": "not_found",
6058
+ "message": "No such resource in this company.",
6059
+ "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90"
6060
+ }
6061
+ }
6062
+ }
6063
+ }
6064
+ },
6065
+ "422": {
6066
+ "description": "Refused; `error.code` says why and `error.errors` names the fields",
6067
+ "content": {
6068
+ "application/json": {
6069
+ "schema": {
6070
+ "$ref": "#/components/schemas/ValidationRefusal"
6071
+ },
6072
+ "example": {
6073
+ "error": {
6074
+ "code": "validation_failed",
6075
+ "message": "The per page must be at least 1.",
6076
+ "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90",
6077
+ "errors": {
6078
+ "per_page": [
6079
+ "The per page must be at least 1."
6080
+ ]
6081
+ }
6082
+ }
6083
+ }
6084
+ }
6085
+ }
6086
+ },
6087
+ "429": {
6088
+ "description": "Rate limited; `Retry-After` says when — code `rate_limited`",
6089
+ "content": {
6090
+ "application/json": {
6091
+ "schema": {
6092
+ "$ref": "#/components/schemas/Refusal"
6093
+ },
6094
+ "example": {
6095
+ "error": {
6096
+ "code": "rate_limited",
6097
+ "message": "Too Many Attempts.",
6098
+ "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90"
6099
+ }
6100
+ }
6101
+ }
6102
+ }
6103
+ }
6104
+ },
6105
+ "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.",
6106
+ "requestBody": {
6107
+ "required": true,
6108
+ "content": {
6109
+ "application/json": {
6110
+ "schema": {
6111
+ "type": "object",
6112
+ "properties": {
6113
+ "adjustments": {
6114
+ "type": "array",
6115
+ "minItems": 1,
6116
+ "maxItems": 100,
6117
+ "items": {
6118
+ "type": "object",
6119
+ "properties": {
6120
+ "user_id": {
6121
+ "type": "integer",
6122
+ "description": "The employee this belongs to, by the id this API issued."
6123
+ },
6124
+ "type": {
6125
+ "type": "string",
6126
+ "enum": [
6127
+ "service_charge",
6128
+ "single_addition_pre_tax",
6129
+ "single_addition_post_tax",
6130
+ "single_loan",
6131
+ "single_deduction_pre_tax",
6132
+ "single_deduction_post_tax"
6133
+ ],
6134
+ "x-clockster-set": "PayrollSingleAdjustmentsType",
6135
+ "description": "What the amount does: an addition or a deduction, before or after tax, a service charge or a one-off loan."
6136
+ },
6137
+ "amount": {
6138
+ "type": "number",
6139
+ "minimum": 0,
6140
+ "maximum": 999999999,
6141
+ "description": "How much, never negative — `type` says which way it goes. At most two decimal places."
6142
+ },
6143
+ "date": {
6144
+ "type": "string",
6145
+ "format": "date",
6146
+ "description": "The day it is dated, `YYYY-MM-DD`. The payslip whose period holds this day takes it in."
6147
+ },
6148
+ "title": {
6149
+ "type": [
6150
+ "string",
6151
+ "null"
6152
+ ],
6153
+ "maxLength": 250,
6154
+ "description": "The name this is shown under."
6155
+ }
6156
+ },
6157
+ "required": [
6158
+ "user_id",
6159
+ "type",
6160
+ "amount",
6161
+ "date"
6162
+ ]
6163
+ },
6164
+ "description": "The adjustments to file, up to 100 a call."
6165
+ }
6166
+ },
6167
+ "required": [
6168
+ "adjustments"
6169
+ ]
6170
+ }
6171
+ }
6172
+ }
6173
+ }
6174
+ }
6175
+ },
6176
+ "/company/v3/payroll/single-adjustments/{id}": {
6177
+ "delete": {
6178
+ "operationId": "company.v3.payroll.single-adjustments.destroy",
6179
+ "tags": [
6180
+ "Payroll"
6181
+ ],
6182
+ "summary": "Delete a single adjustment",
6183
+ "parameters": [
6184
+ {
6185
+ "name": "id",
6186
+ "in": "path",
6187
+ "required": true,
6188
+ "description": "The id of the row, as this API issued it.",
6189
+ "schema": {
6190
+ "type": "integer"
6191
+ }
6192
+ }
6193
+ ],
6194
+ "responses": {
6195
+ "200": {
6196
+ "description": "Success",
6197
+ "content": {
6198
+ "application/json": {
6199
+ "schema": {
6200
+ "type": "object",
6201
+ "properties": {
6202
+ "data": {
6203
+ "$ref": "#/components/schemas/DeleteOutcome"
6204
+ }
6205
+ },
6206
+ "required": [
6207
+ "data"
6208
+ ]
6209
+ },
6210
+ "example": {
6211
+ "data": {
6212
+ "id": 47809,
6213
+ "result": "deleted"
6214
+ }
6215
+ }
6216
+ }
6217
+ }
6218
+ },
6219
+ "401": {
6220
+ "description": "No token, or one this surface does not accept — code `unauthenticated`",
6221
+ "content": {
6222
+ "application/json": {
6223
+ "schema": {
6224
+ "$ref": "#/components/schemas/Refusal"
6225
+ },
6226
+ "example": {
6227
+ "error": {
6228
+ "code": "unauthenticated",
6229
+ "message": "Unauthenticated.",
6230
+ "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90"
6231
+ }
6232
+ }
6233
+ }
6234
+ }
6235
+ },
6236
+ "403": {
6237
+ "description": "A token without the company ability — code `forbidden`",
6238
+ "content": {
6239
+ "application/json": {
6240
+ "schema": {
6241
+ "$ref": "#/components/schemas/Refusal"
6242
+ },
6243
+ "example": {
6244
+ "error": {
6245
+ "code": "forbidden",
6246
+ "message": "Forbidden.",
6247
+ "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90"
6248
+ }
6249
+ }
6250
+ }
6251
+ }
6252
+ },
6253
+ "404": {
6254
+ "description": "No such row in the calling company — code `not_found`",
6255
+ "content": {
6256
+ "application/json": {
6257
+ "schema": {
6258
+ "$ref": "#/components/schemas/Refusal"
6259
+ },
6260
+ "example": {
6261
+ "error": {
6262
+ "code": "not_found",
6263
+ "message": "No such resource in this company.",
6264
+ "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90"
6265
+ }
6266
+ }
6267
+ }
6268
+ }
6269
+ },
6270
+ "422": {
6271
+ "description": "Refused; `error.code` says why and `error.errors` names the fields",
6272
+ "content": {
6273
+ "application/json": {
6274
+ "schema": {
6275
+ "$ref": "#/components/schemas/ValidationRefusal"
6276
+ },
6277
+ "example": {
6278
+ "error": {
6279
+ "code": "validation_failed",
6280
+ "message": "The per page must be at least 1.",
6281
+ "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90",
6282
+ "errors": {
6283
+ "per_page": [
6284
+ "The per page must be at least 1."
6285
+ ]
6286
+ }
6287
+ }
6288
+ }
6289
+ }
6290
+ }
6291
+ },
6292
+ "429": {
6293
+ "description": "Rate limited; `Retry-After` says when — code `rate_limited`",
6294
+ "content": {
6295
+ "application/json": {
6296
+ "schema": {
6297
+ "$ref": "#/components/schemas/Refusal"
6298
+ },
6299
+ "example": {
6300
+ "error": {
6301
+ "code": "rate_limited",
6302
+ "message": "Too Many Attempts.",
6303
+ "request_id": "9f2b1c7a-4d3e-4b8a-9f10-2c5d6e7a8b90"
6304
+ }
6305
+ }
6306
+ }
6307
+ }
6308
+ }
6309
+ },
6310
+ "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`."
6311
+ }
6312
+ },
5572
6313
  "/company/v3/positions": {
5573
6314
  "get": {
5574
6315
  "operationId": "company.v3.positions.index",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "clockster"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Official Python SDK for the Clockster Company API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -52,6 +52,73 @@ class PayrollPayslips(_Namespace):
52
52
  )
53
53
 
54
54
 
55
+ class PayrollSingleAdjustments(_Namespace):
56
+ """`clockster.payroll.single_adjustments`."""
57
+
58
+ def create(self, body: PayrollSingleAdjustmentsCreateBody, *, idempotency_key: str | None = None) -> PayrollSingleAdjustmentsCreateResponse:
59
+ """Create single adjustments
60
+
61
+ Up to 100 one-off amounts — a bonus, a service charge, a penalty — each for one person
62
+ and one day. All or nothing: a `422` means none of the batch landed.
63
+
64
+ **An adjustment is read when a payslip is calculated.** A `draft` payslip whose period
65
+ holds `date` takes it in on its next calculation. An `approved` or `paid` one does not:
66
+ it is recalculated only by hand in the web application, and nothing here tells you
67
+ whether that happened. Check the payslip's status for the month before filing into it.
68
+
69
+ **Send an `Idempotency-Key`.** An adjustment carries no key of yours, so a retry after a
70
+ timeout files it a second time unless the header says it is the same attempt. The
71
+ answer lists what was created, in the order sent.
72
+ """
73
+ return cast(
74
+ PayrollSingleAdjustmentsCreateResponse,
75
+ self._transport.request(
76
+ "POST",
77
+ "/company/v3/payroll/single-adjustments",
78
+ json=body,
79
+ idempotency_key=idempotency_key,
80
+ ),
81
+ )
82
+
83
+ def delete(self, id: int) -> PayrollSingleAdjustmentsDeleteResponse:
84
+ """Delete a single adjustment
85
+
86
+ Deletes the adjustment. A payslip already calculated with it keeps the amount until it
87
+ is calculated again — for an `approved` or `paid` one, only by hand in the web
88
+ application.
89
+
90
+ Another company's id is a `404`.
91
+ """
92
+ return cast(
93
+ PayrollSingleAdjustmentsDeleteResponse,
94
+ self._transport.request(
95
+ "DELETE",
96
+ f"/company/v3/payroll/single-adjustments/{id}",
97
+ ),
98
+ )
99
+
100
+ def list(self, *, per_page: int | None = None, cursor: str | None = None, users: list[int] | None = None, types: list[PayrollSingleAdjustmentsType] | None = None, date_from: str | None = None, date_to: str | None = None) -> PayrollSingleAdjustmentsListResponse:
101
+ """List single adjustments
102
+
103
+ One-off additions and deductions, oldest first, with who they are for and the day they
104
+ are dated.
105
+
106
+ `amount` is never negative: `type` says whether it is added or taken off, and whether
107
+ before or after tax. `date_from` and `date_to` bound the day, inclusive.
108
+
109
+ Rows filed in the web application are listed too, and may carry `13th_pay`, which is
110
+ computed there rather than filed here.
111
+ """
112
+ return cast(
113
+ PayrollSingleAdjustmentsListResponse,
114
+ self._transport.request(
115
+ "GET",
116
+ "/company/v3/payroll/single-adjustments",
117
+ query={"per_page": per_page, "cursor": cursor, "users": users, "types": types, "date_from": date_from, "date_to": date_to},
118
+ ),
119
+ )
120
+
121
+
55
122
  class WebhooksDeliveries(_Namespace):
56
123
  """`clockster.webhooks.deliveries`."""
57
124
 
@@ -509,6 +576,7 @@ class Payroll(_Namespace):
509
576
  def __init__(self, transport: _SyncTransport) -> None:
510
577
  super().__init__(transport)
511
578
  self.payslips = PayrollPayslips(transport)
579
+ self.single_adjustments = PayrollSingleAdjustments(transport)
512
580
 
513
581
 
514
582
  class Positions(_Namespace):
@@ -1256,6 +1324,73 @@ class AsyncPayrollPayslips(_AsyncNamespace):
1256
1324
  )
1257
1325
 
1258
1326
 
1327
+ class AsyncPayrollSingleAdjustments(_AsyncNamespace):
1328
+ """`clockster.payroll.single_adjustments`."""
1329
+
1330
+ async def create(self, body: PayrollSingleAdjustmentsCreateBody, *, idempotency_key: str | None = None) -> PayrollSingleAdjustmentsCreateResponse:
1331
+ """Create single adjustments
1332
+
1333
+ Up to 100 one-off amounts — a bonus, a service charge, a penalty — each for one person
1334
+ and one day. All or nothing: a `422` means none of the batch landed.
1335
+
1336
+ **An adjustment is read when a payslip is calculated.** A `draft` payslip whose period
1337
+ holds `date` takes it in on its next calculation. An `approved` or `paid` one does not:
1338
+ it is recalculated only by hand in the web application, and nothing here tells you
1339
+ whether that happened. Check the payslip's status for the month before filing into it.
1340
+
1341
+ **Send an `Idempotency-Key`.** An adjustment carries no key of yours, so a retry after a
1342
+ timeout files it a second time unless the header says it is the same attempt. The
1343
+ answer lists what was created, in the order sent.
1344
+ """
1345
+ return cast(
1346
+ PayrollSingleAdjustmentsCreateResponse,
1347
+ await self._transport.request(
1348
+ "POST",
1349
+ "/company/v3/payroll/single-adjustments",
1350
+ json=body,
1351
+ idempotency_key=idempotency_key,
1352
+ ),
1353
+ )
1354
+
1355
+ async def delete(self, id: int) -> PayrollSingleAdjustmentsDeleteResponse:
1356
+ """Delete a single adjustment
1357
+
1358
+ Deletes the adjustment. A payslip already calculated with it keeps the amount until it
1359
+ is calculated again — for an `approved` or `paid` one, only by hand in the web
1360
+ application.
1361
+
1362
+ Another company's id is a `404`.
1363
+ """
1364
+ return cast(
1365
+ PayrollSingleAdjustmentsDeleteResponse,
1366
+ await self._transport.request(
1367
+ "DELETE",
1368
+ f"/company/v3/payroll/single-adjustments/{id}",
1369
+ ),
1370
+ )
1371
+
1372
+ async def list(self, *, per_page: int | None = None, cursor: str | None = None, users: list[int] | None = None, types: list[PayrollSingleAdjustmentsType] | None = None, date_from: str | None = None, date_to: str | None = None) -> PayrollSingleAdjustmentsListResponse:
1373
+ """List single adjustments
1374
+
1375
+ One-off additions and deductions, oldest first, with who they are for and the day they
1376
+ are dated.
1377
+
1378
+ `amount` is never negative: `type` says whether it is added or taken off, and whether
1379
+ before or after tax. `date_from` and `date_to` bound the day, inclusive.
1380
+
1381
+ Rows filed in the web application are listed too, and may carry `13th_pay`, which is
1382
+ computed there rather than filed here.
1383
+ """
1384
+ return cast(
1385
+ PayrollSingleAdjustmentsListResponse,
1386
+ await self._transport.request(
1387
+ "GET",
1388
+ "/company/v3/payroll/single-adjustments",
1389
+ query={"per_page": per_page, "cursor": cursor, "users": users, "types": types, "date_from": date_from, "date_to": date_to},
1390
+ ),
1391
+ )
1392
+
1393
+
1259
1394
  class AsyncWebhooksDeliveries(_AsyncNamespace):
1260
1395
  """`clockster.webhooks.deliveries`."""
1261
1396
 
@@ -1713,6 +1848,7 @@ class AsyncPayroll(_AsyncNamespace):
1713
1848
  def __init__(self, transport: _AsyncTransport) -> None:
1714
1849
  super().__init__(transport)
1715
1850
  self.payslips = AsyncPayrollPayslips(transport)
1851
+ self.single_adjustments = AsyncPayrollSingleAdjustments(transport)
1716
1852
 
1717
1853
 
1718
1854
  class AsyncPositions(_AsyncNamespace):
@@ -83,6 +83,16 @@ __all__ = [
83
83
  "PayrollPayslipsListRowSalary",
84
84
  "PayrollPayslipsListRowUser",
85
85
  "PayrollPayslipsStatus",
86
+ "PayrollSingleAdjustmentsCreateAdjustment",
87
+ "PayrollSingleAdjustmentsCreateBody",
88
+ "PayrollSingleAdjustmentsCreateResponse",
89
+ "PayrollSingleAdjustmentsCreateRow",
90
+ "PayrollSingleAdjustmentsCreateRowUser",
91
+ "PayrollSingleAdjustmentsDeleteResponse",
92
+ "PayrollSingleAdjustmentsListResponse",
93
+ "PayrollSingleAdjustmentsListRow",
94
+ "PayrollSingleAdjustmentsListRowUser",
95
+ "PayrollSingleAdjustmentsType",
86
96
  "PositionsDeleteResponse",
87
97
  "PositionsGetData",
88
98
  "PositionsGetResponse",
@@ -239,6 +249,8 @@ LocationsInclude = Literal["managers"]
239
249
 
240
250
  PayrollPayslipsStatus = Literal["draft", "approved", "paid"]
241
251
 
252
+ PayrollSingleAdjustmentsType = Literal["service_charge", "single_addition_pre_tax", "single_addition_post_tax", "single_loan", "single_deduction_pre_tax", "single_deduction_post_tax"]
253
+
242
254
  SchedulesLeaveType = Literal["annual", "unpaid", "sick", "unpaid_sick", "maternity", "paternity", "special", "day_off", "compensatory", "personal", "emergency", "unexcused_absence"]
243
255
 
244
256
  SchedulesType = Literal["work", "free", "leave"]
@@ -798,6 +810,60 @@ class PayrollPayslipsListMeta(TypedDict):
798
810
  next_cursor: str | None
799
811
  prev_cursor: str | None
800
812
 
813
+ class PayrollSingleAdjustmentsListResponse(TypedDict):
814
+ data: list[PayrollSingleAdjustmentsListRow]
815
+ links: PageLinks
816
+ meta: PageMeta
817
+
818
+ class PayrollSingleAdjustmentsListRow(TypedDict):
819
+ id: int
820
+ user: PayrollSingleAdjustmentsListRowUser
821
+ type: str
822
+ title: str | None
823
+ amount: float | None
824
+ date: str
825
+ created_at: str
826
+
827
+ class PayrollSingleAdjustmentsListRowUser(TypedDict):
828
+ id: int
829
+ external_id: str | None
830
+
831
+ class PayrollSingleAdjustmentsCreateResponse(TypedDict):
832
+ data: list[PayrollSingleAdjustmentsCreateRow]
833
+
834
+ class PayrollSingleAdjustmentsCreateRow(TypedDict):
835
+ id: int
836
+ user: PayrollSingleAdjustmentsCreateRowUser
837
+ type: str
838
+ title: str | None
839
+ amount: float | None
840
+ date: str
841
+ created_at: str
842
+
843
+ class PayrollSingleAdjustmentsCreateRowUser(TypedDict):
844
+ id: int
845
+ external_id: str | None
846
+
847
+ class PayrollSingleAdjustmentsCreateBody(TypedDict):
848
+ # The adjustments to file, up to 100 a call.
849
+ adjustments: list[PayrollSingleAdjustmentsCreateAdjustment]
850
+
851
+ class PayrollSingleAdjustmentsCreateAdjustment(TypedDict):
852
+ # The employee this belongs to, by the id this API issued.
853
+ user_id: int
854
+ # What the amount does: an addition or a deduction, before or after tax, a service charge or a
855
+ # one-off loan.
856
+ type: PayrollSingleAdjustmentsType
857
+ # How much, never negative — `type` says which way it goes. At most two decimal places.
858
+ amount: float
859
+ # The day it is dated, `YYYY-MM-DD`. The payslip whose period holds this day takes it in.
860
+ date: str
861
+ # The name this is shown under.
862
+ title: NotRequired[str | None]
863
+
864
+ class PayrollSingleAdjustmentsDeleteResponse(TypedDict):
865
+ data: DeleteOutcome
866
+
801
867
  class PositionsListResponse(TypedDict):
802
868
  data: list[PositionsListRow]
803
869
  links: PageLinks
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes