Browse the docs
DocsDevelopers / APIDevelopers

Resources and endpoints

Time entries, clients, projects, tasks, tags, members, and reports: their methods, paths, and example payloads.

The API exposes the resources you already work with in Cadence. Every path below is relative to /api/v1, every request needs a bearer key with the right scope, and every result is confined to your bound organisation.

Time entries

Time entries are the core resource. List and read them with time:read; create and update them with time:write.

GET /time-entries
Keyset-paginated list, newest first. Optional filters: user_id, client, project_id, task_id, billable (true/false), and a from/to window on started_at.
POST /time-entries
Create an entry for the key's own member. Returns 201.
GET /time-entries/{id}
Fetch one entry by id; 404 if it is not in your organisation.
PATCH /time-entries/{id}
Update your own entry. Only the entry's author can edit it.

A create request looks like this:

http
POST /api/v1/time-entries HTTP/1.1
Authorization: Bearer ck_live_a1b2c3d4_...
Content-Type: application/json
Idempotency-Key: 9d1f6c2e-3b4a-4f7e-8c21-77a0b1c2d3e4
json
{
  "started_at": "2026-06-17T09:00:00Z",
  "ended_at": "2026-06-17T10:30:00Z",
  "client": "Acme Corp",
  "project_id": "1f2e3d4c-5b6a-4789-90ab-cdef01234567",
  "description": "Discovery call",
  "billable": true
}

The response wraps the created entry under data:

json
{
  "data": {
    "id": "0a9b8c7d-6e5f-4a3b-2c1d-0e9f8a7b6c5d",
    "org_id": "abcdef01-2345-6789-abcd-ef0123456789",
    "user_id": "11112222-3333-4444-5555-666677778888",
    "started_at": "2026-06-17T09:00:00Z",
    "ended_at": "2026-06-17T10:30:00Z",
    "duration_seconds": 5400,
    "client": "Acme Corp",
    "client_id": null,
    "project_id": "1f2e3d4c-5b6a-4789-90ab-cdef01234567",
    "project": "Website rebuild",
    "task_id": null,
    "description": "Discovery call",
    "billable": true,
    "clickup_task_id": null,
    "clickup_time_entry_id": null,
    "source": "api",
    "created_at": "2026-06-17T10:30:01Z"
  }
}

ended_at must not precede started_at; a reversed pair is a 422 validation error. duration_seconds is computed for you.

Idempotent creates

Send an Idempotency-Key header on a POST /time-entries to make retries safe. Replaying the same key with the same payload returns the original entry (200) instead of creating a duplicate; reusing the key with a different payload is rejected with 409 idempotency_conflict.

Never clear an external time-entry id on sync
A time entry carries clickup_time_entry_id (and clickup_task_id) linking it to a pushed ClickUp entry. The API does not set or touch these fields: the desktop app's sync owns them. If you build a two-way sync, never write a null over an existing clickup_time_entry_id: clearing it makes Cadence re-push the entry and create a duplicate on the next sync. Treat that id as read-only and preserve it on every update.

Catalogue: clients, projects, tasks, tags

The catalogue is the shared structure your time is tracked against. List with catalogue:read; create with catalogue:write (and an admin member, so creating catalogue records is an admin action. All four list endpoints accept status as a filter; projects also accept client_id, and tasks accept project_id.

GET, POST /clients
List or create clients.
GET, POST /projects
List or create projects. client_id is required on create.
GET, POST /tasks
List or create tasks. project_id is required on create.
GET, POST /tags
List or create tags.

Creating a client:

http
POST /api/v1/clients HTTP/1.1
Authorization: Bearer ck_live_a1b2c3d4_...
Content-Type: application/json
json
{ "name": "Acme Corp", "code": "ACME", "colour": "#4f7cff" }
json
{
  "data": {
    "id": "2b3c4d5e-6f70-4812-93a4-b5c6d7e8f901",
    "org_id": "abcdef01-2345-6789-abcd-ef0123456789",
    "name": "Acme Corp",
    "code": "ACME",
    "colour": "#4f7cff",
    "status": "active",
    "is_system": false,
    "created_at": "2026-06-17T10:31:00Z",
    "updated_at": "2026-06-17T10:31:00Z"
  }
}

Members

GET /members returns the organisation's members and roles. It needs members:read and an admin member: ordinary members get 403 forbidden_role. There is no member management over the API in v1; that stays in the product. See managing members.

json
{
  "data": [
    {
      "user_id": "11112222-3333-4444-5555-666677778888",
      "full_name": "Sam Carter",
      "email": "sam@acme.example",
      "org_role": "org_owner",
      "status": "active",
      "joined_at": "2026-05-01T08:00:00Z"
    }
  ]
}

Reports

The report endpoints return the same server-aggregated figures the in-app reports use, reconciled across views. They need reports:read and an admin member. All three require a from and to window, and accept tz_offset (defaulting to +00:00) plus repeatable client_id, user_id, project_id filters and a billable flag.

GET /reports/by-client
Totals broken down by client.
GET /reports/by-project
Totals flattened to projects under each client.
GET /reports/by-user
Totals by member, plus a member-by-client matrix.
http
GET /api/v1/reports/by-client?from=2026-06-01T00:00:00Z&to=2026-07-01T00:00:00Z&tz_offset=%2B02:00 HTTP/1.1
Authorization: Bearer ck_live_a1b2c3d4_...

Integrations

GET /integrations (integrations:read) returns connector status only: provider, a derived status of connected, expired, or disconnected, and timestamps. Connector tokens are never returned by the API.

Pagination

Larger collections (time entries) use keyset pagination, never offsets. Pass limit (1 to 500, default 100) and follow the opaque next_cursor from each page back in as ?cursor= until has_more is false:

json
{
  "data": [ /* ... entries ... */ ],
  "has_more": true,
  "next_cursor": "eyJzdGFydGVkX2F0IjoiMjAyNi0wNi0xN1QwOTowMDowMFoiLCJpZCI6Ii4uLiJ9"
}

Treat the cursor as opaque: do not parse or construct it. When has_more is false and next_cursor is null, you have reached the end of the list.