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