# Build with the TrackTimer API.

Source: https://www.tracktimer.app/docs

Use the TrackTimer REST API to find accessible clients and projects, control your project timer, and read your time entries. Build a personal integration or connect tracked hours to your own tools.

Working with an AI agent? [Connect through the hosted MCP server](https://www.tracktimer.app/docs/mcp) to control timers, correct your time, and review monthly earnings.

## Base URL

`https://www.tracktimer.app/api/v1`

All endpoints require a bearer API key. Keys belong to one user and one workspace. Examples use placeholders, not real credentials.

## Make your first request

1. Sign in to your workspace and create a personal API key in Settings. Read [authentication and key management](https://www.tracktimer.app/docs/authentication) first.
2. Call [List clients](https://www.tracktimer.app/docs/list-clients), then [List projects](https://www.tracktimer.app/docs/list-projects) with a returned client ID.
3. [Start a timer](https://www.tracktimer.app/docs/start-timer) using a returned project ID and an idempotency key. Starting a timer can stop your existing active timer.
4. [Stop the timer](https://www.tracktimer.app/docs/stop-timer) and read the saved record through [List time entries](https://www.tracktimer.app/docs/list-time-entries).

```sh
curl "https://www.tracktimer.app/api/v1/clients" \
  --header "Authorization: Bearer $TRACKTIMER_API_KEY"
```

In these shell examples, TRACKTIMER_API_KEY is a local placeholder variable you supply securely. Keep credentials in your server-side secret store; do not embed them in a public website or commit them.

## Endpoint reference

### [Create a project](https://www.tracktimer.app/docs/create-project)

`POST /api/v1/projects`

Create a project as a workspace admin and automatically join it.

### [List clients](https://www.tracktimer.app/docs/list-clients)

`GET /api/v1/clients`

Discover active workspace clients according to the API key owner’s role.

### [List projects for a client](https://www.tracktimer.app/docs/list-projects)

`GET /api/v1/projects`

Find assigned, unarchived projects for a client in the key’s workspace.

### [Get the active timer](https://www.tracktimer.app/docs/get-active-timer)

`GET /api/v1/timer`

Read the key owner’s current timer in the bound workspace.

### [Start or switch a timer](https://www.tracktimer.app/docs/start-timer)

`POST /api/v1/timers/start`

Start a project timer and atomically complete any timer already running for the user.

### [Stop a timer](https://www.tracktimer.app/docs/stop-timer)

`POST /api/v1/timers/{timerId}/stop`

Complete a timer owned by the key user in the bound workspace.

### [List personal time entries](https://www.tracktimer.app/docs/list-time-entries)

`GET /api/v1/time-entries`

Read personal workspace history with project filtering and cursor pagination.

## What this API covers

V1 exposes the seven operations documented here, including [project creation](https://www.tracktimer.app/docs/create-project) for owners and admins. It does not expose client creation, invoices, or agency-wide financial reports. API access follows the key owner's current workspace membership and project access.

[Errors and safe retries](https://www.tracktimer.app/docs/errors-and-retries) · [Pagination](https://www.tracktimer.app/docs/pagination)

## Documentation downloads

- [Documentation index](https://www.tracktimer.app/llms.txt): A compact guide to the public documentation.
- [All documentation as Markdown](https://www.tracktimer.app/llms-full.txt): The complete public documentation in one file.
- [OpenAPI specification](https://www.tracktimer.app/openapi.json): The machine-readable REST API contract.

---

# TrackTimer MCP setup for AI agents

Source: https://www.tracktimer.app/docs/mcp

Connect TrackTimer to Codex, Claude, Claude Code, Cursor, or VS Code. Copy one setup prompt to track time, review entries, and check earnings from your AI agent.

Your time, in the conversation

## One prompt to get connected.

Paste this into an agent that can configure MCP connections. It will set up TrackTimer and guide you through approval. Clients without setup access will give you manual steps.

```text
Connect this agent to TrackTimer's remote MCP server, named tracktimer, at https://www.tracktimer.app/api/mcp using Streamable HTTP and OAuth browser sign-in.

Use this client's supported MCP setup method and inspect its existing configuration first. Preserve all other servers and settings. Reuse an existing matching TrackTimer connection; do not create duplicates or overwrite a different connection. Use user-level configuration when available unless I request project-level setup.

If you cannot configure MCP directly, give me the exact manual steps for this client. Documentation: https://www.tracktimer.app/docs/mcp

Start the client's OAuth authorization flow and let me complete TrackTimer sign-in, select my workspace, and approve permissions in my browser. Never ask me to paste tokens, passwords, or API keys into chat.

After authorization, verify the connection by listing my available clients or reading my active timer. Do not start, stop, or edit any time entries during setup. Tell me whether the connection is verified or what step remains.
```

## Or choose your agent

Use the setup instructions below, then approve the connection in TrackTimer.

### Server URL · Streamable HTTP

```text
https://www.tracktimer.app/api/mcp
```

Sign in with your TrackTimer account and passkey, choose your workspace, and review permissions. No API key is needed. Return to your agent and ask: “List my available clients in TrackTimer.”

### Connect TrackTimer to Codex

In a terminal with the Codex CLI installed, run these commands, then complete the browser sign-in. The local Codex app, CLI, and IDE extension share MCP configuration.

```sh
codex mcp add tracktimer --url https://www.tracktimer.app/api/mcp
codex mcp login tracktimer
```

[OpenAI setup documentation](https://developers.openai.com/codex/mcp/)

### Connect TrackTimer to Claude

[Add to Claude](https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=TrackTimer&connectorUrl=https%3A%2F%2Fwww.tracktimer.app%2Fapi%2Fmcp)

In Claude, open Customize → Connectors → Add custom connector. Name it TrackTimer, paste the server URL, and connect. Complete the TrackTimer authorization in your browser, then enable the connector in your conversation. Availability can depend on your plan and organization settings.

[Claude connector documentation](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp)

### Connect TrackTimer to Claude Code

If TrackTimer already appears through your connected Claude account, reuse it. Otherwise, run this command to add TrackTimer for your user account. Open Claude Code, run `/mcp`, and authenticate the TrackTimer server.

```sh
claude mcp add --transport http --scope user tracktimer https://www.tracktimer.app/api/mcp
```

[Claude Code MCP documentation](https://code.claude.com/docs/en/mcp)

### Connect TrackTimer to Cursor

[Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=tracktimer&config=eyJ1cmwiOiJodHRwczovL3d3dy50cmFja3RpbWVyLmFwcC9hcGkvbWNwIn0%3D)

Add the following server to your user configuration at `~/.cursor/mcp.json`, preserving existing servers. Open Cursor’s MCP settings and connect TrackTimer to complete OAuth in your browser.

```json
{
  "mcpServers": {
    "tracktimer": {
      "url": "https://www.tracktimer.app/api/mcp"
    }
  }
}
```

[Cursor MCP documentation](https://prod.cursor.com/docs/mcp)

### Connect TrackTimer to Visual Studio Code

[Add to Visual Studio Code](vscode:mcp/install?%7B%22name%22%3A%22tracktimer%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fwww.tracktimer.app%2Fapi%2Fmcp%22%7D)

Open the Command Palette and run **MCP: Add Server**. Choose HTTP, paste the server URL, name it `tracktimer`, and choose user configuration. Start the server, review its trust prompt, and complete browser authorization. Use it from GitHub Copilot Chat.

[VS Code MCP documentation](https://code.visualstudio.com/docs/agent-customization/mcp-servers)

Setup instructions checked against provider documentation on September 10, 2026. Client interfaces may change. Product names and logos belong to their respective owners.

## Using TrackTimer through MCP

## What you can ask

- “Start a billable timer for the website project with the note Homepage revisions.”
- “Stop my timer and note that I finished the navigation.”
- “Review my time entries and their correction history.”
- “Change the note on yesterday's completed entry to Client review, with the reason Clarified the work performed.”
- “How much have I earned this month? Break it down by project.”

Access stays bound to your user and the workspace you approved. Switching workspaces on the website does not redirect an existing connection. Even workspace owners and administrators see and change only their own time through this server.

Starting a timer stops any timer you currently have running, including in a different workspace. Other workspaces' timer details are not disclosed. Your agent should start or switch work only when you ask it to.

## Available tools and permissions

- `list_clients` — discover assigned clients; `clients:read`.
- `list_projects` — discover assigned projects for a client; `projects:read`.
- `get_active_timer`, `list_time_entries`, `get_time_entry` — inspect your own tracked time; `timers:read`.
- `get_time_entry_audit` — review reasons and before/after changes; `timers:read`.
- `start_timer`, `stop_timer` — control your timer; `timers:write`.
- `update_time_entry` — correct completed entries; `time-entries:write`.
- `get_time_summary` — calculate monthly time and earned pay; `reports:read`.

The server does not create clients, projects, invoices, or payments, or administer teammates. Read-only permissions cannot run write tools.

## Understand earnings

Monthly summaries use your profile timezone unless you supply an IANA timezone such as `America/Denver`. You can request a particular month with `YYYY-MM`. Entries crossing month boundaries contribute only the time inside that month.

Earnings use the pay rate saved on each completed entry, including nonbillable work. Running and voided entries are excluded. These amounts represent earned pay, not invoices sent or money received. Results include duration in seconds, exact decimal-dollar strings, rounded display amounts, and project totals in USD.

## Corrections and retries

Timer start and stop require an `idempotencyKey`. Your agent must create a unique key for each intended action and reuse that key with identical arguments when retrying. A network interruption can happen after an action succeeds, so generating a new key on retry can start another timer.

To correct completed time, first call `get_time_entry`. Supply its `updatedAt` value as `expectedUpdatedAt` together with the requested changes and a reason. If the entry changed, read it again and review the difference before submitting another correction. Changes preserve audit history. Moving an entry to a different assigned project recalculates its saved rates.

Entry and audit lists are paginated. Follow `nextCursor` until it is null to inspect the complete history. Use `get_time_summary` for monthly totals; adding up the first page of entries is incomplete.

## Disconnect and troubleshooting

Open [Settings](https://www.tracktimer.app/app/settings) to disconnect all your agents for the selected workspace. This revokes every agent sharing that workspace authorization. Revoked grants and removed workspace membership are checked on subsequent requests. Reconnecting requires authorization again.

Connections share a limit of 120 requests per minute for your workspace authorization. Respect `Retry-After` on HTTP 429 responses. HTTP 401 means authorization is needed; HTTP 403 can mean missing permission or unavailable workspace access. Tool errors include a code and request ID without exposing internal database details.

The endpoint supports current MCP and stateless 2025 Streamable HTTP clients. It returns JSON or request-scoped streaming responses and progress for summaries when requested. It does not provide the older separate SSE endpoint or persistent subscriptions. Browser clients must use the TrackTimer origin; hosted and native agents can connect directly.

Examples use illustrative IDs and data. Use the values returned by your own workspace. [Review error handling and retry behavior.](https://www.tracktimer.app/docs/errors-and-retries)

---

# Authentication and API keys

Source: https://www.tracktimer.app/docs/authentication

Create a workspace-bound API key and understand scopes, expiration, and request authentication.

Use a personal API key to call `https://www.tracktimer.app/api/v1`. Every endpoint requires `Authorization: Bearer YOUR_API_KEY`. Browser session cookies alone do not authenticate these routes, and there is no API-key query parameter.

## Create a key

1. Sign in and select the workspace the integration should access.
2. Open [Settings](https://www.tracktimer.app/app/settings). Use **Verify for sensitive changes** if needed: key creation and revocation require passkey verification within the last five minutes.
3. Under **Personal API keys**, enter a recognizable name and select **Create key**.
4. Copy the key when shown and store it in your integration's secret manager. The plaintext is shown only at creation.

Keys use the `tt_` prefix and expire after 365 days. Create a replacement before expiration, update the integration, then revoke the old key in Settings. The current interface does not offer custom expiration or a scope picker.

Examples use `$TRACKTIMER_API_KEY` as a placeholder for a secret injected into your shell environment. They contain no working credential. Keep keys out of client-side bundles, repositories, screenshots, and request logs.

## Workspace and user boundaries

A key is bound to its creating user and the workspace selected at creation. Switching workspaces in the website does not retarget the key. Each request checks the key and current workspace membership. Invalid bindings or missing membership are rejected.

Project discovery returns assigned, unarchived projects. Owners and admins can discover every active client in their workspace, including clients without projects; other members see clients with assigned projects. Time-entry history contains only the key user's entries in the bound workspace, including historical entries.

## Scopes

Keys created in Settings receive all five default scopes:

- `clients:read`: discover accessible clients.
- `projects:read`: discover accessible projects for a client.
- `projects:write`: create projects when the key owner is a workspace owner or admin. Existing keys retain their scopes; create a replacement key to enable this permission.
- `timers:read`: read the active timer and personal time-entry history.
- `timers:write`: start and stop timers.

The server checks the required scope on every request. A verified key missing it receives `403 INSUFFICIENT_SCOPE`. The public API does not provide client creation, project editing, time-entry editing, invoicing, or organization financial reports.

## Request and response basics

GET requests need no body. Starting a timer takes JSON with `Content-Type: application/json`. Timer POST endpoints require a nonblank `Idempotency-Key` of at most 255 characters after trimming. Persist a fresh unique operation identifier before each intended timer mutation; keep it unchanged when retrying that operation.

Successful v1 responses return HTTP 200 (201 for project creation) and JSON directly, with no `data` envelope. They include an `X-Request-Id` header. Error bodies carry their own `requestId`. All seven operations share a per-key limit of 120 requests per 60-second window. See [errors and retries](https://www.tracktimer.app/docs/errors-and-retries).

## First request

```sh
curl "https://www.tracktimer.app/api/v1/clients" \
  --header "Authorization: Bearer $TRACKTIMER_API_KEY"
```

Examples use illustrative IDs and data. Use the values returned by your own workspace. [Review error handling and retry behavior.](https://www.tracktimer.app/docs/errors-and-retries)

---

# Errors, rate limits, and safe retries

Source: https://www.tracktimer.app/docs/errors-and-retries

Handle API failures, the 120-request rate limit, and idempotent timer mutations.

All documented route errors use this JSON envelope. Values below are illustrative:

```json
{
  "error": {
    "code": "IDEMPOTENCY_KEY_REQUIRED",
    "message": "A valid Idempotency-Key header is required.",
    "details": {}
  },
  "requestId": "example-request-id"
}
```

`error.code` is machine-readable; `error.message` describes the failure. `error.details` is an object, usually empty. Validation failures include an `issues` array with `path`, `code`, and `message` on each issue. `requestId` identifies the failed request. Successes carry `X-Request-Id`; do not assume that header is present on errors.

## Status codes

- **400 — `INVALID_REQUEST`:** invalid parameter/body, or empty/malformed start JSON. Fix the input. The start body rejects unknown properties.
- **400 — `IDEMPOTENCY_KEY_REQUIRED`:** a timer POST lacks a usable key, or its trimmed value exceeds 255 characters.
- **400 — `INVALID_CURSOR`:** a cursor passed text-format validation but could not decode to the expected payload. Text-format failures use `INVALID_REQUEST` instead.
- **401 — `INVALID_API_KEY`:** missing, malformed, invalid, expired, or revoked credentials; also an invalid workspace binding or missing membership.
- **403 — `INSUFFICIENT_SCOPE`:** the verified key lacks the endpoint's scope.
- **404 — `PROJECT_NOT_FOUND`:** start cannot find an assigned, unarchived project/client with member rate setup in this workspace.
- **404 — `TIMER_NOT_FOUND`:** the stop target is not owned by this user in this workspace, or does not exist.
- **409 — `IDEMPOTENCY_CONFLICT`:** a mutation key was used with different input. Investigate the operation tracking rather than blindly generating another key after an uncertain response.
- **409 — `TIMER_VOIDED`:** a fresh stop targets a voided entry. An existing successful receipt can still be replayed.
- **409 — `TIMER_ALREADY_STOPPED`:** the stop update could not find an open entry at mutation time. Reconcile state. Normally stopping an already completed entry succeeds.
- **429 — `API_KEY_RATE_LIMITED`:** more than 120 requests in the key's 60-second window. Pause and reduce frequency. The current API does not set `Retry-After`.
- **500 — `INTERNAL_ERROR`:** the server could not complete the request, including response-contract failures. Keep the `requestId` for investigation.

Infrastructure failures and unsupported HTTP methods can have different response shapes. Check status and content type before parsing JSON.

## Project creation

[Create project](https://www.tracktimer.app/docs/create-project) is not idempotent. After an uncertain response, list projects and reconcile before creating again; do not automatically retry.

## Retry timer mutations safely

Create and persist an operation ID before either timer POST. After a timeout, lost response, or retryable server failure, retry the same endpoint, key, and input with bounded exponential backoff and jitter. Keep the operation ID until the result is known. Do not start a second timer with a fresh key because the first response was lost.

Start receipts are keyed by user and idempotency key and compare normalized input including workspace, member, project, billability, and note. Omitted `billable` equals `true`; omitted `note` normalizes to `null`, while an empty string is distinct. A successful replay returns the original response snapshot. Elapsed time is not refreshed and the timer may have since stopped. Read [the active timer](https://www.tracktimer.app/docs/get-active-timer) to reconcile current state.

Stop receipts are keyed by user and idempotency key in a separate operation namespace. Their target must match. Prefer globally unique operation IDs anyway, with different IDs for start and stop. Replaying a successful stop does not restart or extend the entry.

Retries still need a valid key, membership, and scope. No public receipt expiration guarantee is specified; never intentionally recycle old IDs. Allow a rate-limit window to recover after 429. Do not automatically retry validation, authentication, access, or conflict errors without addressing the cause.

Examples use illustrative IDs and data. Use the values returned by your own workspace. [Review error handling and retry behavior.](https://www.tracktimer.app/docs/errors-and-retries)

---

# Paginate personal time entries

Source: https://www.tracktimer.app/docs/pagination

Read time-entry history with opaque cursors and understand ordering and concurrent changes.

Only [list time entries](https://www.tracktimer.app/docs/list-time-entries) is paginated. Client and project lists do not use cursors.

## First page

Omit `cursor` initially. `limit` defaults to 20 and accepts integers from 1 through 100. Optional `projectId` must be a UUID. There are no date-range, status, user, or offset parameters.

```sh
curl "https://www.tracktimer.app/api/v1/time-entries?limit=20" \
  --header "Authorization: Bearer $TRACKTIMER_API_KEY"
```

Responses contain `entries` and `nextCursor`. Non-null `nextCursor` means another row existed beyond the page when queried. `null` means no subsequent page was found at that time.

## Next page

Pass the returned cursor unchanged, retaining the same workspace key and project filter. Replace `CURSOR_FROM_RESPONSE` below with the returned value:

```sh
curl "https://www.tracktimer.app/api/v1/time-entries?limit=20&cursor=CURSOR_FROM_RESPONSE" \
  --header "Authorization: Bearer $TRACKTIMER_API_KEY"
```

Continue until `nextCursor` is `null`. Do not send literal `null`, an empty cursor, or a reconstructed cursor. Cursors are opaque, at most 1,024 characters, and use base64url characters. Their internal encoding is not an integration contract.

## Ordering and changes

Entries sort by `startedAt` descending, then ID descending to break ties. A cursor continues strictly after the last entry in that ordering. The server fetches one extra row to decide whether to return another cursor.

This is not a frozen snapshot. Newer entries created after the first page will not appear in subsequent older pages. Edits to start time can move entries relative to the boundary. For synchronization, deduplicate by entry ID and refresh leading pages as needed; a cursor is not a permanent synchronization token.

History includes active, completed, and voided personal entries in the workspace, including archived projects or projects the user is no longer assigned to. A project filter with no personal history returns an empty result rather than an access error: `{"entries":[],"nextCursor":null}`.

Examples use illustrative IDs and data. Use the values returned by your own workspace. [Review error handling and retry behavior.](https://www.tracktimer.app/docs/errors-and-retries)

---

# Create a project

Source: https://www.tracktimer.app/docs/create-project

Create a project as a workspace admin and automatically join it.

`POST /api/v1/projects`

Required scope: `projects:write`

Create a project under an active client in the key’s workspace. The creator is assigned in the same transaction, so a project needs no prior time entries or active timer. Creation does not start a timer.

## Request

POST `https://www.tracktimer.app/api/v1/projects` with bearer authentication, `projects:write`, and `Content-Type: application/json`. The current workspace role must be owner or admin. Existing keys need replacement to receive the new scope.

- `clientId`: required UUID of an active client in this workspace.
- `name`: required project name, trimmed, 1–200 characters.
- `color`: optional supported color or null to inherit the client color.

Unknown properties are rejected. The server chooses the workspace and creator from the key.

```sh
curl --request POST "https://www.tracktimer.app/api/v1/projects" \
  --header "Authorization: Bearer $TRACKTIMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"clientId":"11111111-1111-4111-8111-111111111111","name":"Website refresh"}'
```

## Response — 201

`project` contains its UUID, parent client UUID, name, and resolved color.

```json
{"project":{"id":"22222222-2222-4222-8222-222222222222","clientId":"11111111-1111-4111-8111-111111111111","name":"Website refresh","color":"#ff5c35"}}
```

Use the returned project ID to [start a timer](https://www.tracktimer.app/docs/start-timer). Normal member rate requirements still apply. The creator can later remove their assignment through workspace member management.

## Errors and retries

This endpoint is not idempotent and does not use `Idempotency-Key`. Never automatically retry project creation after a timeout, server error, or lost response: the project may already exist. [List projects](https://www.tracktimer.app/docs/list-projects) for the client and reconcile before deciding to create again.

Missing scope or a non-admin role returns 403. An unknown, archived, or other-workspace client returns 404. Invalid input returns 400. See [shared errors](https://www.tracktimer.app/docs/errors-and-retries).

Examples use illustrative IDs and data. Use the values returned by your own workspace. [Review error handling and retry behavior.](https://www.tracktimer.app/docs/errors-and-retries)

---

# List clients

Source: https://www.tracktimer.app/docs/list-clients

Discover active workspace clients according to the API key owner’s role.

`GET /api/v1/clients`

Required scope: `clients:read`

Owners and admins receive all active clients in the bound workspace, including empty clients. Other members receive clients with at least one active assigned project. Archived clients are excluded. Results sort by client name.

## Request

Requires bearer authentication and `clients:read`. No query parameters or body are needed. This list is not paginated.

```sh
curl "https://www.tracktimer.app/api/v1/clients" \
  --header "Authorization: Bearer $TRACKTIMER_API_KEY"
```

## Response — 200

- `clients`: array, possibly empty.
- `clients[].id`: client UUID.
- `clients[].name`: nonempty client name.

```json
{
  "canCreateProjects": false,
  "clients": [
    {
      "id": "11111111-1111-4111-8111-111111111111",
      "name": "Northstar"
    }
  ]
}
```

`canCreateProjects` is true only when the current user is an owner or admin and the key includes `projects:write`. When true, [create a project](https://www.tracktimer.app/docs/create-project) for an empty client; its creator is assigned automatically. Use a returned ID with [list projects](https://www.tracktimer.app/docs/list-projects). Authentication, scope, rate-limit, and server failures use [shared errors](https://www.tracktimer.app/docs/errors-and-retries).

Examples use illustrative IDs and data. Use the values returned by your own workspace. [Review error handling and retry behavior.](https://www.tracktimer.app/docs/errors-and-retries)

---

# List projects for a client

Source: https://www.tracktimer.app/docs/list-projects

Find assigned, unarchived projects for a client in the key’s workspace.

`GET /api/v1/projects`

Required scope: `projects:read`

Returns the user's assigned, unarchived projects under the requested unarchived client. Results sort by client name and project name.

## Request

Requires bearer authentication and `projects:read`. The query parameter `clientId` is a required UUID. Missing, empty, or non-UUID input returns `400 INVALID_REQUEST`. No request body or pagination.

```sh
curl "https://www.tracktimer.app/api/v1/projects?clientId=11111111-1111-4111-8111-111111111111" \
  --header "Authorization: Bearer $TRACKTIMER_API_KEY"
```

## Response — 200

- `projects`: array, possibly empty.
- `projects[].id`: project UUID.
- `projects[].clientId`: parent client UUID.
- `projects[].name`: nonempty project name.

```json
{
  "projects": [
    {
      "id": "22222222-2222-4222-8222-222222222222",
      "clientId": "11111111-1111-4111-8111-111111111111",
      "name": "Website refresh"
    }
  ]
}
```

A valid UUID with no visible matches returns `{"projects":[]}`, including unknown, inaccessible, or archived clients. Discovery checks assignment and archive state; starting also needs the member's rate setup. A discovery result alone does not guarantee [start](https://www.tracktimer.app/docs/start-timer) will succeed. See [shared errors](https://www.tracktimer.app/docs/errors-and-retries).

Examples use illustrative IDs and data. Use the values returned by your own workspace. [Review error handling and retry behavior.](https://www.tracktimer.app/docs/errors-and-retries)

---

# Get the active timer

Source: https://www.tracktimer.app/docs/get-active-timer

Read the key owner’s current timer in the bound workspace.

`GET /api/v1/timer`

Required scope: `timers:read`

Returns the active timer only if it belongs to the key's workspace. Requires bearer authentication and `timers:read`. No query parameters or body.

```sh
curl "https://www.tracktimer.app/api/v1/timer" \
  --header "Authorization: Bearer $TRACKTIMER_API_KEY"
```

## Response — 200

`timer` is `null` or an object with these fields:

- `id`: UUID of this timer/time entry.
- `clientId`, `projectId`: client and project UUIDs.
- `clientName`, `projectName`: nonempty display names.
- `billable`: boolean indicating whether the entry is billable.
- `note`: string up to 500 characters, or `null`.
- `startedAt`: ISO 8601 timestamp with timezone offset, normally UTC `Z`.
- `serverNow`: server timestamp used for this response's elapsed calculation.
- `elapsedSeconds`: nonnegative integer elapsed seconds as of `serverNow`.
- `payRateCents`: nonnegative integer hourly pay rate in cents, snapshotted on the entry. This is not the client billing rate.
- `currency`: always `"USD"`.

Integer duration and pay-rate fields are bounded by 2,147,483,647.

```json
{
  "timer": {
    "id": "33333333-3333-4333-8333-333333333333",
    "clientId": "11111111-1111-4111-8111-111111111111",
    "clientName": "Northstar",
    "projectId": "22222222-2222-4222-8222-222222222222",
    "projectName": "Website refresh",
    "billable": true,
    "note": "Homepage revisions",
    "startedAt": "2026-09-10T16:00:00.000Z",
    "serverNow": "2026-09-10T16:05:00.000Z",
    "elapsedSeconds": 300,
    "payRateCents": 5000,
    "currency": "USD"
  }
}
```

`{"timer":null}` means no active timer is visible in this workspace. The user can still have an active timer in another workspace.

Elapsed time is a server-calculated snapshot, not a live stream. Poll within the shared limit. Pay rate is the start-time snapshot; client billing rates and project margins are not exposed. See [shared errors](https://www.tracktimer.app/docs/errors-and-retries).

Examples use illustrative IDs and data. Use the values returned by your own workspace. [Review error handling and retry behavior.](https://www.tracktimer.app/docs/errors-and-retries)

---

# Start or switch a timer

Source: https://www.tracktimer.app/docs/start-timer

Start a project timer and atomically complete any timer already running for the user.

`POST /api/v1/timers/start`

Required scope: `timers:write`

Starts a timer on an assigned, unarchived project in the key's workspace. Any active timer for the user is completed in the same transaction, even on the same project or in a different workspace.

## Request

Requires bearer authentication, `timers:write`, JSON content type, and `Idempotency-Key` with 1–255 characters after trimming. Persist a unique ID in `$TRACKTIMER_OPERATION_ID` before sending. Reuse it only for retries of this operation.

JSON body fields:

- `projectId`: required project UUID.
- `billable`: optional boolean, default `true`.
- `note`: optional string, maximum 500 characters. Omission stores `null`; empty string is accepted. A JSON `null` is not accepted as request input.

Unknown fields are rejected. Callers cannot supply start time, rates, user ID, workspace ID, or previous timer ID. The server chooses the timestamp and snapshots the applicable rates.

```sh
curl --request POST "https://www.tracktimer.app/api/v1/timers/start" \
  --header "Authorization: Bearer $TRACKTIMER_API_KEY" \
  --header "Idempotency-Key: $TRACKTIMER_OPERATION_ID" \
  --header "Content-Type: application/json" \
  --data '{"projectId": "22222222-2222-4222-8222-222222222222", "billable": true, "note": "Homepage revisions"}'
```

## Response — 200

- `timer`: new active timer, or the original snapshot on replay. The shared response schema permits null, but successful start returns an object.
- `stoppedTimerId`: UUID of the previous timer completed in this workspace, otherwise `null`.

Fields in `timer`:

- `id`: UUID of this timer/time entry.
- `clientId`, `projectId`: client and project UUIDs.
- `clientName`, `projectName`: nonempty display names.
- `billable`: boolean indicating whether the entry is billable.
- `note`: string up to 500 characters, or `null`.
- `startedAt`: ISO 8601 timestamp with timezone offset, normally UTC `Z`.
- `serverNow`: server timestamp used for this response's elapsed calculation.
- `elapsedSeconds`: nonnegative integer elapsed seconds as of `serverNow`.
- `payRateCents`: nonnegative integer hourly pay rate in cents, snapshotted on the entry. This is not the client billing rate.
- `currency`: always `"USD"`.

Integer duration and pay-rate fields are bounded by 2,147,483,647.

```json
{
  "timer": {
    "id": "33333333-3333-4333-8333-333333333333",
    "clientId": "11111111-1111-4111-8111-111111111111",
    "clientName": "Northstar",
    "projectId": "22222222-2222-4222-8222-222222222222",
    "projectName": "Website refresh",
    "billable": true,
    "note": "Homepage revisions",
    "startedAt": "2026-09-10T16:00:00.000Z",
    "serverNow": "2026-09-10T16:00:00.000Z",
    "elapsedSeconds": 0,
    "payRateCents": 5000,
    "currency": "USD"
  },
  "stoppedTimerId": null
}
```

## Switching and replay

A fresh key starts a new entry even on the same project. Access checks occur before stopping any active timer. Missing assignment, archived project/client, or missing member rate setup returns `404 PROJECT_NOT_FOUND` without switching.

Only one timer is active per user across workspaces. Starting here can complete one elsewhere, but hides its identifier: `stoppedTimerId` is null. Integrations should make this switching behavior clear to their users.

Same normalized input and key return the stored result without creating another entry. It may no longer be active; [read current state](https://www.tracktimer.app/docs/get-active-timer) to reconcile. Different input with the same start key returns `409 IDEMPOTENCY_CONFLICT`. See [retry guidance](https://www.tracktimer.app/docs/errors-and-retries).

Examples use illustrative IDs and data. Use the values returned by your own workspace. [Review error handling and retry behavior.](https://www.tracktimer.app/docs/errors-and-retries)

---

# Stop a timer

Source: https://www.tracktimer.app/docs/stop-timer

Complete a timer owned by the key user in the bound workspace.

`POST /api/v1/timers/{timerId}/stop`

Required scope: `timers:write`

Stops an entry with a server-generated end timestamp and duration. The target must belong to this user and workspace.

## Request

Requires bearer authentication, `timers:write`, and `Idempotency-Key` of 1–255 characters after trimming. `timerId` is a required UUID path parameter. The endpoint does not read a request body: send no body. It cannot update notes or accept an end timestamp.

Use the ID returned by the active-timer endpoint. Persist a stop operation ID in `$TRACKTIMER_OPERATION_ID`, distinct from the start ID.

```sh
curl --request POST "https://www.tracktimer.app/api/v1/timers/33333333-3333-4333-8333-333333333333/stop" \
  --header "Authorization: Bearer $TRACKTIMER_API_KEY" \
  --header "Idempotency-Key: $TRACKTIMER_OPERATION_ID"
```

## Response — 200

- `timer`: always `null` in this response.
- `stoppedTimerId`: UUID of the specified stopped or already completed entry.

```json
{
  "timer": null,
  "stoppedTimerId": "33333333-3333-4333-8333-333333333333"
}
```

Read [time-entry history](https://www.tracktimer.app/docs/list-time-entries) for completed duration and earnings. `timer:null` is a mutation result, not the current global timer state. Another timer can still be running if this target was already completed by a switch.

## Completed, voided, and inaccessible targets

An already completed target normally succeeds without changing its end time, including with a new stop ID. Replaying a successful receipt does not extend it. A fresh stop on a voided entry returns `409 TIMER_VOIDED`; an existing successful receipt can still replay after the entry is voided.

Unknown, other-user, or other-workspace targets return `404 TIMER_NOT_FOUND`. Invalid UUIDs return `400 INVALID_REQUEST`. Reusing a stop key for a different accessible target returns `409 IDEMPOTENCY_CONFLICT`. A failed open-entry update can return `409 TIMER_ALREADY_STOPPED`. See [shared errors](https://www.tracktimer.app/docs/errors-and-retries).

Examples use illustrative IDs and data. Use the values returned by your own workspace. [Review error handling and retry behavior.](https://www.tracktimer.app/docs/errors-and-retries)

---

# List personal time entries

Source: https://www.tracktimer.app/docs/list-time-entries

Read personal workspace history with project filtering and cursor pagination.

`GET /api/v1/time-entries`

Required scope: `timers:read`

Returns the key user's entries in its workspace: active, completed, and voided. Historical entries include archived projects or projects the user is no longer assigned to. This is not organization-wide history.

## Request

Requires bearer authentication and `timers:read`. No body. Optional query parameters:

- `projectId`: project UUID filtering the user's history.
- `limit`: integer from 1 through 100, default 20.
- `cursor`: opaque `nextCursor` from the preceding page; omit initially. Maximum 1,024 base64url characters.

There are no date, status, offset, or user filters. Unknown parameters are not supported filters. Entries sort by start time descending, then ID descending.

```sh
curl "https://www.tracktimer.app/api/v1/time-entries?limit=20&projectId=22222222-2222-4222-8222-222222222222" \
  --header "Authorization: Bearer $TRACKTIMER_API_KEY"
```

## Response — 200

- `entries`: array, possibly empty.
- `nextCursor`: next-page string, or `null` if none was found.

Each entry contains:

- `id`: entry UUID.
- `clientId`, `projectId`: client and project UUIDs.
- `clientName`, `projectName`: nonempty display names.
- `status`: `"active"`, `"completed"`, or `"voided"`.
- `billable`: boolean. Nonbillable entries can still produce member earnings.
- `note`: string up to 500 characters, or `null`.
- `startedAt`: ISO 8601 timestamp with timezone offset, normally UTC `Z`.
- `endedAt`: timestamp or `null` for an unfinished entry.
- `durationSeconds`: nonnegative integer recorded duration, or `null` if not finalized. This is not a live elapsed value.
- `payRateCents`: nonnegative integer hourly pay rate in cents, snapshotted on the entry.
- `earnings`: decimal-dollar string or `null`. Completed entry earnings use duration and pay rate independently of billability. Active entries and entries without duration return null. Voided entries with recorded duration return `"0"`.
- `currency`: always `"USD"`.

Duration and pay-rate integers are bounded by 2,147,483,647. Earnings strings have up to 12 fractional places, not necessarily two. Preserve decimal precision. Client billing rates and organizational profit fields are not included.

```json
{
  "entries": [
    {
      "id": "33333333-3333-4333-8333-333333333333",
      "clientId": "11111111-1111-4111-8111-111111111111",
      "clientName": "Northstar",
      "projectId": "22222222-2222-4222-8222-222222222222",
      "projectName": "Website refresh",
      "billable": true,
      "note": "Homepage revisions",
      "startedAt": "2026-09-10T16:00:00.000Z",
      "payRateCents": 5000,
      "currency": "USD",
      "status": "completed",
      "endedAt": "2026-09-10T17:30:00.000Z",
      "durationSeconds": 5400,
      "earnings": "75"
    }
  ],
  "nextCursor": null
}
```

A valid project UUID with no matching personal history returns an empty list. Invalid limit, ID, or cursor text returns `400 INVALID_REQUEST`; an undecodable payload returns `400 INVALID_CURSOR`. Read [pagination](https://www.tracktimer.app/docs/pagination) before requesting subsequent pages and [shared errors](https://www.tracktimer.app/docs/errors-and-retries) for other failures.

Examples use illustrative IDs and data. Use the values returned by your own workspace. [Review error handling and retry behavior.](https://www.tracktimer.app/docs/errors-and-retries)
