Clockify Unofficial SDK
Unofficial. This project is not affiliated with, endorsed by, or supported by CAKE.com d.o.o. "Clockify" is a trademark of CAKE.com d.o.o. Use at your own risk against the Clockify API.
Typed Python SDK, sync and async, for the Clockify Working API and Reports API.
Every response is a validated, frozen pydantic
model with attribute access and real Python types — not a raw dict.
Table of contents
- About
- Key features
- Architecture
- Getting started
- Usage
- Configuration
- Development
- Platform notes
- Contributing
- Security
- License
About
The Clockify REST API returns plain JSON, which leaves every caller to
hand-roll response parsing, pagination, retries, and error mapping. This SDK
does that once for the Working API: validated models, a lazy pagination
iterator, retry with backoff, and a typed exception hierarchy. Reports API
support is not implemented yet — see
docs/sdk/coverage.md.
Key features
- Typed models everywhere. Requests and responses are pydantic models with camelCase↔snake_case aliasing, not raw dicts.
- Workspace-bound sub-clients.
client.workspace(id)returns aWorkspaceClientthat carries the workspace ID, removing it from every method call. - Lazy pagination.
list()returns an iterator that follows Clockify's offset pagination;list_page()exposes one page at a time for manual cursor control. - Built-in retry.
429and5xxresponses retry with jittered backoff, honoringRetry-Afterand skipping retries on non-idempotent commands. - Sync and async.
ClockifyClientuseshttpx.Client;AsyncClockifyClientexposes the same resources onhttpx.AsyncClient. - Typed exception hierarchy. HTTP failures map to specific
clockify.errorsexceptions (AuthenticationError,NotFoundError,RateLimitError, and others) instead of a generic HTTP exception. - Multi-region support.
Region.GLOBALand the regional Clockify hosts are built in; an explicitbase_urloverrides either.
Architecture
ClockifyClient owns one httpx.Client and exposes root-level namespaces
plus workspace(id) / default_workspace(), which return a WorkspaceClient
bound to a workspace. See docs/sdk/index.md for
the full layering (with diagrams) and how to add a new endpoint.
docs/sdk/coverage.md is the authoritative
endpoint-to-method coverage matrix.
Getting started
Prerequisites
- Python 3.14 or later
- A Clockify API key (
CLOCKIFY_API_KEY) — see Authentication
Contributing to the SDK itself additionally requires mise — see Development.
Installation
uv add clockify-unofficial-sdk
# or
pip install clockify-unofficial-sdk
To track an unreleased commit instead, use a uv git dependency:
uv add "clockify-unofficial-sdk @ git+https://github.com/gajaguar/clockify-sdk"
Usage
Authentication
import os
from clockify import ClockifyClient
os.environ["CLOCKIFY_API_KEY"] = "..."
client = ClockifyClient() # reads CLOCKIFY_API_KEY
client = ClockifyClient(api_key="...") # explicit argument takes precedence
# api_key also accepts a zero-argument callable, invoked lazily on every request
# instead of once at construction time — useful for a rotating or externally-managed
# key (e.g. one read from an OS keyring by the calling application).
client = ClockifyClient(api_key=lambda: keychain.current_clockify_key())
An add-on authenticates with the token Clockify issues to it instead of an
API key. The SDK then sends X-Addon-Token in place of X-Api-Key:
client = ClockifyClient(addon_token="...")
client = ClockifyClient() # reads CLOCKIFY_ADDON_TOKEN if no API key is set
client = ClockifyClient(addon_token=lambda: store.current_token())
An API key and an add-on token are mutually exclusive: passing both
arguments, or setting both environment variables with no argument, raises a
ConfigurationError. An explicit argument beats the other credential's
environment variable. Add-on tokens are rate limited to 50 requests per
second per workspace, and an add-on usually sets
options=ClientOptions(base_url=...) to the backend URL Clockify gives it.
The SDK's only credential sources are the api_key and addon_token
arguments (string or provider) and, as their sole fallbacks, the
CLOCKIFY_API_KEY and CLOCKIFY_ADDON_TOKEN environment variables. It has
no OS keyring/keychain integration, no 1Password or other password-manager
support, no OAuth/SSO flow, and no interactive prompts — that is an
application-level concern for whatever consumes this SDK (see
clockify-cli for an example
that layers all of that on top).
Getting a credential
- Open your profile menu and choose Profile settings.
- Open the Advanced tab and choose Manage API keys.
- Choose Generate new and give the key a name.
- Copy the key. Clockify does not show it again once you leave the page.
Any user can generate keys for their own account, and a key can be renamed or deleted from the same page. Clockify has no OAuth flow. An add-on token comes from Clockify when the add-on is installed in a workspace.
Recipes
Start a timer, list today's running/finished entries, then stop it:
import datetime
from clockify import ClockifyClient, TimeEntryCreate, TimeEntryFilter
with ClockifyClient() as client:
workspace = client.default_workspace()
user = client.user.me()
payload = TimeEntryCreate(description="Writing docs")
started = workspace.time_entries.start(user.id, payload)
print(f"started {started.id}")
today = datetime.datetime.now(datetime.UTC)
today = today.replace(hour=0, minute=0, second=0, microsecond=0)
entry_filter = TimeEntryFilter(start=today)
for entry in workspace.time_entries.list(user.id, entry_filter=entry_filter):
print(entry.description, entry.time_interval.duration)
stopped = workspace.time_entries.stop(user.id)
print(stopped.time_interval.duration)
Use the async client with async with and await:
import asyncio
from clockify import AsyncClockifyClient
async def main() -> None:
async with AsyncClockifyClient() as client:
workspace = await client.default_workspace()
async for project in workspace.projects.list():
print(project.name)
asyncio.run(main())
A credential provider runs inside the event loop, so it must not block, and
event_hooks passed to the async client must be coroutine functions. See
docs/sdk/async-client.md.
Handle API errors with the typed exception hierarchy:
from clockify.errors import NotFoundError, RateLimitError
try:
project = workspace.projects.get("does-not-exist")
except NotFoundError:
print("project not found")
except RateLimitError:
print("rate limited even after built-in retries")
Manage pagination directly instead of iterating the full collection:
page = workspace.projects.list_page(page=1, page_size=50)
print(len(page.items), page.items)
Configuration
| Variable | Where it's read | Default | Description |
|---|---|---|---|
CLOCKIFY_API_KEY |
ClockifyClient() |
none, required | API key used when api_key is not passed explicitly. |
CLOCKIFY_ADDON_TOKEN |
ClockifyClient() |
none | Add-on token used when neither credential is passed. |
CLOCKIFY_TEST_API_KEY |
tests/live suite (-m live) |
none | Enables the live smoke tests against a real workspace. |
CLOCKIFY_TEST_WORKSPACE_ID |
tests/live suite (-m live) |
none | Workspace the live smoke tests run against. |
ClockifyClient(options=ClientOptions(...)) accepts:
| Parameter | Type | Default | Description |
|---|---|---|---|
region |
Region |
Region.GLOBAL |
Selects the Working/Reports API hosts. See Region below. |
base_url |
str | None |
region default | Overrides the Working API host. |
reports_base_url |
str | None |
region default | Overrides the Reports API host. |
timeout |
float |
30.0 |
httpx request timeout in seconds. |
retry |
RetryPolicy | None |
see below | Retry behavior for 429/5xx responses. |
event_hooks |
dict[str, list[Callable]] | None |
None |
Passed through to httpx.Client. |
RetryPolicy defaults: max_attempts=3, backoff_base=0.5,
max_backoff=30.0, retry_statuses={429, 500, 502, 503, 504},
respect_retry_after=True.
Region members: GLOBAL, EU_CENTRAL_1, US_EAST_2, EU_WEST_2,
AP_SOUTHEAST_2, DEVELOPER. Only the GLOBAL hosts are verified against a
live account — the others are transcribed from Clockify's docs; pass an
explicit base_url/reports_base_url if one turns out to be wrong.
Development
make install # sync Python deps, install node tooling, install pre-commit hook
make check # read-only gate: lint, format-check, mypy, pyright,
# md-lint, spell, pylint, commits-check
make fix # apply safe auto-fixes (format, ruff --fix, markdownlint --fix)
make test # run the test suite
make build # build the sdist and wheel into dist/
Run make help for the full target list; every target accepts
FILES="..." to scope to specific paths/globs.
Toolchain
- uv — dependency management and virtualenvs (
hatchlingbuild backend) - httpx — HTTP transport
- pydantic — request/response models and validation
- ruff — linting and formatting (
lint.select = ["ALL"], curated ignores) - mypy + pyright — static type checking
- pytest + pytest-cov + respx — testing, coverage, HTTP mocking
- markdownlint-cli2 + cspell (pnpm, dev-only) — Markdown lint & spell check
- checkmake — lints the
Makefileitself (make makefile-lint) - pylint + a custom checker plugin (a
uvgit dependency pinned inpyproject.toml) — custom checkers for personal-preference rules ruff doesn't cover (e.g. no docstrings — see below) - pre-commit — git hook running the generic hygiene hooks (trailing-whitespace, end-of-file-fixer, check-yaml, check-toml, check-merge-conflict, check-added-large-files, mixed-line-ending), markdownlint-cli2, cspell, checkmake, ruff, ruff-format, mypy, and pylint before each commit
- mise — pins the whole toolchain version (Python, uv, node, pnpm,
pre-commit, checkmake) in
mise.toml - conventional-git — validates commit messages and branch names against
Conventional Commits/Conventional Branch (
make commits-check)
Endpoint comments
Docstrings are forbidden (see AGENTS.md), so each resource
method carries a one-line # METHOD /path comment above its definition, and
the authoritative endpoint-to-method mapping lives in
docs/sdk/coverage.md.
Project layout
.
├── pyproject.toml # deps, ruff/mypy/pyright/pytest/coverage config
├── Makefile # check/fix command surface (root: shared targets)
├── mk/python.mk # Python-specific targets, wired into the Makefile
├── mise.toml # pinned toolchain versions
├── docs/ # OKF bundle: SDK architecture, endpoint coverage, conventions
├── src/clockify/ # SDK package
└── tests/
├── unit/ # respx-backed, offline, deterministic
└── live/ # marker-gated (`-m live`), hits a real workspace
Platform notes
- CI (
.github/workflows/ci.yml,python.yml) runsmake check && make teston every push and pull request againstmain.make check && make testis still the gate to run locally before every commit — a local--no-verifybypass of the pre-commit hook is the case CI exists to catch. requires-python = ">=3.14"excludes most current Python installations (3.11–3.13); this is a deliberate, revisitable floor, accepted to keep modern-syntax features (PEP 695 generics,StrEnum,datetime.UTC) available now and lowered later without a breaking change.mise.toml's[env]forcesuvonto the mise-provided interpreter (UV_PYTHON_PREFERENCE = "only-system",UV_PYTHON_DOWNLOADS = "never"), so.python-versionis intentionally absent — mise is the single source of truth for the pinned Python version.- Clockify rate limits differ by plan and are not fully documented upstream;
retry defaults are conservative and configurable — see
src/clockify/retry.py.
Contributing
See CONTRIBUTING.md.
Security
Report suspected vulnerabilities through GitHub's private vulnerability reporting, or email dev@gajaguar.com, instead of opening a public issue. A Clockify API key grants full access to a workspace — never commit one, and rotate immediately if one is exposed.
License
Distributed under the MIT License. See LICENSE for details.
Metadata
Release files for clockify-unofficial-sdk 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| clockify_unofficial_sdk-1.1.0.tar.gz | 123.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| clockify_unofficial_sdk-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 159.5 kB
Release files / clockify_unofficial_sdk-1.1.0.tar.gz
| Download URL | clockify_unofficial_sdk-1.1.0.tar.gz |
|---|---|
| Size | 123.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5c1222f0e0fe035903f05207fab32d1f3196a943643029b5875ec1d542c380eb
|
|
BLAKE2b-256 checksum How to use checksums |
0fa347f8ce077fad5f7870011d686420d49bc0ce3da48ea3880783f8c343c344
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / clockify_unofficial_sdk-1.1.0-py3-none-any.whl
| Download URL | clockify_unofficial_sdk-1.1.0-py3-none-any.whl |
|---|---|
| Size | 36.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fdaf60697544452893f0450b9497938210d4b49c3058787270fdfb26d166c847
|
|
BLAKE2b-256 checksum How to use checksums |
9dda8d12f34891485192265aadad4fa846acd3d1a2b5337ea599b02ad5f4b8e1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency log