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.
Declaring an agent (delegated entries)
Section intitulée « Declaring an agent (delegated entries) »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: agentTickr-Agent: claude-code/2.1.0Tickr-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.
Attaching token usage
Section intitulée « Attaching token usage »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.
Automatic agent sessions
Section intitulée « Automatic agent sessions »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,taskIdandtagIdsare applied to the same entry — this is how a running session is renamed instead of being ended and reopened — together withstoppedAt(within the bounds) and usage:descriptionabsent ornullkeeps the stored one;taskIdabsent keeps the task and an explicitnullclears it;tagIdsabsent ornullkeeps the tags,[]removes them, a list replaces them.projectIdis immutable. A different value answers422(error onprojectId, codeimmutable) 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 changedtaskIdmust belong to the entry’s project (invalid;requiredon an entry without project) — both422. Tag ids that no longer exist are ignored (unlike at creation, where an unknown tag is422 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 one409 segment_detached. A finalised, non-detached segment can still be renamed but its end cannot move (409 segment_finalized). - The
200body 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 withtaskIdabsent and once withtaskId: null(keep vs clear) is409 agent_revision_conflict, not a replay.
working: truewith a recentobservedAt(≤ 5 min old at reception) shows the owner an “agent active” indicator for 10 minutes.final: truefinalises the segment irreversibly: no laterstoppedAt, 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
404while the deployment has agent sessions disabled (AGENT_SEGMENTS_ENABLED=false). The signed-in web app reads the flag asagentSegmentsEnabledfromGET /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, theagent_sessionorigin option) while it isfalse. The flag is read when the web app loads: after the deployment changes it, reload the page. 503 workspace_busy(withRetry-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 afterRetry-After, with exponential back-off and jitter; never bump the revision or mark it sent because of a503. The usage endpoint follows the same rule.PUT /api/v1/entries/{id}/agent-usagereads 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.
Does agent time count as work time?
Section intitulée « Does agent time count as work time? »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. ThePUTbody must carry an explicittrueorfalse: an empty body ({}) ornullanswers422(requiredoncountAgentTimeAsWorkTime) and leaves the setting unchanged.PUT /api/projects/{id}/agent-time-policy—{ "countAgentTime": true | false | null }(nullinherits the client’s policy, then the workspace’s). The field is required: an empty body ({}) answers422(requiredoncountAgentTime), a value that is not a boolean ornullanswers422(invalid). Admin or Owner only: any other role gets403 workspace_admin_requiredwhatever the project id; an unknown project answers404. It changes the policy and nothing else — prefer it to the projectPATCHbelow.PATCH /api/clients/{id}/PATCH /api/projects/{id}—countAgentTime: true|false, orclearCountAgentTime: trueto 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}rewritesclientIdanddescriptionon every call — an absentclientIddetaches the project from its client and an absentdescriptionclears it — so always send their current values along with the policy field. A clientPATCHcarryingcountAgentTimeorclearCountAgentTimeis serialised with the workspace’s entry writes, like the project policyPUT: a segment written at the same time decides with the new policy, and thePATCHmay answer503 workspace_busy(nothing written — retry afterRetry-After). A clientPATCHwithout 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:
POST /api/workspaces/{workspaceId}/agent-time-policy/reapply/previewwith{ "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 aconfirmationTokenvalid 15 minutes.projecttakesprojectIdonly,clienttakesclientIdonly (all the client’s projects),workspaceandno_projecttake no id;no_projectcovers the workspace’s agent sessions without a project. The optionalfrom/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);fromaftertoanswers422 invalid_scope(onfrom). 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 with422 reapply_scope_too_large: narrow the scope — per client, per project,no_projectfor the sessions without a project, or a period.POST /api/workspaces/{workspaceId}/agent-time-policy/reapplywith{ "confirmationToken": "…" }. The plan is recomputed under a lock for the same scope and period: any difference — including a change among the blocked entries — answers409 reapply_plan_changed(preview again), and a scope that has grown past the bound answers422 reapply_scope_too_large; a replayed token409 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 answers409 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).
Submissions and agent sessions
Section intitulée « Submissions and agent sessions »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 plustotalTokens,cacheShare(cache reads / all input-side tokens),incompleteUsageShare(share of the tokens recorded on entries whose usage quality is notcomplete), and thecounted/notCountedsplit;series: one point per local day or ISO week (Monday) of the workspace timezone that has data;byAgent,byProject,byClient,byMember(nullfor aworkspace_user, who only ever sees their own entries): seconds, entry count and tokens per row (key_null= none);byModelandbyProvider: 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=…; passnext_cursorunchanged — an invalid cursor restarts at the first page. Both routes accept an optionalagentSessionId(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.
Client-approval documents
Section intitulée « Client-approval documents »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-settingsalso returnsdefaultAgentTimeDisplay(mergedby default);PUT /api/workspaces/me/agent-time-settings/displaywith{ "defaultAgentTimeDisplay": "merged" | "split" }changes it. The value is required:{}ornullanswers422(required).PATCH /api/clients/{id}—agentTimeDisplay: "merged" | "split", orclearAgentTimeDisplay: trueto inherit the workspace default again (Admin+).agentTimeDisplayisnullon a client that inherits.POST /api/clients/{id}/approval-requeststakes an optionalagentTimeDisplay(default: the client’s, else the workspace’s) andincludeAiUsageAnnex(defaultfalse). 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.
Workspace archive
Section intitulée « Workspace archive »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) |