# 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)
