Skip to content
TTrackTimer
API documentation

Errors, rate limits, and safe retries

View Markdown

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

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

{
  "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 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 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.