Skip to content
TTrackTimer
API documentation

Start or switch a timer

View Markdown

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.

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.

{
  "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 to reconcile. Different input with the same start key returns 409 IDEMPOTENCY_CONFLICT. See retry guidance.