# Uku API v3 > Task management and billing API for service companies. ## Authentication Every request requires two headers: - `X-Uku-Company`: Your company UUID (get from Settings → API) - `X-API-Key`: Your API key (format: `uku_live_...`) An API key grants full read access to all of your company's data and is bound only by its write scope — protect it like a password. ## Base URL `https://v4.getuku.com/api/v3` ## Scopes Each API key is granted one tier: - `read` — read-only (GET). - `write` — all writes except financial fields. - `admin` — everything, including financial fields (invoice/contract money, product/tax pricing, IBAN/VAT, flextime, reclaims) and key/webhook management. `admin` includes `write`, which includes `read`. Endpoint notes mark where `admin` is required. Financial resources (invoices, contracts, agreements, invoice-sellers, monitors, taxes, budgets, flextime, reclaims, product prices) require the financials tier (`admin` today) for READS as well as writes (2026-07-15). A 403 reports the required tier, e.g. `Required: write`. ### Personal keys (`kind: 'personal'`) Every key so far has been an **integration key** — company-wide, scope-only, the same for every request regardless of who's calling. `POST /auth/keys` also supports `kind: 'personal'` (2026-07-15): a key that acts AS one specific person, scoped to exactly that person's own backoffice visibility (which clients/tasks/time-entries they can see) and write rights (which they can create/edit/delete) — identical to what that person would see and do logged into the backoffice UI. Built for AI-assistant/MCP use (Claude Desktop, Claude Code) where the acting identity should be a real team member, not the whole company. A personal key's scopes are **not chosen by the caller** — they are always fixed to `read` + `write`, never `admin`, never `financials`. That means a personal key **always** gets `403 FINANCIAL_SCOPE_REQUIRED` on money resources (invoices, contracts, agreements, invoice-sellers, monitors, taxes, budgets, reclaims, flextime) — even though it carries `write` — regardless of the acting person's own financial rights in the backoffice. This is the intended v1 boundary: a personal key's financial blast radius is nil. Only an `admin`-scoped key may create a personal key (for itself or, self-issuance aside, for another active company member); a personal key can never call any `/auth/keys` endpoint itself, including to manage its own siblings. ## Endpoints ### Clients - `GET /api/v3/clients` — List clients (paginated) - `GET /api/v3/clients/{id}` — Get client - `POST /api/v3/clients` — Create client (scope: write) - `PATCH /api/v3/clients/{id}` — Update client (scope: write) - `DELETE /api/v3/clients/{id}` — Soft-delete client (scope: write) - `POST /api/v3/clients/{id}/documents-folder` — Provision (or return existing) the client's cloud-drive documents folder (scope: write; 409 `DRIVE_NOT_CONNECTED` if no drive is connected, 400 `FOLDER_PROVISION_FAILED` on provider error) ### Tasks - `GET /api/v3/tasks` — List tasks (paginated) - `GET /api/v3/tasks/{id}` — Get task - `POST /api/v3/tasks` — Create task (scope: write) - `PATCH /api/v3/tasks/{id}` — Update task (scope: write) - `DELETE /api/v3/tasks/{id}` — Soft-delete task (scope: write) ### Time Entries - `GET /api/v3/time-entries` — List time entries (paginated) - `GET /api/v3/time-entries/{id}` — Get time entry - `POST /api/v3/time-entries` — Create time entry (scope: write) - `PATCH /api/v3/time-entries/{id}` — Update time entry (scope: write) - `DELETE /api/v3/time-entries/{id}` — Soft-delete time entry (scope: write) ### Contacts - `GET /api/v3/contacts` — List contacts (paginated) - `GET /api/v3/contacts/{id}` — Get contact - `POST /api/v3/contacts` — Create contact (scope: write) - `PATCH /api/v3/contacts/{id}` — Update contact (scope: write) - `DELETE /api/v3/contacts/{id}` — Soft-delete contact (scope: write) ### Invoices - `GET /api/v3/invoices` — List invoices (paginated) - `GET /api/v3/invoices/{id}` — Get invoice ### Members - `PATCH /api/v3/members/{id}` — Update member (scope: write; credit fields need admin) - `GET /api/v3/members` — List team members (paginated) - `GET /api/v3/members/{id}` — Get member - `flextime_credit`/`vacation_credit` read as `null` on both GETs unless the key has `financials` (2026-07-15) — the rest of the member row stays open on `read`. ### Invoices (write) - `POST /api/v3/invoices` — Create draft invoice (scope: admin) - `PATCH /api/v3/invoices/{id}` — Update invoice header (scope: admin + `If-Match`) - `POST /api/v3/invoices/{id}/mark-paid` — Record payment (scope: admin + `If-Match`; 409 `INVOICE_NOT_PAYABLE` unless `created`/`sent`) - `POST /api/v3/invoices/{id}/send` — Queue for delivery, async (scope: admin + `If-Match`; 202 Accepted; 409 `INVOICE_NOT_SENDABLE` unless `created`/`sent`/`paid`) - Row CRUD: `GET/POST /api/v3/invoices/{id}/rows`, `GET/PATCH/DELETE /api/v3/invoices/{id}/rows/{row_id}` ### Products - `GET /api/v3/products` — List products (paginated) - `GET /api/v3/products/{id}` — Get product - `POST /api/v3/products` — Create product (scope: admin) - `PATCH /api/v3/products/{id}` — Update product (scope: write; money fields need admin) - `DELETE /api/v3/products/{id}` — Soft-delete product (scope: admin). Refused with `409 PRODUCT_IN_USE` while a contract or invoice row still references the product. - Price CRUD: `GET/POST /api/v3/products/{id}/prices`, `GET/PATCH/DELETE /api/v3/products/{id}/prices/{price_id}` (money fields need admin) - On `GET /api/v3/products[/{id}]`, the money-bearing fields (`tax_id`, `tax_rate`, `tax_name`, `pricelist_type`, `billable_filter`, `bill_by`, `unit`, `article_id`, `resource_id`, `billing_project_id`) read as `null`, and the embedded `prices` reads as `[]`, unless the key has `financials` (2026-07-15) — the rest of the product row stays open on `read`. ### Topics - `GET /api/v3/topics` — List topics - `GET /api/v3/topics/{id}` — Get topic - `POST /api/v3/topics` — Create topic (scope: write) - `PATCH /api/v3/topics/{id}` — Update topic (scope: write) - `DELETE /api/v3/topics/{id}` — Soft-delete topic (scope: write) ### Contracts - `GET /api/v3/contracts` — List contracts (paginated) - `GET /api/v3/contracts/{id}` — Get contract - `POST /api/v3/contracts` — Create contract (scope: admin) - `PATCH /api/v3/contracts/{id}` — Update contract (scope: write + `If-Match`; admin for money fields) - `DELETE /api/v3/contracts/{id}` — Soft-delete contract (scope: admin + `If-Match`; active/finished must move to `pending` first) - Row CRUD: `GET/POST /api/v3/contracts/{id}/rows`, `GET/PATCH/DELETE /api/v3/contracts/{id}/rows/{row_id}` (row writes need scope admin + `If-Match`) ### Client Groups - `GET /api/v3/client-groups` — List client groups - `GET /api/v3/client-groups/{id}` — Get client group - `POST /api/v3/client-groups` — Create (scope: write) - `PATCH /api/v3/client-groups/{id}` — Update - `DELETE /api/v3/client-groups/{id}` — Soft-delete. Refused with `409 GROUP_IN_USE` while the group still has active members. ### Invoice Sellers - `GET /api/v3/invoice-sellers` — List invoice seller profiles - `GET /api/v3/invoice-sellers/{id}` — Get invoice seller - `PATCH /api/v3/invoice-sellers/{id}` — Update (scope: write; most profile fields — `name`, `address`, `company_number`, `vat_number`, `bank_1/2`, `iban_1/2` — need admin; only `status`/`logo_url` are write-tier) ### Agreements - `GET /api/v3/agreements`, `GET /api/v3/agreements/{id}` — company-wide agreements - `POST /api/v3/agreements`, `PATCH/DELETE /api/v3/agreements/{id}` — mutate (scope: admin; financial). `PATCH`/`DELETE` require `If-Match`; `POST` (create) does not. Create requires `date_from`; `cost_type` is "hour" (default) or "month". - Per member: `GET/POST /api/v3/members/{id}/agreements`, `GET/PATCH/DELETE /api/v3/members/{id}/agreements/{agreement_id}` - Both list endpoints accept `date_from` / `date_until` filters. ## Preview resources (schema may change) Preview endpoints are always available; their responses include `X-Api-Preview: true`. Schemas may change across releases. Stable GA promotion is announced per-resource in the changelog. ### Projects (preview) - `GET /api/v3/projects` (list filters: `status`, `client_id`, `manager_id`, `project_type_id`, `q`), `GET /api/v3/projects/{id}` - `POST/PATCH/DELETE /api/v3/projects[/{id}]` (scope: write) - Create requires `project_type_id`. Status is changed via `status_action` on PATCH (`activate`/`cancel`/`archive`/`unarchive`), not by writing a raw status. Also carries `status_text` and `event_date`. ### Monitors (preview, threshold writes need `admin`) - `GET /api/v3/monitors`, `GET /api/v3/monitors/{id}` — list filters: `client_id`, `type`, `role_person_id`, `frequency`, `active` - `POST /api/v3/monitors` (scope: admin — `value`/`range_value` are money) - `PATCH /api/v3/monitors/{id}` (scope: write; changing `value`/`range_value` needs admin + `If-Match`) - `DELETE /api/v3/monitors/{id}` (scope: admin + `If-Match`) - Nested: `GET/POST /api/v3/clients/{id}/monitors` - Overlap detection: `MONITOR_OVERLAP` in the response body `warnings[]` on writes (warning, not blocking; the legacy `X-Uku-Warning: MONITOR_OVERLAP: ` header is also still sent for one deprecation cycle) ### Suppliers + supplier types (preview) - `GET/POST/PATCH/DELETE /api/v3/suppliers[/{id}]` (scope: write) - `GET/PATCH /api/v3/supplier-types[/{id}]` (scope: write) ### Taxes (preview) - `GET /api/v3/taxes`, `GET /api/v3/taxes/{id}` - `PATCH /api/v3/taxes/{id}` (scope: write; `rate`/`percent` need admin) ### Notes (preview) - `GET/POST/PATCH/DELETE /api/v3/notes[/{id}]` (scope: write) - List filters: `related_type` (client/project/supplier/account_member), `related_id`, `type` (info/document/email/billing), `is_pinned` ### Custom fields (preview) - `GET /api/v3/custom-fields` (filters: `entity_type`, `status`, `is_system`, `q`), `GET /api/v3/custom-fields/{id}` - `POST/PATCH/DELETE /api/v3/custom-fields[/{id}]` (scope: write) - System fields are read-only. `name`/`field_type`/`entity_type` are immutable post-create. - Removing an option that's in use returns `409 CUSTOM_FIELD_OPTION_IN_USE`. ### Product fields (preview) — article/resource/cost-center/billing-project lookups - `GET/POST/PATCH/DELETE /api/v3/product-fields[/{id}]` (scope: write) ### Workflow roles (preview) — role slots in workflow templates - `GET/POST/PATCH/DELETE /api/v3/workflow-roles[/{id}]` (scope: write). Fields: `name`, `color`, `sort_order`. Offset-only; `limit` defaults to 100. DELETE is refused with `409 ROLE_IN_USE` while the role is still referenced by a client member or an active task. ### Roles (preview) — job titles / position names - `GET/POST/PATCH/DELETE /api/v3/roles[/{id}]` (scope: write). Field: `name`. Offset-only; `limit` defaults to 100. ### Content templates (preview) — email/document templates with per-locale rows - `GET/POST/PATCH/DELETE /api/v3/content-templates[/{id}]` (scope: write; list filters: `type`, `channel`, `q`) - Rows: `POST /api/v3/content-templates/{id}/rows` (upsert per locale), `PATCH/DELETE /api/v3/content-templates/{id}/rows/{row_id}` ### Budgets (preview) — project/client budgets with warning thresholds - `GET` (scope: financials), `POST` (scope: admin), `PATCH` (scope: write; `budget_net_sum`, `warning_rate`, `status` need admin), `DELETE` (scope: admin) — `/api/v3/budgets[/{id}]` ### Task relations (preview) — task dependencies - `GET /api/v3/task-relations?task_id=` — list dependencies for a task - `POST /api/v3/task-relations` body `{task_id, depends_on_id}` (scope: write) - `DELETE /api/v3/task-relations/{id}` — remove dependency ### Flextime (preview) — member flextime balance - `GET` (scope: financials), `POST` (scope: admin), `PATCH` (scope: write; `duration` needs admin), `DELETE` (scope: admin) — `/api/v3/flextime[/{id}]` ### Client members (preview) — team member assignments to clients - `GET/POST/PATCH/DELETE /api/v3/client-members[/{id}]` (scope: write; list filters: `client_id`, `person_id`, `role`) ### Attachments (preview) — file uploads on tasks and notes - `GET /api/v3/attachments` — metadata list (filters: `related_type` ∈ task/note, `related_id`, `q`) - `GET /api/v3/attachments/{id}` — metadata - `GET /api/v3/attachments/{id}/download` — streams file (fetched from cloud if needed; `409` while a fresh upload is still processing) - `POST /api/v3/tasks/{id}/attachments` — multipart upload to a task - `POST /api/v3/notes/{id}/attachments` — multipart upload to a note (surfaces on the note's client/project Files page) - Field name: `file`. Max 150MB → `413 FILE_TOO_LARGE`. MIME allowlist (images, PDF, Office, text, JSON, XML, ZIP, EML) → `415 UNSUPPORTED_MIME`. - Extension allowlist + a magic-byte content check: a blocked/unlisted extension → `415 BLOCKED_EXTENSION`/`415 UNSUPPORTED_EXTENSION`; content that doesn't match the declared type → `415 MIME_MISMATCH`. - `DELETE /api/v3/attachments/{id}` — soft delete (bytes retained for backup/restore) - Scope: `write:attachments` ### Calendar (preview) — vacation / time-off / flextime + global holiday reads - `GET /api/v3/calendar` — filter by `type`, `person_id`, `status`, `date_from`/`date_until`, `scope=company|global|all` - `GET /api/v3/calendar/{id}` — single entry - `POST /api/v3/calendar` — create vacation/flextime/sick_leave/time_off entry (scope: write) - `PATCH /api/v3/calendar/{id}` — update (only writable types; holidays are read-only) - `DELETE /api/v3/calendar/{id}` — soft delete - Approval of credit-bearing types (`vacation`/`flextime`) is backoffice-only: any create/update that would set — or unset, or type-flip while — `status: "approved"` returns `409 CALENDAR_APPROVAL_UI_ONLY`. Entries are created `in_review`; `sick_leave`/`time_off` are unaffected. ### Task automations (preview) — scheduled email/notification rules per task/client/project - `GET/POST/PATCH/DELETE /api/v3/task-automations[/{id}]` (scope: write) ### Delegations (preview) — vacation task delegation to another member - `GET/POST/PATCH/DELETE /api/v3/delegations[/{id}]` (scope: write) ### Reclaims (preview) — billing reclaim audit + undo - `GET /api/v3/reclaims[/{id}]` — read-only listing of reclaimed work - `PATCH /api/v3/reclaims/{id}` — bare `undone_at` writes are rejected (409 `RECLAIM_UNDO_NOT_SUPPORTED`) — use the undo endpoint below - `POST /api/v3/reclaims/{id}/undo` (scope: `financials`) — reverses the reclaimed time entries/field quantities and marks the reclaim undone (409 `RECLAIM_NOT_UNDOABLE` if already undone / locked invoice / source task gone) ### Auth - `GET /api/v3/auth/keys` — List API keys (scope: admin) - `POST /api/v3/auth/keys` — Create API key (scope: admin; `kind: 'integration'|'personal'`, see "Personal keys" above) - `POST /api/v3/auth/keys/{id}/rotate` — Rotate key (24h grace period, scope: admin; preserves `kind` + acting person) - `DELETE /api/v3/auth/keys/{id}` — Revoke API key (scope: admin) - All four endpoints are refused outright (403 `PERSONAL_KEY_CANNOT_MANAGE_KEYS`) for a personal-key caller — key management is admin-only, and a personal key must never mint/list/rotate/revoke any key, including itself. ### Health - `GET /api/v3/health` — Liveness/readiness probe (no auth required) ## Webhooks **Coming soon — not available yet.** The `/api/v3/webhooks` endpoints are not part of this release and currently return `503 WEBHOOKS_COMING_SOON`. No events are delivered. Poll the resource endpoints instead; a signed, documented webhook feature will follow. ## Pagination Two modes: **offset** (default) and **cursor** (opt-in via `?cursor=`). ### Offset mode `limit` (1-200, default 50; a few preview list endpoints default to 100), `offset` (0-10000), `sort` (prefix `-` for desc; `sort` is not accepted on `roles`/`workflow-roles`). ```json {"data": [...], "meta": {"total": 42, "limit": 50, "offset": 0, "has_more": false}} ``` ### Cursor mode Available on the core resources only: clients, tasks, time-entries, contacts, invoices, members, products, topics, contracts, client-groups, invoice-sellers, projects, monitors, suppliers. Smaller preview/config resources are offset-only. Pass `?cursor=` from previous response's `meta.next_cursor`. No total count (faster). ```json {"data": [...], "meta": {"limit": 50, "has_more": true, "next_cursor": "eyJj..."}} ``` In offset mode, `total` is reported as `10000+` once it exceeds 10000. ## Rate Limiting Tiered: 120 req/min for reads (GET), 30 req/min for writes (POST/PATCH/DELETE). Per API key. Headers: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `X-RateLimit-Tier`. 429 response includes `Retry-After` header. ## Idempotency Send an `Idempotency-Key` header (any string, max 200 chars) on `POST /api/v3/invoices`, `/contracts`, `/agreements`, or `/time-entries` to make a retried create safe. The key is scoped to your API key. A repeat request with the same key replays the original stored response verbatim (with `Idempotency-Replayed: true`) instead of creating a duplicate — the route is not re-invoked. A concurrent duplicate (the first request hasn't finished yet) gets `409 IDEMPOTENCY_CONFLICT`. Only successful (2xx) responses are stored, for 24h; a failed request is never replayed — just retry it. Omitting the header is a no-op (no dedup, current behavior). Sub-resource writes (e.g. `/invoices/{id}/rows`) don't support this yet. ## Errors ```json {"error": {"code": "NOT_FOUND", "message": "Resource not found"}} ``` Codes: MISSING_COMPANY (400), INVALID_COMPANY (400), VALIDATION_ERROR (400), INVALID_CURSOR (400), MISSING_AUTH (401), UNAUTHORIZED (401), FORBIDDEN (403), FINANCIAL_SCOPE_REQUIRED (403), SUBSCRIPTION_READ_ONLY (403), NOT_FOUND (404), STALE_WRITE (412), FILE_TOO_LARGE (413), UNSUPPORTED_MIME (415), UNSUPPORTED_EXTENSION (415), BLOCKED_EXTENSION (415), MIME_MISMATCH (415), PRECONDITION_REQUIRED (428), RATE_LIMIT_EXCEEDED (429), INTERNAL_ERROR (500). A bad foreign-key reference returns INVALID_ (400) (e.g. INVALID_CLIENT, INVALID_PERSON, INVALID_PROJECT); filter params can return UNKNOWN_FILTER_FIELD (400), UNKNOWN_OPERATOR (400), INVALID_FILTER_VALUE (422); read-only entities/fields return READ_ONLY (403). Some endpoints also return 409 conflicts (e.g. CLIENT_MEMBER_EXISTS, CUSTOM_FIELD_OPTION_IN_USE, LAST_PRICE_REQUIRED, INVOICE_LOCKED, CONTRACT_LOCKED, MONITOR_EXISTS, INVALID_STATUS_TRANSITION, IDEMPOTENCY_CONFLICT, DRIVE_NOT_CONNECTED, FOLDER_PROVISION_FAILED). Responses may include `"warnings": [{"code": "...", "message": "..."}]` for deprecation notices. ## Full Reference See /api/v3/llms-full.txt for complete field definitions, examples, and constraints. See /api/v3/docs for interactive Swagger UI. See /api/v3/redoc for ReDoc reference.