Errors, rate limits, and safe 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:
{
"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.hint provides resolution guidance. Other fields may include retry timing. Validation failures include an issues array with path, code, and message on each issue. requestId identifies the failed request. Responses handled by the REST API carry X-Request-Id, including errors.
Rate-limit headers
After API-key verification, responses include RateLimit (policy name, remaining requests r, reset seconds t) and RateLimit-Policy (quota q, window seconds w) following the IETF RateLimit draft. Compatibility fields RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset also expose the verified quota state. Quotas reset at fixed 60-second boundaries. Reset is seconds until the next boundary; polling does not extend the window. A rate-limited response includes Retry-After in seconds when the verifier supplies a retry delay. Missing or invalid credentials do not receive fabricated quota counters. These advisory values are shared across requests using the same key; concurrent requests can consume remaining quota.
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 useINVALID_REQUESTinstead. - 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. Honor theRetry-Afterdelay when supplied. - 500 —
INTERNAL_ERROR: the server could not complete the request, including response-contract failures. Keep therequestIdfor 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. A 503 AUTH_SERVICE_UNAVAILABLE means authentication could not be completed because a dependency failed; it does not mean the API key expired. Keep your key and retry after the Retry-After: 10 delay using bounded backoff. The request was rejected before the endpoint action ran; timer retries should still reuse the same idempotency key and input. Allow a rate-limit window to recover after 429. Do not automatically retry validation, authentication, access, or conflict errors without addressing the cause.