Skip to content

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.

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.

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).
curl -X POST "https://<your-instance>/api/v1/agent-entries" \
  -H "Authorization: Bearer spantail_aat_youragenttoken" \
  -H "Content-Type: application/json" \
  -d '{
    "sessionId": "sess_8f21",
    "durationMinutes": 42,
    "projectId": "prj_site",
    "description": "Refactored the auth middleware",
    "usage": { "totalTokens": 18400, "inputTokens": 12000, "outputTokens": 6400 },
    "startedAt": "2026-06-28T09:00:00.000Z",
    "endedAt": "2026-06-28T09:42:00.000Z"
  }'

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.
curl -X POST "https://<your-instance>/api/v1/agent-events" \
  -H "Authorization: Bearer spantail_aat_youragenttoken" \
  -H "Content-Type: application/json" \
  -d '{
    "sessionId": "sess_8f21",
    "projectId": "prj_site",
    "events": [
      {
        "sourceId": "msg_01a",
        "timestamp": "2026-06-28T09:00:00.000Z",
        "model": "claude-opus-4",
        "usage": { "input_tokens": 8000, "output_tokens": 3200 },
        "attributes": { "vcs.ref.head.name": "feature/auth-refactor" }
      },
      {
        "sourceId": "msg_01b",
        "timestamp": "2026-06-28T09:12:00.000Z",
        "model": "claude-opus-4",
        "usage": { "input_tokens": 4000, "output_tokens": 3200 },
        "attributes": { "vcs.ref.head.name": "feature/auth-refactor" }
      }
    ]
  }'

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.
curl -X POST "https://<your-instance>/api/v1/agent-events/finalize" \
  -H "Authorization: Bearer spantail_aat_youragenttoken" \
  -H "Content-Type: application/json" \
  -d '{
    "sessionId": "sess_8f21",
    "endedAt": "2026-06-28T09:15:00.000Z",
    "description": "Refactored the auth middleware",
    "context": { "refs": ["github:acme/site#128"] }
  }'

The response is the updated agent session, in the same shape as the ingest responses.

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).
curl "https://<your-instance>/api/v1/agent-entries?workspaceId=wrk_demo&agentId=agt_bot" \
  -H "Authorization: Bearer spantail_pat_yourtoken"

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).
curl -X POST "https://<your-instance>/api/v1/agent-entries/delete" \
  -H "Authorization: Bearer spantail_pat_yourtoken" \
  -H "Content-Type: application/json" \
  -d '{ "workspaceId": "wrk_demo", "ids": ["ae_01K2", "ae_01K3"] }'

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.
{
  "totalMinutes": 540,
  "totalTokens": 184000,
  "totalInputTokens": 120000,
  "totalOutputTokens": 64000,
  "entryCount": 12,
  "byDate": [
    { "date": "2026-06-28", "minutes": 42, "tokens": 18400, "count": 1, "inputTokens": 12000, "outputTokens": 6400 }
  ],
  "byAgent": [
    { "agentId": "agt_bot", "minutes": 540, "tokens": 184000, "count": 12 }
  ]
}

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.

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.

[
  { "id": "agt_bot", "type": "claude_code", "name": "Build bot" }
]