Skip to content

Agent provenance

This content is not available in your language yet.

Tickr records where every time entry comes from: web (the app), api (an API key), agent (a human entry created through an AI agent), agent_session (automatic working time of an agent), imports and calendar. The origin is immutable and exposed on every entry as origin.

Any POST /api/v1/entries made with an API key is api. To record that the entry was created by an agent on behalf of the key’s owner, send two headers:

Tickr-Origin: agent
Tickr-Agent: claude-code/2.1.0

Tickr-Agent is <name>/<version>: name ^[a-z0-9][a-z0-9._-]{0,63}$, version ^[A-Za-z0-9][A-Za-z0-9.+_-]{0,31}$. Tickr-Origin: agent without Tickr-Agent is refused (422 agent_required). The entry belongs to the key’s owner and always counts as work time.

PUT /api/v1/entries/{id}/agent-usage (own agent entries only, for every role allowed to write — an Analyst gets 403 analyst_read_only; a colleague’s entry answers 404):

{
"revision": 1,
"agentSessionId": "sess-7f3a",
"usageQuality": "complete",
"usage": [
{ "provider": "anthropic", "model": "claude-opus-5-5", "inputTokens": 1200, "outputTokens": 340,
"cacheCreationInputTokens": 0, "cacheReadInputTokens": 900 }
]
}

The session id is set once; the revision must grow; the same revision with a different body answers 409 agent_revision_conflict; a different session 409 agent_session_mismatch. Up to 20 (provider, model) rows, providers anthropic, bedrock, vertex, foundry, other.

This route requires an API key (401 otherwise) and the Tickr-Agent: <name>/<version> header (otherwise 422 agent_required); the created entry is stamped with that agent name and version.

PUT /api/v1/agent-segments/{segmentId} upserts one segment of agent activity (half-open [startedAt, stoppedAt), at most 24 h, never crossing the next UTC midnight, stoppedAt at most 5 min in the future, start within the last 30 days at creation). The id is chosen by the client (UUID), the body is revisioned and idempotent:

{
"revision": 3,
"agentSessionId": "sess-7f3a",
"startedAt": "2026-10-12T09:00:00Z",
"stoppedAt": "2026-10-12T09:42:10Z",
"projectId": "…", "taskId": null, "tagIds": [], "description": "Claude Code session",
"working": true, "observedAt": "2026-10-12T09:42:08Z",
"final": false,
"usageQuality": "pending",
"usage": [ … ]
}
  • At creation the mapping fields (projectId, taskId, tagIds, description) set the entry. On a later accepted revision, description, taskId and tagIds are applied to the same entry — this is how a running session is renamed instead of being ended and reopened — together with stoppedAt (within the bounds) and usage:
    • description absent or null keeps the stored one; taskId absent keeps the task and an explicit null clears it; tagIds absent or null keeps the tags, [] removes them, a list replaces them.
    • projectId is immutable. A different value answers 422 (error on projectId, code immutable) and nothing of the revision is written; open a new segment to change project. Resending the stored project (what every earlier client does) changes nothing.
    • The applied values are validated like at creation: description ≤ 2000 characters (too_long), a changed taskId must belong to the entry’s project (invalid; required on an entry without project) — both 422. Tag ids that no longer exist are ignored (unlike at creation, where an unknown tag is 422 invalid_tag): a heartbeat never fails because a tag was deleted; the response lists the tags actually applied.
    • A segment locked by a timesheet submission answers 409 entry_locked, a detached one 409 segment_detached. A finalised, non-detached segment can still be renamed but its end cannot move (409 segment_finalized).
    • The 200 body is the up-to-date entry (description, taskId, taskName, tagIds): read it to know that the server applied the rename (an older server ignores these fields and returns the entry unchanged). Replay rules are unchanged, except that the same revision number and content sent once with taskId absent and once with taskId: null (keep vs clear) is 409 agent_revision_conflict, not a replay.
  • working: true with a recent observedAt (≤ 5 min old at reception) shows the owner an “agent active” indicator for 10 minutes.
  • final: true finalises the segment irreversibly: no later stoppedAt, no reactivation (409 segment_finalized); usage and its quality may still change.
  • A human edit of the segment in Tickr detaches it: further automatic writes answer 409 segment_detached; open a new segment.
  • The route answers 404 while the deployment has agent sessions disabled (AGENT_SEGMENTS_ENABLED=false). The signed-in web app reads the flag as agentSegmentsEnabled from GET /api/auth/me (session authentication, not available with an API key) and hides the agent-session interface (counting-policy cards, re-apply shortcuts, display cards, “active agents” indicator, the agent_session origin option) while it is false. The flag is read when the web app loads: after the deployment changes it, reload the page.
  • 503 workspace_busy (with Retry-After) means the workspace was busy — for example a timesheet submission in progress — and nothing was written. Retry the same revision with exactly the same body after Retry-After, with exponential back-off and jitter; never bump the revision or mark it sent because of a 503. The usage endpoint follows the same rule.
  • PUT /api/v1/entries/{id}/agent-usage reads the workspace settings after it acquires the coordination lock: a write queued behind a settings change (for example enabling timesheet validation) is judged with the new settings.

Agent sessions are never running timers and never count toward the 3-timer cap.

Each agent_session entry carries a frozen countsAsWorkTime, decided at creation from a cascade: project › client › workspace (null = inherit; workspace default off). Delegated agent entries and every human origin always count. Non-counted entries are listed and exported with their real duration but excluded from dashboards, monthly stats, tracker suggestions, submissions and client approvals. Billing apply and the rate resnapshot still update non-counted entries (so they stay consistent if they are counted later), but the seconds and amounts they report cover counted entries only.

Reading and changing the policy. These routes are used by the Tickr web app (session authentication); they are not available with an API key.

  • GET|PUT /api/workspaces/me/agent-time-settings — { "countAgentTimeAsWorkTime": true }. Any workspace member can read it; changing it requires Admin or Owner. The PUT body must carry an explicit true or false: an empty body ({}) or null answers 422 (required on countAgentTimeAsWorkTime) and leaves the setting unchanged.
  • PUT /api/projects/{id}/agent-time-policy — { "countAgentTime": true | false | null } (null inherits the client’s policy, then the workspace’s). The field is required: an empty body ({}) answers 422 (required on countAgentTime), a value that is not a boolean or null answers 422 (invalid). Admin or Owner only: any other role gets 403 workspace_admin_required whatever the project id; an unknown project answers 404. It changes the policy and nothing else — prefer it to the project PATCH below.
  • PATCH /api/clients/{id} / PATCH /api/projects/{id} — countAgentTime: true|false, or clearCountAgentTime: true to inherit again (Admin+; the other fields keep their usual permissions). Omitting both leaves the policy untouched. Beware: unlike the client route (a true partial patch), PATCH /api/projects/{id} rewrites clientId and description on every call — an absent clientId detaches the project from its client and an absent description clears it — so always send their current values along with the policy field. A client PATCH carrying countAgentTime or clearCountAgentTime is serialised with the workspace’s entry writes, like the project policy PUT: a segment written at the same time decides with the new policy, and the PATCH may answer 503 workspace_busy (nothing written — retry after Retry-After). A client PATCH without these two fields (the display fields included) is never refused that way.

A policy change applies to new segments. Owners and Admins can re-apply it to existing, unlocked segments:

  1. POST /api/workspaces/{workspaceId}/agent-time-policy/reapply/preview with { "scopeKind": "project" | "client" | "workspace" | "no_project", "projectId"?: …, "clientId"?: …, "from"?: "YYYY-MM-DD", "to"?: "YYYY-MM-DD" } → the plan (entries whose decision changes, with durations), the entries blocked by an already submitted period (never changed), totals, and a confirmationToken valid 15 minutes. project takes projectId only, client takes clientId only (all the client’s projects), workspace and no_project take no id; no_project covers the workspace’s agent sessions without a project. The optional from / to (UTC days, inclusive, either one or both) narrow any scope to the entries that started in [from 00:00Z, to + 1 day 00:00Z); from after to answers 422 invalid_scope (on from). The period is bound into the confirmation token. The scope’s candidate entries are its agent sessions that are stopped, not deleted and not locked (not attached to a non-rejected submission line), inside the period when one is given, whether or not their decision changes. A scope with more than 10 000 candidate entries (configurable by the operator) is refused with 422 reapply_scope_too_large: narrow the scope — per client, per project, no_project for the sessions without a project, or a period.
  2. POST /api/workspaces/{workspaceId}/agent-time-policy/reapply with { "confirmationToken": "…" }. The plan is recomputed under a lock for the same scope and period: any difference — including a change among the blocked entries — answers 409 reapply_plan_changed (preview again), and a scope that has grown past the bound answers 422 reapply_scope_too_large; a replayed token 409 reapply_token_used; a malformed, expired, foreign or unknown token — for example after a server restart or redeploy between preview and confirmation: preview again — 422 invalid_confirmation_token. The confirmation updates unlocked rows only (an entry attached to a non-rejected submission line is never re-applied, whether or not timesheet validation is enabled). A move (project or dates), duration change, deletion or decision change of a planned segment by a person that commits before the confirmation locks the planned rows also answers 409 reapply_plan_changed, with nothing applied; other edits (description, tags, billable) do not affect the decision. A person’s edit whose write lands after the confirmation committed wins over it (its decision is recomputed with the same guards, so a counted segment never lands in a submitted period).

When a member submits a period, only counted entries are covered. A counted segment that is not finalised and was written by its agent less than an hour ago blocks the submission (409 agent_segment_open); an older one is finalised at submission time (usage quality pending → incomplete). Submission lines expose agentSeconds (agent time inside the line total) and hasIncompleteAgentUsage.

Agent activity in the dashboard, reports and exports

Section intitulée « Agent activity in the dashboard, reports and exports »

These routes are used by the Tickr web app (session authentication); they are not available with an API key. They read entries only — nothing here waits on the workspace coordination lock.

Origin filter. GET /api/dashboard/analytics, GET /api/reports/agent-activity (and its /sessions pages) and the entry exports accept origins, a comma-separated list of origin codes (web, api, agent, agent_session, import_toggl, import_tickr, calendar, unknown). Absent or empty means no filter; an unknown code answers 422 invalid_origin (on origins).

Dashboard. The analytics response always carries an agentActivity block: totals, byModel (token usage per provider and model) and topProjects (the five projects with the most agent time). It follows the dashboard’s filters and role scope, covers the current period only and uses raw seconds (no rounding). Dashboard totals exclude agent sessions that do not count as work time; the block shows them, split into counted and notCounted.

Agent activity report. GET /api/reports/agent-activity?from=…&to=…[&projects&users&clients&tags&tasks&description_contains&origins&granularity=day|week&limit=1..200] (default 50; values outside 1..200 are clamped) takes the dashboard’s filters and bounds (from and to required, at most 366 days apart) and returns, over the stopped, non-deleted agent and agent_session entries of the period — counted or not:

  • totals: session time (sessionSeconds, segmentCount, sessionCount), delegated entries (delegatedSeconds, delegatedCount), the four token kinds summed over models plus totalTokens, cacheShare (cache reads / all input-side tokens), incompleteUsageShare (share of the tokens recorded on entries whose usage quality is not complete), and the counted / notCounted split;
  • series: one point per local day or ISO week (Monday) of the workspace timezone that has data;
  • byAgent, byProject, byClient, byMember (null for a workspace_user, who only ever sees their own entries): seconds, entry count and tokens per row (key _null = none);
  • byModel and byProvider: tokens and entry counts only — never a duration;
  • sessions: the first page of the agent sessions, one per (member, agentSessionId), with agent, projects, first start, last stop, segment and delegated counts, seconds, tokens and the worst usage quality, newest first. Next pages: GET /api/reports/agent-activity/sessions?<same filters>&cursor=<page.next_cursor>&limit=…; pass next_cursor unchanged — an invalid cursor restarts at the first page. Both routes accept an optional agentSessionId (exact match, blank = no filter) that narrows every aggregate to that one session, on top of the role scope and the other filters (it never widens them).

The report never exposes whether an agent is working right now (that stays on the owner’s GET /api/me/agent-activity/live), and carries no amounts. Token counters are JSON integers capped at 9 007 199 254 740 991 (the largest integer JavaScript represents exactly): a counter equal to that value means “at least this many”; they are never negative, and the two shares stay between 0 and 1.

CSV and XLSX exports. GET /api/me/workspaces/{workspaceId}/exports/entries.xlsx, GET /api/workspaces/{workspaceId}/exports/entries/csv and its /entries/count accept origins and agentSessionId, and append ten columns after the existing ones (XLSX columns 11–20): origin, agent, agent_version, agent_session_id, counts_as_work_time (true / false), usage_quality, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens (summed over models; empty when the entry has no token usage). Entries that do not count as work time are exported like the others; there is no total row. agentSessionId (camelCase, like GET /api/entries?agentSessionId=) is an exact match on the entry’s agent session id and bounds the export to that session: absent or blank means no filter, an unknown session exports no rows, and it combines (AND) with the other filters.

How agent time appears in the documents a client signs is chosen by Owners and Admins (Tickr web app routes, session authentication):

  • GET /api/workspaces/me/agent-time-settings also returns defaultAgentTimeDisplay (merged by default); PUT /api/workspaces/me/agent-time-settings/display with { "defaultAgentTimeDisplay": "merged" | "split" } changes it. The value is required: {} or null answers 422 (required).
  • PATCH /api/clients/{id} — agentTimeDisplay: "merged" | "split", or clearAgentTimeDisplay: true to inherit the workspace default again (Admin+). agentTimeDisplay is null on a client that inherits.
  • POST /api/clients/{id}/approval-requests takes an optional agentTimeDisplay (default: the client’s, else the workspace’s) and includeAiUsageAnnex (default false). Both are frozen on the request and in its document: later setting changes never alter a request already sent.

merged renders the hours as before. split adds an “of which agent time” column per project and task and in the total: the agent-session time counted as work time, already included in the hours (delegated agent entries are human time). The optional annex lists tokens per model and per project from exactly the approved time — never a duration per model — and marks usage that was incomplete when the request was sent. Amounts stay at the human rate. The public approval view (GET /api/public/client-approval/{token}) returns the frozen document. A merged document — and every document sent before this release — has exactly the fields it had before: no agentTimeDisplay, no agentSeconds. A split document adds snapshot.agentTimeDisplay ("split"), snapshot.agentSeconds and the same field on each project, task and day. snapshot.aiUsageAnnex is present, in either mode, only when the annex was included.

The workspace backup ZIP (schema_version 2) carries each entry’s origin, agent identity and session, counting decision, finalisation, usage quality, detachment and token usage, plus the client, project and workspace agent-time settings. It includes entries that do not count as work time, never the “agent active” indicator. Importing it keeps those decisions as archived (they are not recomputed from the target’s settings) and attaches the usage to the new entries; the workspace-level settings are applied only with overwriteExisting=true. An archive containing agent sessions is refused (422 agent_segments_disabled) while agent sessions are disabled on the server. Archives with schema_version 1 import as before: origin import_tickr, counted, no usage, settings unchanged. A malformed archive of any version (an incomplete manifest.json, a null item, a duplicated id or name, an oversized text, an end before its start, a billable currency without its rate, an agent segment outside its bounds…) is refused with 422 before anything is written.

Status Code When
401 — missing or invalid API key (segment route and usage endpoint)
403 analyst_read_only Analyst calling either PUT route (segment, usage)
403 workspace_admin_required settings PUTs (counting and display), the re-apply routes, the project policy PUT, and the policy / display fields (countAgentTime, clearCountAgentTime, agentTimeDisplay, clearAgentTimeDisplay) of the client / project PATCH
403 workspace_mismatch re-apply routes called with a {workspaceId} that is not the caller’s active workspace
404 — usage endpoint: entry missing / deleted / foreign / not agent; segment endpoint while agent sessions are disabled; project policy PUT: unknown or foreign project
409 entry_locked locked entry, or a new counted segment inside a submitted period
409 agent_session_mismatch, agent_revision_conflict, segment_id_unavailable, segment_detached, segment_finalized segment / usage rules above
409 agent_segment_open submission blocked by an active counted segment
409 reapply_plan_changed, reapply_token_used re-apply confirmation
422 invalid_origin, agent_required, invalid_agent, invalid_usage, invalid_usage_quality, invalid_revision, invalid_tag, invalid_segment_bounds, invalid_scope, reapply_scope_too_large, invalid_confirmation_token, required, invalid, invalid_granularity validation (errors[]; required: the settings PUT without an explicit countAgentTimeAsWorkTime, or the project policy PUT without countAgentTime; invalid: the project policy PUT with a countAgentTime that is neither true, false nor null; invalid_scope also covers a re-apply period whose from is after its to) · report / dashboard / export query parameters: invalid_origin (origins), invalid_granularity (granularity), from_to_required, invalid_period, period_too_large, invalid_guid_in_filter
422 invalid_value, required display settings: an integer outside the enum (invalid_value), or the display PUT without defaultAgentTimeDisplay (required); an unknown name ("foo") is refused earlier with 400 by the JSON binder
422 agent_segments_disabled, invalid_origin, invalid_value, invalid_agent, invalid_usage_quality, invalid_revision, invalid_usage, agent_session_not_resumable, invalid_segment_bounds, null_element, duplicate_id, duplicate_name, invalid_manifest workspace archive import blockers (errors[].field names the location in the file, e.g. time_entries.origin; an agent_last_write_at later than the importing server’s clock is not refused: it is restored as the server’s current time; client names that are equal for the database’s case-insensitive citext (it compares lower() of both sides; the import asks the database itself, so the Kelvin sign, U+0130 “İ” vs “i” and the like are equated exactly as the unique index does) are the same client — duplicate_name on clients.name inside one archive, reused against the target)
422 invalid_value entry create / PATCH / bulk / timer stop: an entry spanning more than 2 147 483 647 seconds (~68 years) — errors[].field is stoppedAt (items[i].stoppedAt for bulk, startedAt when only the start was moved); timer start, an entry PATCH sending startedAt or clearStop that leaves the entry running, and a bulk item with stoppedAt: null: a running entry that started more than that long ago (it could never be stopped) — field startedAt (items[i].startedAt); the archive import refuses the same span on time_entries.stopped_at (the Toggl CSV import refuses it at pre-flight too, invalid_value on rows.stoppedAtLocal, the details listing the row indices)
429 — rate limit exceeded: the re-apply routes (preview and confirmation share one bucket) use the agent-policy-reapply policy, 20 requests per minute per user; the /api/v1 routes (segment, usage) keep their v1 policy
503 workspace_busy the workspace coordination lock stayed busy longer than the server’s budget; nothing written — retry the same request after Retry-After (see Errors)