Docs Read API

Read API: sessions and calls

The /api routes the viewer and your scripts read, their auth and errors: projects, sessions, events, outlines, replays, blobs and model calls.

Read API

All /api/* routes take a read key or basic auth, answer JSON with Cache-Control: no-store, and share these errors:

StatusWhenBody
400a bad parameter{"error":"limit must be between 1 and 500","status":400}
401no valid read credential{"error":"missing or invalid token","status":401}
403the server binds loopback and the Host header is not loopback (DNS-rebinding guard){"error":"forbidden host","status":403}
404unknown route, a session outside the key's project, or a session id with a raw / (the error says to send %2F){"error":"not found","status":404}
405wrong method{"error":"method not allowed","status":405}

Times are epoch milliseconds. since is inclusive and until exclusive, both on a session's start.

The examples use $CSH (the server's base URL) and $READ_KEY from Basics.

GET /api/projects

Project ids the server knows: default, every project in the config, every project named by a configured key, and every project that has sessions in the store (so a --role viewer replica with only a * key still lists them all). A scoped read key gets only its own project.

shell
curl -sS "$CSH/api/projects" -H "Authorization: Bearer $READ_KEY"
json
{"items":["default","support","team"]}

GET /api/sessions

Sessions, newest first.

ParameterDefaultMeaning
limit501 to 500
offset0rows to skip
cursornextCursor from the previous page; steadier than offset while data arrives
projectallone project (* read keys only; scoped keys are always filtered)
scopealltop leaves out subagent sessions
parentonly children of this session id
agent, model, flag, repoexact match on the session
since, untilstart time range
qsubstring of title, id, repo, branch or model (up to 500 characters)
wherea --where query, and-ed with the other filters
shell
curl -sS "$CSH/api/sessions?project=team&limit=1" -H "Authorization: Bearer $READ_KEY"
json
{
  "items": [{
    "id": "team/triage-12", "agent": "triage-agent", "startedAt": 1793880000000, "endedAt": 1793880001200,
    "stats": {"events": 3, "turns": 1, "toolCalls": 0, "toolErrors": 0, "filesChanged": 0,
      "usage": {"inputTokens": 410, "outputTokens": 38, "costUsd": 0.001405},
      "latency": {"wallMs": 1200, "llmCalls": 1, "llmMs": 1200, "llmP50Ms": 1200, "llmP95Ms": 1200}},
    "flags": [], "model": "gpt-4o", "traceIds": ["4bf92f3577b34da6a3ce929d0e0e4736"], "project": "team", "childCount": 0
  }],
  "limit": 1, "offset": 0, "hasMore": true, "nextCursor": "1793880000000:team/triage-12"
}

childCount is the number of subagent sessions under each row. nextCursor is present only when hasMore is true; pass it back as cursor for the next page:

shell
curl -sS "$CSH/api/sessions?project=team&limit=1&cursor=1793880000000:team/triage-12" -H "Authorization: Bearer $READ_KEY"

That page holds team/chat-1.

With where, rows follow the query's sort: (newest first without one) and pages go by offset; cursor answers 400. A bad query answers 400 with reason, the 0-based offset of the problem and next:

shell
curl -sS "$CSH/api/sessions?where=agent:codex%20modle:x" -H "Authorization: Bearer $READ_KEY"
json
{"error":"where: unknown key \"modle\"","status":400,"reason":"unknown_key","offset":12,"next":["did you mean model:?"]}

GET /api/sessions/{id}

One session, the same shape as a list row without childCount. Percent-encode the id.

shell
curl -sS "$CSH/api/sessions/team%2Fchat-1" -H "Authorization: Bearer $READ_KEY"
json
{"id":"team/chat-1","agent":"support-bot","startedAt":1793872800000,"stats":{"events":5,"turns":1,"toolCalls":1,"toolErrors":0,"filesChanged":0,"usage":{"inputTokens":820,"outputTokens":46,"costUsd":0.00251,"estimatedCostUsd":0.00251},"latency":{"wallMs":3000,"llmCalls":1,"llmMs":900,"llmP50Ms":900,"llmP95Ms":900,"toolMs":400,"toolP95Ms":400}},"flags":[],"model":"gpt-4o","title":"Where is order 1042?","endedAt":1793872803000,"project":"team"}

The fields below are grouped by topic: cost, parent and model, model switches, proxy calls, API errors, waste and cache breaks, launch and forks, hooks and redaction, memory and instruction files, unapproved actions, and tool errors and fold errors.

Cost.

FieldMeaning
stats.usage.costUsdThe reported or gateway-billed cost where there is one, else token counts times list price
stats.usage.estimatedCostUsdThe part of costUsd that was estimated (in the example, all of it)
stats.usage.estimatedCostByTierThe estimate totalled by priceTier. Left out when every call was standard
stats.duplicateUsage (calls, tokens, costUsd)Model-call usage left out because an enclosing call or an earlier copy of the same response already counted it

Each estimated llm.call has payload.costEstimated: true. It adds payload.pricedAs when a sibling model's price was used, and payload.priceTier when it was priced at batch, flex, priority, fast, long-context or US-only rates (batch+long-context for both).

Parent and model.

FieldMeaning
parentSet on a child session: sessionId, plus eventId, subagentId and type when known
parent.typeThe subagent type (explore, …), or "harness" for an agent started by another agent's shell tool (codex exec from Claude Code's Bash tool)
modelThe model that made most of the main-thread model calls (calls without a subagentId; the earlier one on a tie), else the last model any event named
stats.modelsModel calls per model, subagent calls included ({"claude-sonnet-5-5": 3, "claude-sonnet-5": 2}). Absent when one model or none made them
stats.modelSwitchThe session's first model switch, described below

A harness link named by the child's hook (spawnedBy) has no eventId. It is set only when that session is in the store and was open when the child started, and the child's session.start payload does not carry it.

Model switches. stats.modelSwitch (from, to, at, trigger) is the last call on from, then the first call on to at at, in the same scope (the main thread, or one subagent). A change is a switch when:

  • the agent recorded a fallback for it,
  • it is inside a subagent (a Claude Code subagent's own session included), or
  • the main thread handed over: every main-thread call on from came before the first call on to.

Without a trigger or a record, each main-thread model needs at least two calls. So a helper model's single call before or after the others (a title) is not a switch, and a helper model whose calls run between the main model's never is. A switch and back is one switch.

FieldMeaning
triggerfallback for a recorded fallback (with reason, such as overloaded or refusal); refusal or error when the last call on from stopped on a refusal or failed; command when a /model command came right before the first call on to; absent otherwise
subagenttrue when the first switch was in a subagent
countThe number of switches, when there was more than one
switchedCallsCalls that ran on a model a switch moved to, until the scope went back
extraUsdWhat those calls cost at their model's list prices, minus the same tokens at from's list prices (standard rates, so negative when the other model is cheaper). Absent when either model has no price
  • A switch adds the model:switch tag to flags.
  • Claude Code's model_fallback and model_refusal_fallback transcript records become a notification with payload.kind: "model.fallback" and add the model:fallback tag.
  • A call whose payload.stopReason is refusal, content_filter or safety (any case) adds the refusal flag.
  • Calls logged by coding.sh proxy count here only in a session with no other model calls, as they do for usage.

Proxy calls. Model calls logged by coding.sh proxy have source: "proxy". Their payload adds these fields. reasoningEffort through compaction appear when the request or response has them, and parentSessionId through upstreamAuth when the client names them.

FieldMeaning
provider, apiapi is messages, count_tokens, chat or responses
statusCode, stream, ttfbMs, ttftMs, stopReasonThe answer's status, whether it streamed, time to first byte and to first token, and the stop reason
systemHash, toolsHash16 hex characters each
toolNames, toolCount, messageCountThe request's tools and messages
promptThe last user message, clipped
linkedByHow the call found its session: header, metadata, previous-response or conversation
rlRequestsRemaining, rlTokensRemaining, …Rate limits, when the API sent them
errorClassOn failure; values below
requestRef, responseRefThe stored request and response blobs. Absent in metadata mode
requestPagedtrue when the request's messages are stored in blocks, not inside the request blob (show <session>#<n> puts them back)
reasoningEffortFrom reasoning.effort, reasoning_effort or output_config.effort
serviceTier, speedThe response's value, else the request's. requestedServiceTier when the request asked for another tier
thinkingBudgetThe request's thinking budget
compactiontrue on a call that compacted the conversation (Codex's /responses/compact, or a compaction block or output item)
parentSessionIdNamed by the client: the session that started this one. For a Claude Code subagent, whose calls go to <session>:sub:<agentId>, the parent's
agentId, parentAgentId, agentTypeNamed by Claude Code
forkedFromSessionId, subagentKindNamed by Codex
accountHash16 hex characters of the sha256 of the account id
upstreamAuthCLIProxyAPI's credential index
gatewayThe gateway's id (for example cliproxyapi), when the upstream the call went on to is a known gateway. It tags the session gateway:<id>

A session seen only through the proxy takes its parent from its calls' parentSessionId. When the session also has model calls from the agent's transcript or OpenTelemetry, those count toward stats and the proxy's copies are detail only. Failed proxy calls count either way.

API errors.

FieldMeaning
stats.llmErrors (count, statusCode, error)llm.call events with ok: false or a status of 400 or more, with the first failure's status and error. Absent when none failed

errorClass is one of rate_limit (429), overloaded (529), auth (401, 403), too_large (413), timeout, server (other 5xx), invalid_request (other 4xx), network and api.

Each API error Claude Code retried (a transcript's api_error line) is an error event with source: "api". A backfill also keeps what the transcript records about it:

FieldMeaning
statusCode, errorTypeThe HTTP status and the API's error type (rate_limit_error, overloaded_error, …)
errorClassAs above
rateLimitTypeThe limit that was hit, such as seven_day or five_hour
resetsAtWhen the limit resets (ms)
retryAfterMsHow long Claude Code waited before the next attempt
retryAttempt, maxRetriesWhich attempt this was, out of how many

A weekly-limit stall then reads as hundreds of rate_limit errors on seven_day, not as one transient error.

Waste and cache breaks. Both stats are absent when there is nothing to report.

FieldMeaning
stats.waste (duplicateFetches, duplicateFetchTokens, readOnlyCalls)Read-only calls that repeated an earlier call in the same context with the same result, and the tokens they sent again (output length / 4)
stats.cacheBreaks (count, lostTokens, extraUsd, byCause)Model calls that found the prompt cache gone: the previous prompt was over 4,096 tokens, this call read less than half of it from cache, and more than 2,048 tokens were billed again
stats.cacheBreaks.byCausecompaction, model-switch, idle (5 minutes, or an hour after a 1-hour cache write) and other
stats.cacheBreaks.byIdleIdle breaks by where the wait was: wait (one tool call in the same scope ran past the cache lifetime, such as a Bash command polling a job), subagent (a subagent sat idle between turns) or away (the main session did). Absent when no break was idle
stats.cacheBreaks.extraUsdThe cache-write (or input) price minus the cache-read price for those tokens. Left out when the model has no price; unpriced then counts the breaks
  • --where cache.breaks>0, cache.lost (the re-billed tokens) and cache.cost (extraUsd) query and sort on them, e.g. coding.sh sessions --where 'cache.breaks>0 sort:-cache.cost'.
  • A session that wrote a prompt cache of at least one block over at least three turns, but read back fewer tokens than it wrote, is tagged cache:low.
  • coding.sh usage --cache reports the hit rate, breaks and their cost per group. exp compare adds the cache breaks / run and cache waste / run metrics.

Launch and forks.

FieldMeaning
stats.launchheadless for a session run without a person at the keyboard (claude -p, the Claude Agent SDK, codex exec, Codex as an MCP server), interactive for a terminal or IDE session. From Claude Code's entrypoint and Codex's originator and launch (details). Absent when the start does not say. Used by --where launch:headless and --facets launch
stats.inheritedTokensOn a Claude Code fork subagent (subagent_type: "fork"): the tokens of the parent's context it started with, the prompt and output of the parent's call that started it. Absent for other sessions
  • A fork subagent's session holds only its own model calls, so the parent's calls count once, in the parent. The fork's reads of the inherited context from the prompt cache count in its usage.

Hooks and redaction.

FieldMeaning
stats.hookContext (count, tokens, bySource)Texts hooks added to the model's context (Claude Code), and their size in tokens (characters / 4). Absent when no hook added context
stats.hookContext.bySourceWhose hook it was: plugin:<name>, plugin (a plugin not installed where the session was parsed), settings (a hook in a settings file) or unknown (the agent recorded no command). Each source adds a hook:<source> tag, such as hook:plugin:terse
stats.redactionsValues redaction replaced before storage, by rule kind ({"anthropic-key": 1, "password": 2}). Never holds the values. Absent when nothing was redacted
  • Redaction counts per event are in payload.redactions, which metadata mode keeps. A tool output moved to a blob counts what was redacted inside the blob.
  • A vendor token kind adds the secret:exposed tag to flags.
  • A vendor-shaped value that is plainly made up counts as placeholder-key, which adds no tag: a published example key, EXAMPLE, test or stub after the prefix, a repeated character or chunk, or eight consecutive characters.
  • Events stored before redaction counting existed are counted from the [redacted:<kind>] markers in their payload.

Memory and instruction files. stats.memoryUse lists the memory and instruction files the session's events name (AGENTS.md and the other agents' instruction files, Claude auto-memory, rules, skills). It is what coding.sh harness audit counts.

FieldMeaning
useloaded, read or wrote
path, tsThe file's absolute path, and the last time of that use
skill, byInstead of path, for a skill an event names without a path: its name, and tool (a Skill tool call) or harness (the agent's own record of loaded skills)
  • Files the agent loads at start without an event are not in it.
  • It is absent when the events name none, and metadata sinks do not get it. See Memory and instruction files.

Unapproved actions.

FieldMeaning
stats.unapprovedActions (count, byKind, first)Destructive or paid commands the agent ran after a user turn that did not approve them (the unapproved-action flag; rules in concepts). Absent when there were none. --where unapproved>0 lists those sessions
stats.unapprovedActions.byKindrm-rf, git-reset-hard, git-clean, git-discard, git-push-force, db-drop, forced-subcommand, data-dir and paid
stats.unapprovedActions.firstkind, command (whitespace collapsed, at most 200 characters), at (ms), and afterError: true when a tool call had failed since the user's turn

Tool errors and fold errors.

FieldMeaning
stats.toolErrorsTool calls that really failed
stats.benignToolErrorsNon-zero exits that are not errors: a shell search that found nothing (grep, rg, git grep and the like exiting 1 with no output), a probe that answered no (test -f, which, command -v), and a diff, cmp or git diff --exit-code that found differences. Absent when zero
stats.interruptedToolCallsCalls the user or the agent stopped (exit 130, or an interrupted result). Absent when zero
stats.foldErrorSet when coding.sh could not fold the session's stored events into its stats, a coding.sh bug, not the agent's. The text is the error, at most 500 characters. Absent when the fold worked
  • A session with stats.foldError holds only what the events say directly: the agent, the first and last event times, the cwd and stats.events, plus the fold:error tag. coding.sh ingest folds it again, and coding.sh doctor counts these sessions.
  • The benign and interrupted rule also decides repeated-tool-failure, which only counts real failures, and each phase's toolErrors in the outline.

Three notification kinds carry the events behind these stats:

payload.kindFields
model.fallbackfrom, to, trigger, direction, text
forktoolUseId (the parent's Agent call) and inheritedTokens
hook.contexthookEvent, hookName, hookSource, command, chars, tokens and text (clipped). Metadata mode keeps only hookEvent, hookSource, chars and tokens

A missing session, or one outside a scoped key's project, gets 404:

shell
curl -sS "$CSH/api/sessions/team%2Fchat-1" -H "Authorization: Bearer $SUPPORT_READ_KEY"
json
{"error":"session not found","status":404}

GET /api/sessions/{id}/events

A session's events in order. Parameters: limit (1 to 20,000, default 5,000), offset, and kinds (comma-separated). Native-ingest ids come back as stored hashes.

shell
curl -sS "$CSH/api/sessions/team%2Fchat-1/events?kinds=tool.start,tool.end" -H "Authorization: Bearer $READ_KEY"
json
{
  "items": [
    {"id": "54b366d0…", "v": 0, "sessionId": "team/chat-1", "ts": 1793872802000, "agent": "support-bot",
     "kind": "tool.start", "payload": {"tool": "lookup_order", "toolUseId": "t1", "input": {"id": 1042}},
     "source": "sdk", "project": "team"},
    {"id": "22270b1c…", "v": 0, "sessionId": "team/chat-1", "ts": 1793872802400, "agent": "support-bot",
     "kind": "tool.end", "payload": {"tool": "lookup_order", "toolUseId": "t1", "ok": true, "output": "shipped"},
     "durationMs": 400, "source": "sdk", "project": "team"}
  ],
  "limit": 5000, "offset": 0, "hasMore": false
}

GET /api/sessions/{id}/outline

A long session at a glance: its phases (the start, each resume, compaction and long idle gap), the first event of each local day, and its derails. index, from and to count events in time order, the same order /events pages through.

shell
curl -sS "$CSH/api/sessions/team%2Fchat-1/outline" -H "Authorization: Bearer $READ_KEY"
json
{
  "phases": [
    {"n": 1, "kind": "start", "label": "start", "startTs": 1793872802000, "endTs": 1793876402000,
     "from": 0, "to": 1840, "eventId": "54b366d0…", "events": 1840, "prompts": 6, "toolCalls": 512,
     "toolErrors": 9, "filesChanged": 14, "tokens": 2210000, "derails": []},
    {"n": 2, "kind": "compaction", "trigger": "auto", "label": "compacted (auto)", "startTs": 1793876410000,
     "endTs": 1793880010000, "from": 1840, "to": 3600, "eventId": "9a1f03c2…", "events": 1760, "prompts": 2,
     "toolCalls": 498, "toolErrors": 31, "filesChanged": 3, "tokens": 1980000,
     "derails": [{"flag": "failure-streak", "ts": 1793877000000, "eventId": "c41e88a0…", "index": 2210,
                  "endTs": 1793877090000, "detail": "6 failed tool calls in a row (Bash)"}]}
  ],
  "days": [{"date": "2026-11-04", "ts": 1793872802000, "index": 0, "eventId": "54b366d0…"}],
  "derails": [{"flag": "failure-streak", "ts": 1793877000000, "eventId": "c41e88a0…", "index": 2210,
               "endTs": 1793877090000, "detail": "6 failed tool calls in a row (Bash)"}],
  "events": 3600,
  "turns": {"count": 8, "lengths": [412, 260, 1168, 300, 512, 220, 410, 318], "failed": [2, 4]},
  "failures": {"count": 40, "at": [655, 1902, 1904]},
  "compactions": [1840],
  "subagents": {"count": 1, "items": [{"id": "a33e97aa…", "start": 2950, "end": 3011, "type": "general-purpose", "childSessionId": "…"}]},
  "minimap": {"size": 30, "rows": [[30, 0, 12, 0], [30, 2, 14, 0]]}
}

The span fields come from the session's span model. A turn starts at each prompt of the main agent (or a turn id not seen before); a subagent's events stay in the turn that ran it.

FieldMeaning
turns.lengthsThe number of events in each turn, so a turn's first index is the sum of the lengths before it. At most 2,000 turns, with "truncated": true past that
failures.atThe first 100 failed events: failed tool calls, errors, and model calls, subagents or test runs that failed
minimap.rowsAt most 120 runs of size events, each [events, failures, tool calls, compactions]

A session over 20,000 events builds its model in the background the first time: that outline has "spans": "building" in place of the span fields. Sessions of 5,000 events or more keep their model in the index, so a restarted viewer does not build it again.

GET /api/sessions/{id}/rows

One page of a turn's rows, for a timeline that shows a long session without loading it.

ParameterDefaultMeaning
turn0The turn, from 0
afterThe last event index already shown; the page starts after it
limit2001 to 1,000 rows
fold0 shows every event

A run of 12 or more routine events (model calls, successful tool calls, spans, notifications) folds into one row, with its first and last three events left as rows. A fold never holds a failure, a prompt, a reply, a compaction, a subagent's start or end, a file change or a test result.

Open a fold by asking for its range with after=<from - 1>&fold=0. next, when present, is the after of the next page. A turn, after or limit out of range is a 400.

shell
curl -sS "$CSH/api/sessions/team%2Fchat-1/rows?turn=0&limit=3" -H "Authorization: Bearer $READ_KEY"
json
{
  "turn": {"n": 0, "start": 0, "end": 4, "ts": 1793872800000, "endTs": 1793872803000, "tools": 1, "calls": 1,
           "failures": 0, "subagents": 0, "compactions": 0, "prompt": "Where is order 1042?"},
  "rows": [
    {"i": 0, "id": "d4c45c89…", "kind": "user.message", "ts": 1793872800000, "label": "Where is order 1042?"},
    {"i": 1, "id": "0889ecc3…", "kind": "llm.call", "ts": 1793872801000, "label": "gpt-4o", "durationMs": 900},
    {"i": 2, "id": "54b366d0…", "kind": "tool.start", "ts": 1793872802000, "label": "lookup_order"}
  ],
  "next": 2
}

chat-1 is one turn of five events, so nothing in it folds; turn also has a turnId when the events carry one. A long session answers the same way, with a fold row for each folded run and next while the turn has more rows. A fold row, as a fragment that is not from chat-1:

json
{"fold": {"from": 674, "to": 701, "events": 28, "tools": 12, "calls": 4}}

GET /api/sessions/{id}/tree

The whole multi-agent run this session belongs to, as a tree: its top-level agent (found by following parent up) and every subagent under it.

  • Each node carries the session's own model, usage, tool calls, files changed and failure flags.
  • totals sums them over the whole tree, with the run's wall durationMs and every failure flag in it, worst first.
  • worst names the node to fix first (the worst-flagged, costliest session), or is null when nothing is flagged.
shell
curl -sS "$CSH/api/sessions/9f1c2e…/tree" -H "Authorization: Bearer $READ_KEY"
json
{
  "root": "9f1c2e…",
  "totals": {
    "sessions": 4, "usage": {"inputTokens": 2100000, "outputTokens": 41000, "costUsd": 3.74},
    "toolCalls": 612, "toolErrors": 40, "filesChanged": 17, "durationMs": 1800000,
    "flags": ["premature-completion"]
  },
  "worst": {"sessionId": "9f1c2e…:sub:a2", "role": "fixer", "flags": ["premature-completion"]},
  "tree": [
    {"id": "9f1c2e…", "agent": "claude-code", "role": "root", "model": "claude-sonnet-5",
     "depth": 0, "durationMs": 1800000, "usage": {"costUsd": 1.1}, "toolCalls": 300, "toolErrors": 2,
     "filesChanged": 3, "flags": [], "children": [
       {"id": "9f1c2e…:sub:a2", "role": "fixer", "depth": 1, "usage": {"costUsd": 2.0},
        "toolCalls": 180, "toolErrors": 31, "flags": ["premature-completion"], "children": []}
     ]}
  ]
}

GET /api/sessions/{id}/replay

The session as numbered steps, and the files the agent touched as of any step, rebuilt from its own tool calls. A step is a prompt, a reply, a tool call (with the file changes and test results it produced), a subagent start, a compaction, an error, or a file change no tool call claims.

Without parameters it returns the step list. steps[].eventId is the step's first event, and files names the paths each step wrote, deleted or fully read (relative to root when inside it). baseline is snapshot when coding.sh ui can read the session's start snapshot, else none.

shell
curl -sS "$CSH/api/sessions/cd0185b0-305a-4df2-92fa-6fc6caffdb55/replay" -H "Authorization: Bearer $READ_KEY"
json
{
  "sessionId": "cd0185b0-305a-4df2-92fa-6fc6caffdb55", "root": "/work/demo", "baseline": "none",
  "steps": [
    {"n": 1, "ts": 1794084489400, "kind": "prompt", "label": "Tidy notes.txt and greet.", "eventId": "0c1f…"},
    {"n": 2, "ts": 1794084489611, "kind": "read", "label": "Read notes.txt", "eventId": "5e2a…", "tool": "Read",
     "ok": true, "durationMs": 38, "files": [{"path": "notes.txt", "op": "read"}]},
    {"n": 4, "ts": 1794084489801, "kind": "shell", "label": "$ sed -i 's/alpha/beta/' notes.txt", "eventId": "9b7d…",
     "tool": "Bash", "ok": true, "durationMs": 171, "files": [{"path": "notes.txt", "op": "modify"}]}
  ],
  "files": ["gen.txt", "notes.txt", "out.txt", "sub/inner.txt"]
}
QueryReturns
step=NWhat step N did: detail (the prompt or reply text, the tool input, or for a shell call shell with command, exitCode when known, durationMs and output), changes (one entry per file with a unified diff), and tree (every file touched up to step N, with status added, modified, deleted or read)
step=N&file=<path>The same, plus that file's content as of step N
from=A&to=BWhat steps A through B changed together, one diff per file

A diff with exact: true compares the rebuilt file before and after, with real line numbers.

When the content on one side is unknown, exact is false, the diff is the change as the agent recorded it, and why says what is missing, for example Step 3 patched it before any step showed its content (no earlier read, full write or start snapshot).

A file's content is unknown until a step writes or fully reads it, or the start snapshot has it. An edit that does not match the rebuilt content makes it unknown again.

shell
curl -sS "$CSH/api/sessions/cd0185b0-305a-4df2-92fa-6fc6caffdb55/replay?step=4&file=notes.txt" -H "Authorization: Bearer $READ_KEY"
json
{
  "sessionId": "cd0185b0-305a-4df2-92fa-6fc6caffdb55", "steps": 15,
  "step": {"n": 4, "kind": "shell", "label": "$ sed -i 's/alpha/beta/' notes.txt", "tool": "Bash", "ok": true, "durationMs": 171},
  "detail": {"input": {"command": "sed -i 's/alpha/beta/' notes.txt"},
             "shell": {"command": "sed -i 's/alpha/beta/' notes.txt", "ok": true, "durationMs": 171, "output": "…"}},
  "changes": [{"path": "notes.txt", "op": "modify", "exact": true,
               "diff": "--- a/notes.txt\n+++ b/notes.txt\n@@ -1,2 +1,2 @@\n-alpha\n+beta\n bravo\n"}],
  "tree": [{"path": "notes.txt", "status": "modified", "edits": 1, "lastStep": 4, "here": true}],
  "file": {"path": "notes.txt", "exists": true, "content": "beta\nbravo\n", "changedAt": 4}
}

A step past the last one gets 400. File content is cut at 1 MiB (contentTruncated) and a diff at 512 KB (diffTruncated).

GET /api/sessions/{id}/fork, POST /api/sessions/{id}/fork

Fork the session at a replay step: send the model call behind that step again with changed context files (AGENTS.md and the like) and compare the forked next action with the recorded one. Only coding.sh ui answers these routes; coding.sh serve answers 404. The session needs model calls captured by coding.sh proxy. When forking is on, the replay step list adds "canFork": true.

GET ?step=N returns the call a fork at step N starts from and the context files it carried. It does not call the model.

  • by is tool-use when the call's reply holds the step's tool call, else time (the agent's last call before the step, or for a prompt the first call after it).
  • canCall is false when neither keyEnv nor CLAUDE_CODE_OAUTH_TOKEN (for Anthropic) is set in the environment coding.sh ui runs in.
shell
curl -sS "http://127.0.0.1:4319/api/sessions/cd0185b0-305a-4df2-92fa-6fc6caffdb55/fork?step=4" -H "Authorization: Bearer $UI_TOKEN"
json
{
  "step": {"n": 4, "kind": "shell", "label": "$ npm test"},
  "call": {"id": "7a1c…", "model": "claude-sonnet-4-5", "by": "tool-use", "messages": 5},
  "files": [{"path": "AGENTS.md", "text": "Run the tests with npm test.\n"}],
  "recorded": {"action": {"type": "tool", "name": "Bash", "input": {"command": "npm test"}}, "label": "Bash: npm test"},
  "canCall": true, "keyEnv": "ANTHROPIC_API_KEY"
}

POST with {"step": N, "files": {"AGENTS.md": "…"}, "trials": 1} sends the call with each file replaced, added, or removed when its text is empty. trials is 1 to 5. The request must come from the viewer's own origin. It calls the model and costs money.

In the answer, edits says what changed in the request, each trial has the forked first action and differs, and differ counts the trials whose action is not the recorded one.

json
{
  "sessionId": "cd0185b0-305a-4df2-92fa-6fc6caffdb55",
  "step": {"n": 4, "kind": "shell", "label": "$ npm test"},
  "call": {"id": "7a1c…", "model": "claude-sonnet-4-5", "by": "tool-use", "messages": 5},
  "change": "AGENTS.md changed",
  "edits": [{"path": "AGENTS.md", "how": "replaced", "count": 1}],
  "recorded": {"action": {"type": "tool", "name": "Bash", "input": {"command": "npm test"}}, "label": "Bash: npm test"},
  "trials": [{"action": {"type": "tool", "name": "Bash", "input": {"command": "node --test"}}, "label": "Bash: node --test",
              "differs": true, "model": "claude-sonnet-4-5", "costUsd": 0.0123}],
  "differ": 1
}

A step out of range, a session without stored requests, or a missing key gets 400 with the reason.

GET /api/playground, GET /api/playground/calls/{id}, POST /api/playground/run, POST /api/playground/save

Rebuild one recorded model call as an editable request, send it again, and save it as a dataset case. Only coding.sh ui answers these routes; coding.sh serve answers 404.

GET /api/playground lists the providers and the datasets a case can go to. A provider is ready when its key is set where coding.sh ui runs, or when its base URL is local: anthropic (Messages API; ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL), openai (Responses API) and openai-compatible (chat completions), both from OPENAI_API_KEY and OPENAI_BASE_URL.

GET /api/playground/calls/{id}?session= rebuilds an llm.call event. With a request the proxy stored, source is request and response is the recorded answer. Without one, source is transcript and the request is the conversation before the call.

In a subagent's own session that is the subagent's conversation: its prompt and its own tool calls, without those of a subagent it started. Both are redacted. A call with neither gets 404 with reason no_request.

json
{
  "call": {"id": "7a1c…", "sessionId": "proxy-…", "ts": 1767000000000, "model": "gpt-4.1-mini", "api": "openai.chat"},
  "source": "request",
  "provider": "openai-compatible",
  "request": {"model": "gpt-4.1-mini", "system": "Be terse.", "messages": [{"role": "user", "text": "2+2?"}],
              "tools": [], "maxTokens": 1024},
  "response": {"text": "4", "toolCalls": [], "stopReason": "stop", "usage": {"inputTokens": 40, "outputTokens": 7}}
}

Messages are {"role": "user", "text"}, {"role": "assistant", "text"?, "toolCalls"?: [{"id", "name", "input"}]} or {"role": "tool", "toolUseId", "name", "content", "isError"?}.

POST /api/playground/run with {"provider": "openai-compatible", "request": {…}} sends the request and returns the answer with latencyMs and, when the model has a price, usage.costUsd. It calls the model and costs money.

POST /api/playground/save with {"dataset": "arith", "request": {…}, "expected"?: {…}, "sessionId"?, "callId"?} adds the request as a case to a new version of the dataset, creating it when needed. The case is a frozen prefix, tagged playground and redacted. The answer is {"datasetId", "name", "version", "caseId", "cases", "created"}.

Both POSTs must come from the viewer's own origin and take bodies up to 8 MB.

StatusreasonWhen
400invalid_requesta bad request
400missing_api_keythe provider's key is not set
502provider_errorthe provider failed

GET /api/blobs/{sha256}

The raw content behind a ref in an event's blobs array (full tool output, diffs, the source span). Always text/plain with X-Blob-Size (full size) and X-Truncated (1 when cut at 2 MiB). Add ?full=1 for the whole blob. Blobs are content-addressed and shared across projects: a scoped key that knows another project's hash can fetch it.

Here $BLOB is the source span of the OTLP example above:

shell
BLOB=$(curl -sS "$CSH/api/sessions/team%2Ftriage-12/events?limit=1" -H "Authorization: Bearer $READ_KEY" \
  | node -pe 'JSON.parse(require("fs").readFileSync(0)).items[0].blobs[0]')
curl -sS -i "$CSH/api/blobs/$BLOB" -H "Authorization: Bearer $READ_KEY"
output
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
X-Blob-Size: 513
X-Truncated: 0

{"format":"otlp.span","v":0,"resource":{"service.name":"triage-agent"},"scope":{"name":"manual"},"span":{"name":"chat gpt-4o",…}}

A ref that is not 64 lowercase hex characters gets 400 {"error":"blob ref must be 64 lowercase hex chars","status":400}; an unknown one gets 404 {"error":"blob not found","status":404}.

?range=a-b (or a Range: bytes=a-b header) reads bytes a to b, inclusive: a- reads to the end and -n the last n bytes. The answer is 206 with Content-Range: bytes a-b/size, as application/octet-stream, at most 4 MiB per read. A range that starts past the end gets 416 with Content-Range: bytes */size; a malformed one gets 400.

shell
curl -sS "$CSH/api/blobs/$BLOB?range=0-99" -H "Authorization: Bearer $READ_KEY"

GET /api/calls/{id}

One model call logged by coding.sh proxy: the llm.call event, the request as the client sent it (system prompt and tool definitions put back from their blobs; auth headers are never stored) and the response. {id} is the event id or a unique prefix of at least 6 characters. request and response are null in metadata capture mode.

shell
curl -sS "$CSH/api/calls/319a57022de6" -H "Authorization: Bearer $READ_KEY"
json
{
  "call": {"id": "319a57022de66bd9…", "kind": "llm.call", "source": "proxy", "model": "claude-sonnet-4-5-20250929",
           "usage": {"inputTokens": 12, "outputTokens": 42, "cacheReadTokens": 4600, "cacheWriteTokens": 100},
           "payload": {"api": "messages", "statusCode": 200, "systemHash": "907e1d58151bd256", "toolCount": 3, "…": "…"}},
  "request": {"provider": "anthropic", "api": "messages", "method": "POST", "path": "/v1/messages",
              "headers": {"anthropic-version": "2023-06-01"},
              "body": {"model": "claude-sonnet-4-5", "system": [{"type": "text", "text": "You are a careful coding agent.…"}],
                       "tools": [{"name": "Bash", "…": "…"}], "messages": ["…"]}},
  "response": {"status": 200, "headers": {"request-id": "req_…"}, "body": {"content": ["…"], "stop_reason": "end_turn"}}
}

An unknown or ambiguous id gets 404.

GET /api/calls/{id}/diff

What changed between this call and the model call before it in its session, or ?with=<id>: model, settings, tool definitions, the system prompt as a line diff, and the message history. The same object as coding.sh diff-calls <a> <b> --json.

When this call read less of the prompt cache than the other one cached, cache.cause names the first change in the order the cache reads a request (tools, then system, then messages), or the 5-minute cache lifetime.

json
{
  "a": {"id": "dc1d0bae0de0…", "model": "claude-sonnet-4-5-20250929", "usage": {"cacheReadTokens": 4000, "cacheWriteTokens": 200}},
  "b": {"id": "7c8944291203…", "model": "claude-sonnet-4-5-20250929", "usage": {"cacheReadTokens": 0, "cacheWriteTokens": 4400}},
  "content": "full",
  "params": [],
  "tools": {"same": true, "added": [], "removed": [], "changed": [], "reordered": false},
  "system": {"same": false, "lines": [{"op": " ", "text": "Run the tests before you say done."},
                                      {"op": "-", "text": "Today is 2026-11-06."}, {"op": "+", "text": "Today is 2026-11-07."}],
             "firstChangeLine": 3, "wastedPrefixFrac": 0.023},
  "messages": {"a": 3, "b": 5, "shared": 3, "rewritten": false},
  "cache": {"reused": false, "expected": 4200, "read": 0, "write": 4400, "gapMs": 812,
            "cause": "the system prompt changed (first change: - Today is 2026-11-06.)"}
}

When the system prompt changed and both requests were stored:

  • system.firstChangeLine is the 1-based line of the earlier prompt where the two first differ.
  • system.wastedPrefixFrac is the share (0 to 1, by characters) of the earlier prompt from that point to the end: the part the cache could no longer read.
  • Above 0.2, a system-prompt cache.cause adds a hint to move dynamic content to the end of the system prompt.

Both fields are absent when the system prompt is the same, and in metadata mode.

content is metadata when either request was not stored: tools and system are then compared by their short hashes. The first call of a session, without ?with=, gets 404.