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, synchronous Python SDK 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. - 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())
The SDK's only credential sources are the api_key argument (string or
provider) and the CLOCKIFY_API_KEY environment variable as its sole
fallback. 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).
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)
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_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.0.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.0.0.tar.gz | 108.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| clockify_unofficial_sdk-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 142.0 kB
Release files / clockify_unofficial_sdk-1.0.0.tar.gz
| Download URL | clockify_unofficial_sdk-1.0.0.tar.gz |
|---|---|
| Size | 108.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0ccf969f5eff843409ad18f55d441c9739324a9c79b3abc7081f5212fcb50fde
|
|
BLAKE2b-256 checksum How to use checksums |
a81c04337f4921087778c8d412bdf3bffa2783c6eeeade5d9db6fa8d2828b551
|
| 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 Sep 29, 2026.
Transparency logRelease files / clockify_unofficial_sdk-1.0.0-py3-none-any.whl
| Download URL | clockify_unofficial_sdk-1.0.0-py3-none-any.whl |
|---|---|
| Size | 33.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b5874b736a0c159eed74290f25f2f4b7834d0071bf8b0441a45e1d5b28a2cbf1
|
|
BLAKE2b-256 checksum How to use checksums |
c36e642af59b733581f0b19c4cf3be844e673c1c3a4aa403aa462d1660728364
|
| 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 Sep 29, 2026.
Transparency log