Skip to content
Integrations

Public API v3

Last updated: Sep 8, 2026

By the end of this article you’ll have generated an API v3 key, made an authenticated request, and know how Uku gates financial data, versions money records, and limits your request rate.

API v3 keys can be created by Company Owners and Company Admins on the Elite plan, once the Public API app is activated. Company Members cannot see or create API keys.

The Public API app opens on three tabs: Overview, (old) API for the legacy v1.0 API, and Public API — the current v3 API, and where you’ll spend your time. It’s the tab the app opens on.

Uku has two live APIs, and v3 is the one to build on. It isn’t a cosmetic version bump — v1.0 was a read-oriented window onto part of the product, and v3 is a full read-write interface to nearly all of it.

  • It reaches the whole product. Projects, budgets, notes, attachments, flextime, delegations and much more simply have no v1.0 endpoint.
  • Failures tell you what went wrong. Every error carries a stable code your integration can branch on, instead of a sentence to string-match.
  • Keys can be narrowed. Scope a reporting tool to read-only, lock a key to your servers by IP address, give it an expiry date — none of which v1.0 offers.
API v3 (Recommended)API v1.0 (Legacy)
Base URLhttps://app.getuku.com/api/v3/https://app.getuku.com/api/v1.0/
Signing inTwo headers, no expiring tokenKey + secret exchanged for a JWT that expires in 10 minutes
CoverageNearly all of UkuA limited subset
Writing dataCreate, update, and delete across most resourcesMostly read-only — only clients, contacts, tasks, and webhooks can be created
DocumentationInteractive Swagger + machine-readable OpenAPISandbox docs

The legacy API keeps running and your existing integrations keep working — it is frozen, not switched off. Keys are not interchangeable, though: a v1.0 key won’t authenticate against v3, and a v3 key won’t work on v1.0. Both can be active side by side while you migrate.

You generate an API v3 key from Uku’s Public API app. Each key belongs to your company and to the person who created it, and is shown in full only once.

Path: Settings & AppsPublic APIPublic API

  1. Copy your Company UUID from the field at the top of the tab — this is the X-Uku-Company value every request needs.
  2. Enter a Key name that says what will use it, for example “Power BI reporting” or “Warehouse sync”.
  3. Choose the Scopes the integration needs:
    • Read — read data only.
    • Edit — read, plus create, update, and delete on everyday records such as clients, contacts, tasks, and time entries.
    • All — everything above, plus managing API keys.
  4. Decide on Include financial data (invoices, revenue, rates, budgets). This is a separate checkbox, not a fourth scope — see Financial data is its own permission below. Choosing All ticks it for you and locks it on.
  5. Optionally open Advanced options to set an Expires at (optional) date, or an IP allowlist (optional) (comma-separated addresses or CIDR ranges, for example 192.168.1.1, 10.0.0.0/24) so the key only works from your servers.
  6. Click Generate API key. The full key (it starts with uku_live_) is copied to your clipboard and shown once — “Copy this key now — it will not be shown again.” Store it in your password manager or secrets vault, then click Done.

After that, the table lists the key’s Name, Key prefix, Scopes, Created by, Last used, Expires, and Created — never the key itself. The Scopes column combines both choices into one label, so you’ll see Read, Read + financial data, Edit, Edit + financial data, or All. The trash icon deletes a key, which revokes it immediately for any integration still using it.

Every key described above is an integration key. It belongs to the company, carries the scopes you chose, and behaves the same way whoever triggers the request.

The API can also mint a personal key, which acts as one named member. Every request it makes is limited to what that person sees and may change when they log in to Uku. If they cannot open a client, neither can the key. Create one by sending kind: 'personal' and the member’s id to POST /api/v3/auth/keys, from a key that holds the All scope.

Choose a personal key when the caller should be a real person, such as an assistant answering “what are my tasks this week?”. Choose an integration key when the caller is a system, such as a nightly sync.

A personal key created through POST /auth/keys is deliberately narrow, and you cannot widen it:

  • Its scopes are fixed at read and write. You cannot ask for All, and sending a scopes value returns 400.
  • Financial access is never granted this way, whatever rights the person holds in Uku. Money resources answer 403 FINANCIAL_SCOPE_REQUIRED. The browser sign-in route below is the one exception.
  • Key management is refused, including for the key itself. Every /auth/keys call returns 403 PERSONAL_KEY_CANNOT_MANAGE_KEYS.

An integration key still has a person behind it

Section titled “An integration key still has a person behind it”

“Company-wide” describes the data an integration key sees, not an absence of identity. Every key records who created it. A few rights checks resolve against that person, whatever kind of key it is: deleting a client, managing members, and writing document-type notes.

Anything that moves money needs the Include financial data checkbox — to read it as well as to write it. Without it a request that touches the financial surface returns 403 FINANCIAL_SCOPE_REQUIRED, whatever scope the key holds.

The checkbox sits beside the three scopes rather than inside them, which is the useful part: financial access and write access are independent choices. A Read + financial data key can pull invoice totals for a reporting dashboard while remaining unable to change a single record — the right shape for analytics.

The gate has two shapes, and the same split governs reading and writing.

Whole-record. Invoices and their rows, contracts and their rows, member agreements, product prices, invoice sellers, monitors, taxes, budgets and flextime are gated as a unit. Without the checkbox they return 403 even on a GET — including GET /products/{id}/prices, so a price-catalogue sync on a Read key fails on its first call — and every write is refused, whatever field you were changing. Renaming a contract needs the checkbox as much as changing its invoicing period does: letting an Edit key change “just the name” would let it read back a record it isn’t permitted to see.

Money fields only. Products and members stay readable and writable without the checkbox, but their money fields don’t. A product comes back with its prices, tax settings, billing basis and accounting routing codes empty, a member with the credit balances empty, and changing any of those needs the checkbox — plus an If-Match version stamp.

Writing money always needs both halves: the checkbox and a scope that permits writing.

A contract’s own status is not something you set: Uku derives it from the dates — pending before the range, active within it, finished after. That also decides whether you can delete it. An active or finished contract is refused with 409 CONTRACT_LOCKED; move its whole date range into the future so the derived status becomes pending, then delete.

API v3 exposes the invoice lifecycle actions — sending and marking paid — as endpoints, each needing financial access and the If-Match version stamp:

  • POST /invoices/{id}/send queues the invoice for delivery (email, PDF, e-invoice) and returns 202 — it goes out in the background, and status flips to sent once that finishes. Eligible while created, sent, or paid, else 409 INVOICE_NOT_SENDABLE.
  • POST /invoices/{id}/mark-paid records payment and moves the invoice to paid. Eligible only while created or sent, else 409 INVOICE_NOT_PAYABLE.

Your first API v3 request is a plain HTTPS call carrying two headers — there is no login call and no token to refresh.

Terminal window
curl https://app.getuku.com/api/v3/clients \
-H "X-Uku-Company: your-company-uuid" \
-H "X-API-Key: uku_live_..."

Successful list responses wrap the records in data with a meta block for paging — the exact shape for each endpoint is in the interactive sandbox.

Errors come back in the same shape every time, with a code your integration can branch on:

{
"error": {
"code": "MISSING_COMPANY",
"message": "X-Uku-Company header is required"
}
}

Some errors — a rejected request body, for instance — add a details list naming the fields at fault.

Uku publishes a live, interactive sandbox for API v3 — every endpoint, with its request and response schemas, and a Try it out button that fires real calls against your own company using your key. It is the fastest way to see an endpoint’s exact shape before writing any code, and it is always current because it is generated from the running API.

  • app.getuku.com/api/v3/docs — the interactive sandbox (Swagger UI). Paste your Company UUID and API key once, then call endpoints from the page.
  • app.getuku.com/api/v3/redoc — the same reference in a read-only, easier-to-browse layout.
  • app.getuku.com/api/v3/openapi.json — the machine-readable specification. Hand it to your developer, an API client such as Postman or Insomnia, or an AI coding tool to generate a client.
  • app.getuku.com/api/v3/llms.txt — a plain-text summary written for AI assistants, with the complete reference at llms-full.txt.
  • app.getuku.com/api/v3/capabilities — a machine-readable manifest of what the API can currently do, generated from the running code. No key is needed. Check whether Uku supports something before you have credentials, and have your integration read it at startup instead of hard-coding assumptions that age. Its not_available list answers “why is there no endpoint for this?”. The list names what Uku leaves out on purpose, and what to use instead. Activating a member is one of them: it stays a backoffice action. Handing an API key the power to grant someone access to the firm’s data is not a trade Uku makes.
  • app.getuku.com/api/v3/changelog — the API’s own record of what changed and when. Read it before you assume a behaviour you tested months ago still holds.

API v3 reaches nearly all of Uku. These are the resource families it exposes; the always-current endpoint list lives in the interactive sandbox.

  • Clients and contacts — clients, contacts, client groups, notes, and client members (each person’s role on a client, including their portal-user flag).
  • Work — tasks, task relations, task automations, delegations, projects, topics, workflow roles, roles. Tasks also have their own endpoints for completing and reopening, checklists, comments, and changing many tasks at once.
  • Workflows — client workflow templates and their tasks. Apply a template to many clients, or push template changes out to the clients already using it. Project workflows have their own set.
  • Time — time entries, calendar (vacation, time-off and flextime entries, plus global holidays), flextime and flextime balances.
  • Billing — invoices and rows, contracts and rows, products and prices, taxes, invoice sellers, budgets, monitors, and your company’s billing defaults. Also generating a client’s invoices, and previewing them first. Invoices also carry process_status and process_error. They tell you the billing engine could not price an invoice, which otherwise reads as an ordinary draft. Filter for those with ?has_process_error=true.
  • People — members and their agreements, teams, and changing members across many clients at once.
  • Reports — time summaries, a company-wide KPI snapshot, and the BI figures behind the analytics dashboards. Money figures appear only for a key with financial access.
  • Search and activity — one search across invoices, contacts, suppliers, contracts, tasks and notes, plus the activity trail with field-level before and after values. Clients are not one of the search categories. To find a client by name, use ?q= on the client list, or GET /resolve to turn a name straight into an id.
  • Your own credentialGET /auth/me answers what the key in your hand may do. It reports the company, the person it acts as, the scopes, and the rate-limit tiers.
  • Documents and folders — a client’s folder tree, folder templates, and the documents themselves, on a connected Google Drive, Dropbox or SharePoint.
  • Digitisation — send a client’s documents out to be digitised, and check what came back.
  • Your accounting software — read the invoices it holds, file an Uku invoice into it, and ask whether an invoice has been paid. Its tax and product codes can be synced too.
  • Setup — custom fields, product fields, suppliers, content templates, and the company’s apps. GET /company/apps reports which are active, and POST /company/apps/{id}/activate turns one on. Only Billing, Emailing, Business Analytics and Workforce Management can be activated this way. Some of them switch on others, and the read tells you which before you write. There is no deactivate endpoint, deliberately.
  • Files — attachments on tasks and notes (up to 150 MB per file; executables are rejected).

Some requests take too long to answer directly. Applying or pushing a workflow template across many clients, and changing members across many clients, answer with a job identifier instead. Poll GET /jobs/{id} for progress.

Uku labels some endpoints Preview in the documentation, and those return an X-Api-Preview: true header. They are fully available — the label means the shape may still change, so pin your integration to what you tested.

List endpoints in API v3 share the same query parameters for paging, filtering, and sorting, so what you learn on one resource mostly carries to the others. A few resources support less than the full set — the interactive sandbox shows exactly what each one accepts.

  • Paging?limit= and ?offset= (starts at 0). Most lists take 1–200 and default to 50, but a few default to 100 and the activity trail stops at 100, so read the endpoint’s own range in the sandbox rather than assuming. meta.has_more tells you when to fetch the next page. Beyond 10,000 records, page with ?cursor= where the resource offers it, and follow meta.next_cursor.
  • Filtering — use an operator suffix: ?client_id.eq=123, ?date.gte=2026-01-01, ?status.in=created,sent, ?topic_id.neq=4. Free-text ?q= exists on many resources but not all. Custom fields filter as ?custom_fields.<name>=value. Name the field the way a person would: the title SECRET works, and so does the stored name, whatever the capitals. Two different fields sharing one title are refused rather than guessed between.
  • Sorting?sort=name ascending, ?sort=-created_at descending. A field the resource does not sort on is dropped rather than refused. You get a 200 in the default order, with nothing to say the sort was ignored, so check the first page rather than assuming.

Always filter heavy endpoints (tasks, time entries, invoices) by date or by parent record rather than pulling everything.

To discover what a resource can filter on, send ?zzz.eq=1 and read the list it rejects you with.

If your integration checks Uku on a schedule, two features cut what each check costs you.

Ask only for what changed. GET /tasks/{id}, GET /clients/{id} and GET /invoices/{id} return an ETag. Send it back as If-None-Match on the next read of that record. If nothing changed, Uku answers 304 Not Modified with an empty body and skips rebuilding the record entirely.

Ask only for the fields you use. GET /tasks, GET /clients and GET /invoices accept ?fields=, a comma-separated list of the keys you want. id always comes back, so results stay joinable. Leaving the parameter off changes nothing.

Some of those exclusions save real work rather than just bytes. Dropping assignees and comments from a task list, or rows from invoices, skips the extra database queries behind them. An invoice can carry up to 200 rows. A field name Uku does not recognise is refused with 400 VALIDATION_ERROR listing the valid ones, so your integration can correct itself.

API v3 rate-limits each key separately for reads and writes: 120 read requests and 30 write requests per minute. When a request is counted against your key’s budget, the reply carries four headers:

  • X-RateLimit-Limit — your allowance for this type of request.
  • X-RateLimit-Remaining — how much is left this minute.
  • X-RateLimit-Reset — when the counter resets.
  • X-RateLimit-Tier — which budget was charged, read or write.

They are not on every response, so don’t require them: requests that go through without a key, the documentation endpoints, and any period when Uku’s rate-limit store is briefly unavailable all come back without them. In that last case the limit isn’t being enforced at all.

Go over your key’s budget and the API answers 429 RATE_LIMIT_EXCEEDED with a Retry-After header. Wait for that period instead of retrying immediately.

A second limit counts every request from your IP address, whatever key it carries, and it is checked before the per-key budget. It is deliberately generous — 1,000 requests a minute — because a shared address such as a Zapier or Make egress node carries many firms’ traffic at once. Requests that arrive with no key at all are held to a much tighter 60 a minute from that same address.

Both answer in the same shape as any other refusal: 429 RATE_LIMIT_EXCEEDED in the JSON body, with Retry-After and the X-RateLimit-* headers. So you can branch on error.code here exactly as you would elsewhere.

An idempotency key lets you retry a create request in API v3 without risking a duplicate record. When a create times out you often can’t tell whether it went through, so send an Idempotency-Key header (any unique string you generate) on the original request. A repeat with the same key and a byte-identical body replays the original response instead of creating a second record, and the reply carries Idempotency-Replayed: true. Uku remembers each key for 24 hours.

Byte-identical is literal: Uku fingerprints the raw bytes and does no JSON tidying first, so a retry that re-serialises the payload with its keys in a different order counts as a different request and gets a 409. Keep the body from the first attempt and resend exactly those bytes.

Nearly every create endpoint accepts the header. So do many of the actions that change a record’s state: sending an invoice, marking it paid, completing a task, applying a workflow template.

A list here would drift, so read the current one from the API itself. app.getuku.com/api/v3/capabilities returns it under data.idempotency, naming the endpoints left out and why. No key is needed.

Two things return a 409 instead: retrying while the first request is still running (IDEMPOTENCY_CONFLICT — wait and try again), and reusing a key with a different body (IDEMPOTENCY_KEY_REUSED — use a fresh key for each distinct request).

Webhooks let Uku call your endpoint when something changes, instead of your integration polling Uku for it. Subscribe a URL to an event, and every time that event happens we POST the record to your endpoint, signed and retried. Events cover clients, contacts, tasks, projects, invoices, and time entries.

You watch and manage v3 subscriptions from Uku itself. The Public API tab has a Connected webhooks list showing every subscription’s status, failure count, and recent deliveries, with buttons to Pause or Re-enable a subscription, Delete one, and Replay a failed delivery. Subscriptions are created through the API, not here. The full contract for both engines — subscribing, the delivery payload, signature verification, retries and replay — is in Webhooks.

Money-related records are protected against two systems overwriting each other by a version stamp. As a rule: a record that carries money wants the stamp on every change to an existing record, whatever field you’re touching, while ordinary records — tasks, clients, contacts, time entries — never do. Products and members are the in-between case: changes to their ordinary fields need no stamp, changes to their money fields do.

Which writes need the stamp is kept as one list in the API’s own reference, so it can’t drift: see the full reference, or open the endpoint in the interactive sandbox, which shows an If-Match header on every write that takes one.

When you read a protected record, the response includes an ETag header, which is simply a stamp of when the record last changed. To change or delete the record, send that stamp back as an If-Match header:

  • Leave it out and the API refuses with 428 PRECONDITION_REQUIRED — it wants to know which version you’re editing.
  • Send an old stamp, because someone edited the record in Uku or through another integration in the meantime, and the API refuses with 412 STALE_WRITE. Read the record again and retry with the fresh stamp.

An API v3 key opens your company’s live data to whatever holds it, so treat it as a credential of the same weight as a password to Uku itself.

  • Give the smallest scope that works. A reporting tool that reads money needs Read + financial data, not All — that is what the separate checkbox is for. One that reads no money needs only Read.
  • Lock keys to your servers with the IP allowlist, and set an Expires at date for anything temporary.
  • Deleting a key revokes it immediately, for every integration still using it — so it is the fix for a leak, but it will break anything still holding that key.
  • Always call over HTTPS. A request sent to http:// is rejected with 403 HTTPS_REQUIRED — but it is rejected after it arrives, so the key has already travelled the network unencrypted. Treat any key you have ever sent over plain HTTP as leaked: delete it and generate a new one.
  • A key only ever reaches your own company’s data. Records belonging to another company return 404, not an error that reveals they exist.

Why do I only see the Overview tab in the Public API app?

Section titled “Why do I only see the Overview tab in the Public API app?”

The Public API and (old) API tabs appear only for Company Owners and Company Admins, and only once the Public API app is activated for your company on the Elite plan. If you land on Overview with nothing beside it, that’s why: activate the app from that screen, or ask a Company Owner to. Company Members never see API keys at all.

There was never a public API v2. Uku went from v1.0 straight to v3, so there is no v2 to migrate through, no v2 keys, and no /api/v2/ endpoints — if you’re moving off v1.0, v3 is the next and only step. The jump in number reflects how much changed, not a version you missed.

A v1.0 key can’t authenticate against v3 — the two versions use separate credentials. Generate a new key on the Public API tab and switch to the two-header login (X-Uku-Company + X-API-Key); there is no /login call and no JWT in v3.

Why do I get a “company header required” error (MISSING_COMPANY)?

Section titled “Why do I get a “company header required” error (MISSING_COMPANY)?”

The X-Uku-Company header is missing or empty on the request. Copy the Company UUID from the top of the Public API tab and send it with every request, alongside the API key. When you authenticate with X-API-Key, both headers are required — the key on its own isn’t enough.

On a write, the key’s scope is too narrow: a Read key can’t create or change anything, so generate a new key with Edit.

If the error code is specifically FINANCIAL_SCOPE_REQUIRED, the request touched financial data — reading or writing an invoice, contract, agreement, invoice seller, monitor, tax, budget, flextime or product price, or a money field on a member or product. That needs the Include financial data checkbox, as described under Financial data is its own permission. Keys can’t be edited after they’re made, so generate a replacement with the box ticked and delete the old one.

Why is my filter refused with UNKNOWN_FILTER_FIELD?

Section titled “Why is my filter refused with UNKNOWN_FILTER_FIELD?”

The field name is not one this endpoint filters on. Read details.allowed in the error body — it lists every name you could have used — and correct the spelling or pick the right field. See Paging, filtering, and sorting.

The refusal is the API telling you early rather than handing back the wrong data. A filter name Uku does not recognise is refused, not ignored, so a 200 means your filter was understood.

Why is my filter ignored and I get every record back?

Section titled “Why is my filter ignored and I get every record back?”

Check ?sort= first: a sort field the resource does not recognise is dropped silently, and it is the one query parameter that still fails this way. If the filter came back 200 with everything, the field is a real one whose value matched every record. Check the value you sent, not the name.

Why is my “not equal” filter missing records?

Section titled “Why is my “not equal” filter missing records?”

.neq is a straight “not equal” comparison, so it leaves out every record where the field is empty.

Take ?topic_id.neq=4. It returns tasks on another topic, but not tasks with no topic at all. A list you read as “everything except topic 4” is short by however many records have no topic set.

Nothing warns you: the response is a normal 200 with a plausible-looking list. If you need the empty ones too, fetch them separately and combine the two lists yourself.

Why does my filter match nothing and return an empty list instead of an error?

Section titled “Why does my filter match nothing and return an empty list instead of an error?”

How a wrong filter value fails depends on the field’s type. Numbers, dates and true/false fields are checked, so a value that can’t be read as one returns 422 INVALID_FILTER_VALUE with the expected type in details. Text fields aren’t checked, so a wrong value there simply matches nothing and returns 200 with an empty data array — indistinguishable from a correct filter with no matches.

Task status is where this bites most often, because the value you see in Uku isn’t always the value the API stores: the internal set is new, in_progress, finished, inactive, and archived. Using .eq, you must send one of those exactly. The bare ?status= form is more forgiving and accepts common synonyms.

Do you have a test or sandbox environment?

Section titled “Do you have a test or sandbox environment?”

There is an interactive sandbox at app.getuku.com/api/v3/docs where you can call every endpoint from the browser — but it runs against your real company, because API v3 issues live keys only (uku_live_). There is no separate test company and no test key. While you build, write against a small, clearly-named client you can delete afterwards.

Can I rotate a key without breaking my integration?

Section titled “Can I rotate a key without breaking my integration?”

You can rotate a key without breaking your integration, but only through the API — there’s no rotate button in Uku. A key with the All scope can call POST /api/v3/auth/keys/{key_id}/rotate, which issues a replacement and keeps the old key working for a 24-hour grace period, so you can deploy the new value without downtime. Deleting a key in the interface, by contrast, revokes it immediately.