Agent ingest
The ingest endpoints capture AI coding-agent activity as agent sessions — one row of
agent_entries per agent and session. An agent writes them with its Agent
Access Token (AAT), passed as Authorization: Bearer spantail_aat_….
There are two write paths into the same agent_entries row, and a session is fed by exactly
one of them:
POST /agent-entries— the agent posts a client-computed session summary (e.g. Cursor).POST /agent-events— the agent posts raw per-turn telemetry; the server rolls it up into the session entry (e.g. Claude Code via a Stop hook).
An events-fed session can additionally be finalized (POST /agent-events/finalize) with
closing facts when the session ends — this supplements the entry without touching the
events-derived rollup.
Reads go through the regular user credentials (session or PAT) and follow the project ACL.
The agent session object
Section titled “The agent session object”| Field | Type | Description |
|---|---|---|
id |
string | Unique identifier. |
workspaceId |
string | Owning workspace. |
ownerUserId |
string | The user the agent acts for. |
projectId |
string | null | Project the session was logged against, or null (workspace-level work, or the project was deleted). |
agentId |
string | The agent that produced the session. |
sessionId |
string | External session identifier (idempotency key within the agent). |
entryDate |
string | Local date YYYY-MM-DD, derived from startedAt in the viewer’s timezone at read time. |
durationMinutes |
integer | Session duration in minutes. |
usage |
object | null | Token-usage totals (see below), or null when the source can’t expose them. |
context |
object | null | Non-usage session context (see below), or null when none was captured. |
eventCount |
integer | null | Number of events the rollup was computed from, or null on summary-path sessions (which carry no events). |
description |
string | null | Optional short summary (≤ 2000 chars). |
startedAt |
string | null | ISO 8601 session start. |
endedAt |
string | null | ISO 8601 session end. |
createdAt |
string | ISO 8601 creation instant. |
updatedAt |
string | ISO 8601 last-update instant. |
The usage object carries session token totals. Agents differ in which buckets they expose,
so only totalTokens is required:
| Field | Type | Required | Description |
|---|---|---|---|
totalTokens |
integer | yes | Total tokens for the session. |
inputTokens |
integer | no | Input tokens. |
outputTokens |
integer | no | Output tokens. |
cacheCreationTokens |
integer | no | Cache-creation tokens. |
cacheReadTokens |
integer | no | Cache-read tokens. |
model |
string | no | Model name (≤ 100 chars). |
costUsd |
number | no | Cost in USD, only when the source provides it (summed from per-event costs on the events path). |
The context object carries distinct non-usage facets of the session. Each facet is a list
of up to 20 strings (each ≤ 200 chars). On the events path the server derives models,
branches, and repositories from the events; refs always comes from the client:
| Field | Type | Description |
|---|---|---|
models |
string[] | Distinct models seen in the session, in first-seen order. |
branches |
string[] | Distinct git branches (from the vcs.ref.head.name event attribute). |
repositories |
string[] | Distinct repository URLs (from the vcs.repository.url.full event attribute). |
refs |
string[] | Opaque external references extracted by the client (e.g. github:owner/repo#123). The server never interprets the format. |
Ingest a session summary
Section titled “Ingest a session summary”POST /api/v1/agent-entries agent token
Records one agent session. Idempotent on (agent, sessionId): re-sending the same session
updates the row instead of inserting a duplicate, so retries and batch reconciliation never
double-count.
workspaceId is required: the token carries no workspace, so a payload that names none is
rejected with 400 (the plugin resolves it from the repository link). projectId has no
default anywhere — omitting it records the work at the workspace level. entryDate is never sent —
it is derived from startedAt at read time. When startedAt is omitted it falls back to
endedAt, then to ingest time.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
sessionId |
string | yes | External session id (1–200 chars). |
durationMinutes |
integer | yes | Session duration in minutes (≥ 0). |
workspaceId |
string | yes | Target workspace. A payload naming none is rejected. |
projectId |
string | no | Target project (must belong to the workspace). Omit for workspace-level work. |
usage |
object | no | Token-usage totals (see above). |
context |
object | no | Non-usage session context (see above); the summary path supplies every facet itself. |
description |
string | no | Short summary (≤ 2000 chars). |
startedAt |
string | no | ISO 8601 session start. |
endedAt |
string | no | ISO 8601 session end (must not precede startedAt). |
Ingest per-turn events
Section titled “Ingest per-turn events”POST /api/v1/agent-events agent token
Posts a session’s raw per-turn telemetry: one event per assistant message. The server
idempotently inserts new events, then recomputes the session’s rollup into the same
agent_entries row the summary path writes. Idempotent on (agent, sourceId): re-posting
the cumulative transcript every turn is safe — seen events are no-ops and the rollup is
recomputed (not incremented), so totals converge.
workspaceId is required, exactly as on the summary path; projectId has no default
anywhere (omitting it records workspace-level work). The same live delegation checks as the
summary path apply.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
sessionId |
string | yes | External session id (1–200 chars). |
events |
object[] | yes | 1–5000 per-turn events (see below). |
workspaceId |
string | yes | Target workspace. A payload naming none is rejected. |
projectId |
string | no | Target project (must belong to the workspace). |
Each event is one assistant turn (one API response, one usage block):
| Field | Type | Required | Description |
|---|---|---|---|
sourceId |
string | yes | Transcript message id (1–200 chars); the idempotency key within the agent. |
timestamp |
string | yes | ISO 8601 (UTC) time of the message. |
usage |
object | yes | The agent’s native usage object, stored verbatim. |
model |
string | no | Model name (≤ 100 chars). |
operation |
string | no | What the event records, in gen_ai.operation.name terms. Defaults to chat (one inference turn). |
costUsd |
number | no | Cost of this turn in USD, when the source provides it. Summed into the session’s usage.costUsd. |
attributes |
object | no | Non-usage metadata: up to 20 string/number/boolean values (strings ≤ 500 chars), keyed by OTel attribute names where one exists. |
Recommended attributes keys — the server validates bounds, not names, so anything within
the limits is stored; these are the keys it aggregates into the entry’s context:
| Key | Meaning |
|---|---|
vcs.ref.head.name |
Git branch the agent worked on. |
vcs.repository.url.full |
Repository URL (e.g. from git remote get-url origin). |
process.working_directory |
Working directory of the session. |
app.version |
Agent client version. |
request.id |
Provider request id, for traceability. |
Finalize a session
Section titled “Finalize a session”POST /api/v1/agent-events/finalize agent token
Supplements an events-fed session with closing facts when it ends (e.g. from Claude
Code’s SessionEnd hook): the wall-clock end, a summary description, and external references.
The usage rollup stays derived from events — a finalize never changes it, and a late event
re-post never erases the finalized facts. endedAt never moves the entry backward: it is
clamped so the session cannot end before it started or earlier than already recorded.
Finalizing is best-effort: if the session has no entry yet (its events never arrived), the
request returns 404 and the client may simply ignore it.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
sessionId |
string | yes | External session id (1–200 chars). |
workspaceId |
string | yes | Target workspace. A payload naming none is rejected. |
endedAt |
string | no | ISO 8601 wall-clock session end. |
description |
string | no | Session summary (≤ 2000 chars). |
context |
object | no | Only the refs facet — the other facets are server-derived from events and cannot be overridden here. |
The response is the updated agent session, in the same shape as the ingest responses.
List agent sessions
Section titled “List agent sessions”GET /api/v1/agent-entries scope: read
Returns agent sessions in a workspace, newest first. The read scope is resolved server-side from the caller’s membership: a member sees agent activity in the projects they belong to plus their own agents’ activity (unassigned activity stays owner-only), while a workspace or instance admin sees all agent activity in the workspace.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
workspaceId |
string | yes | Workspace to list from. |
agentId |
string | no | Filter to a single agent. |
from |
string | no | Inclusive start date YYYY-MM-DD. |
to |
string | no | Inclusive end date YYYY-MM-DD. |
limit |
integer | no | Page size, 1–200 (default 50). |
offset |
integer | no | Rows to skip (default 0). |
Delete sessions
Section titled “Delete sessions”POST /api/v1/agent-entries/delete scope: write
Bulk-deletes agent sessions you own. It is a POST with a body (not DELETE, whose bodies
intermediaries handle poorly) and is all-or-nothing: if any id is missing, belongs to
another owner, or is outside the given workspace, nothing is deleted and the call returns
404 (never 403, so foreign ids are never confirmed to exist). Owner-only — an admin cannot
delete another user’s sessions.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
workspaceId |
string | yes | Workspace the sessions belong to. |
ids |
string[] | yes | Session ids to delete (deduplicated). |
GET /api/v1/agent-entries/stats scope: read
Returns aggregated totals for the same filter set as listing, under the same ACL. Unlike the
list, the from/to date window is required — it bounds the scan that buckets rows by
day in the viewer’s timezone. No pagination.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
workspaceId |
string | yes | Workspace to aggregate. |
agentId |
string | no | Filter to a single agent. |
from |
string | yes | Inclusive start date YYYY-MM-DD. |
to |
string | yes | Inclusive end date YYYY-MM-DD. |
Agents that don’t expose token buckets contribute 0 to the input/output splits, so their
sum can be less than totalTokens. Only dates that have entries appear in byDate.
List agents in a workspace
Section titled “List agents in a workspace”GET /api/v1/agent-entries/agents scope: read
Returns the caller’s own agents under a workspace — those with activity here plus the ones
registered to it — for populating a filter. workspaceId is required.