Read time and todo rollups for the workspace
Time and todo rollups over one window, for one workspace.
/v1/time-tracking/summaryViewer- Operation id
- time_tracking_summary
- Access
- Any workspace role can call this.
/v1/time-tracking/summaryTime and todo rollups over one window, for one workspace.
Answers how much time was logged, by whom, on what day and for which team,
plus the state of the workspace's todos. since_days is 1 to 180 and
defaults to 14.
The groupings all add up. Per agent, per day and per team minutes each
sum to total, and billable plus non-billable equals the total at every
level. A row whose agent, day or team is unknown is grouped under a null
key rather than dropped, which is what keeps the sums honest.
An empty window returns zeroes, never an error and never a missing key.
todos.by_ticket is capped, and by_ticket_limit reports the cap so a
caller can tell a short list from a truncated one. No other grouping is
capped.
user_id and team_id narrow everything. For todos, user_id reads as
the todo's assignee and team_id as the owning ticket's team.
Conservation is the contract (FR-31, AC-35, AC-36). Every grouping is
computed from the same filtered row set as total, so per-agent, per-day
and per-team minutes each sum to the reported total, and billable plus
non-billable equals the level's own total. A row whose agent, day or team is
unknown is grouped under a null key rather than dropped. Dropping it is the
one thing that would break the sum, and it is what an inner join instead of
a LEFT JOIN would quietly do.
Sums are accumulated in bigint. SUM(integer) already returns one; the
previous ::int cast narrowed it back down, which is a ceiling nothing hits
at 500 tickets and everything hits eventually.
user_id and team_id narrow the window; a filter the endpoint cannot
honour is rejected rather than dropped, so a caller never reads unfiltered
numbers as though they were filtered. For todos, user_id reads as the
todo's assignee and team_id as the owning ticket's team: the same two
questions, asked of the other table.
Tenancy: the request is scoped to one workspace, and the credential presented is what selects it. A caller with no membership in that workspace is refused with 404, which is indistinguishable from a workspace that does not exist.
Authorisation: any workspace role, including viewer. Reads are open to every member.
Access: Authorization: Bearer carrying either a realm access token or an API key. A key must hold the helpdesk-ingress:read scope, and the workspace role of the person who owns it must meet the floor above.
A realm-issued access token, presented as Authorization: Bearer <token>. It acts as the person who signed in, and every operation resolves their workspace role live from the membership record.
In: header
Query Parameters
1 <= value <= 18014Response Body
application/json
application/json
application/json
curl -X GET "https://example.com/v1/time-tracking/summary"{ "since_days": 14, "total": { "entries": 42, "minutes": 1860, "billable_minutes": 1440, "non_billable_minutes": 420 }, "by_agent": [ { "user_id": "3fe98b76-2409-5003-b75a-93e56b450433", "user_name": "Dana Osei", "entries": 30, "minutes": 1320, "billable_minutes": 1080, "non_billable_minutes": 240 }, { "entries": 12, "minutes": 540, "billable_minutes": 360, "non_billable_minutes": 180 } ], "by_day": [ { "day": "2026-08-13", "minutes": 240, "billable_minutes": 180, "non_billable_minutes": 60 }, { "day": "2026-08-14", "minutes": 1620, "billable_minutes": 1260, "non_billable_minutes": 360 } ], "by_team": [ { "team_id": "d907a188-148a-5522-82a6-55798ccdecb6", "team_name": "Billing", "minutes": 1860, "billable_minutes": 1440, "non_billable_minutes": 420 } ], "todos": { "total": { "total": 31, "open": 9, "done": 22, "overdue": 2 }, "by_assignee": [ { "assignee_id": "3fe98b76-2409-5003-b75a-93e56b450433", "assignee_name": "Dana Osei", "total": 31, "open": 9, "done": 22, "overdue": 2 } ], "by_ticket": [ { "ticket_id": "5e8b0c31-7a4d-4f92-b6c8-1d3e2a5f7b90", "ticket_subject": "Card declined at checkout", "total": 3, "open": 1, "done": 2, "overdue": 0 } ], "by_ticket_limit": 100 }}