> ## Documentation Index
> Fetch the complete documentation index at: https://docs.checkfu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Events

> Drive sessions, consume the ordered event log, resume live streams, and answer a parked run.

The Session event log is the durable **Session execution record and ordering authority**. Every persisted event has a globally unique ID and a monotonically increasing per-Session sequence number. Accepted input, lifecycle, Run lineage, waits, observations, and settlement are interpreted at those ordered positions.

That authority is concern-specific:

* provider-observed model-plane receipts remain authoritative for model Usage;
* the trusted capability and effect boundary remains authoritative for provider effects;
* the ActionApproval resource and responder decision remain the human-decision authority;
* Connection, Memory, Usage, Audit, export, retention, and other specialized aggregates retain their own lifecycle and authority.

Session-visible evidence from those authorities may be appended or projected at a Session position without converting the originating resource into a mere log projection. Harness narration, traces, queues, and UI state cannot substitute for an authoritative Usage receipt, provider-effect receipt, ActionApproval, Connection state, Memory revision, Audit record, or export and erasure boundary.

<Note>
  Plain HTTP against the Checkfu API needs no private package.
  The documented path is usable as written for an admitted Workspace.
  Confirm [capability status](/getting-started/status) before making availability part of your application's contract.
</Note>

## Event envelope

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "id": "evt_0123456789abcdef0123456789abcdef",
  "seq": 12,
  "schema_version": 52,
  "created_at": "2026-08-30T18:00:03.000Z",
  "type": "session.title_updated",
  "provenance": "platform",
  "presentation": {
    "canonical_type": "session.title_updated",
    "provenance": "platform",
    "known": true,
    "classification": "lifecycle",
    "visibility": "detail",
    "coalescible": false,
    "notification": "edit",
    "milestone": false,
    "terminal": false,
    "summary": "Session title updated"
  },
  "payload": {
    "present": true
  }
}
```

Use `seq` for ordering within one Session. Use `id` to deduplicate at-least-once delivery across consumers. The general write `schema_version` (currently `52`) versions the envelope shape itself, independent of the dated wire version. Native event shapes retain their own versions:

* `session.admitted`: `54`.
* Native `run.created` without `collaboration_space_id`: `46`.
* Native `run.created` with `collaboration_space_id`: `53`.

The `collaboration_space_id` variants are read support for Runs admitted through an AgentPlacement, including Runs recorded by the managed chat-channel feature that Checkfu has retired. Existing logs that contain them stay readable. New Sessions created through `POST /v1/sessions` do not carry a space.

The writer chooses the version from the actual event shape; writers do not all advance in lockstep. A version number alone does not establish that an event shape is accepted. Stored event reads accept only the current event shapes and their shape-specific versions. Retired Checkfu event formats are rejected.

### Provenance

`provenance` names **who witnessed** the fact. Every envelope returned by the list endpoint and the SSE stream carries exactly one of five values:

| `provenance` | The fact was witnessed by |
| - | - |
| `wire` | the mandatory model gateway: model input and output observed at the plane boundary |
| `exec` | the platform itself, watching a sandbox or tool operation |
| `narration` | the Harness, accounting for its own activity (including connected-runtime observations) |
| `human` | a person steering the Session: messages, interrupts, and decisions |
| `platform` | the control plane, asserting a lifecycle or settlement fact |

The `exec` lane is realized by one event type today, `exec.tool_invocation`, in the [event catalog](#event-catalog) below.

Provenance is a read-side projection derived from the event type. It is never persisted, and it can never change sequence ordering, idempotency, settlement, retention, or replay. The stored log remains the truth. An event type the current classifier does not recognize arrives as `narration`, so new additive types stay readable by consumers that predate them. `narration` also remains the default display projection: a Harness's account of a turn is what you render, not the raw `wire` evidence beneath it.

Connected-runtime narration may additionally carry `attested: true`. That literal means the Session verified the batch's detached Ed25519 signature against the runtime's registered current key before appending it. It strengthens the harness's self-report; it does not change its `narration` provenance into platform-witnessed `wire` or `exec` evidence. Unsigned connected batches remain valid and omit the field.

### Presentation

`presentation` is a bounded product view derived from the canonical event and its provenance. It is never stored and never replaces `type`, `payload`, or `provenance`:

| Field | Meaning |
| - | - |
| `classification` | `input`, `progress`, `question`, `decision`, `result`, `failure`, `lifecycle`, or `diagnostic` |
| `visibility` | `default`, drill-down `detail`, or normally `hidden` |
| `coalescible` | a consumer may replace an earlier progress rendering instead of adding a row |
| `notification` | create `new`, `edit` an existing projection, or send `none` |
| `milestone` / `terminal` | product significance and unconditional Session-terminal behavior |
| `summary` | a bounded, content-free fallback label; read the canonical payload for authorized content |
| `known` | whether this reader explicitly classified the exact type or open `span.*` family |

Every closed current event has an explicit decision. Unknown future types still arrive with their canonical envelope and a conservative diagnostic presentation, so a product feed never drops evidence it does not yet understand.

The SDK exposes the canonical iterator as `sessions.events.stream(...)`. When you want the ergonomic view, `streamPresented(...)` yields both values together:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
for await (const { event, presentation } of checkfu.sessions.events.streamPresented(sessionId)) {
  renderProductEvent(presentation)
  retainCanonicalEvent(event)
}
```

## Client drive events

Post one of these bodies to `/v1/sessions/{id}/events`:

* `user.message`
* `system.message`
* `user.interrupt`
* `user.custom_tool_result`
* `user.tool_confirmation`
* `user.question_answer`
* `user.define_outcome`

Authenticated server context supplies `authored_by`; client event bytes cannot select or override it. A direct client drive supplies `caused_by: { "kind": "api" }`, may add a 1–512 character `caused_by.request_id` for durable correlation, and may include a `collaboration_space_id`. That field is retained on the event but never grants access, Memory membership, a budget, or a scope. The accepted event retains the server-stamped author for audit and projection. A caller-selected `authored_by` is rejected as `400 validation.malformed`.

<Note>
  An accepted drive event acknowledges input, not completed work. Follow the
  recorded Run outcome. Matching question answers, approval decisions, and tool
  results continue the original Run; waits and internal recovery do not create
  a new public Run.
</Note>

A wait response must name the exact current tool use or Question and pass
current authorization checks. A response to canceled or stale work is refused;
it cannot authorize an old effect. An exact settled-command/key replay can
return its retained outcome, so cancellation does not make every historical
retry a conflict. Authorization denial, unavailable authority, and a missing
Session are separate failures.

### User message

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "user.message",
  "payload": {
    "content": "Summarize the repository.",
    "attachments": [],
    "caused_by": { "kind": "api" }
  }
}
```

If Checkfu finds deceptive Unicode, the first request returns `400 validation.prompt_integrity_review_required` without appending an event. Its typed `error.review` contains the pinned analyzer version, normalized text, and exact findings, so API and SDK callers never need to reimplement Unicode analysis. Review that challenge, then resubmit the **original** content with one explicit choice:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "user.message",
  "payload": {
    "content": "Review pаypal.com before continuing.",
    "prompt_integrity_disposition": "accepted_normalization",
    "caused_by": { "kind": "api" }
  }
}
```

Use `accepted_normalization` to let the server replace mixed-script confusables and remove bidirectional controls, or `preserved_original` to send the reviewed source exactly. Do not send the already-corrected string with a disposition: the server rejects a choice when its own analysis finds nothing, preventing consent from being replayed against changed text. The returned `user.message` contains the effective content and a server-authored `prompt_integrity` receipt. Requests with no findings omit both fields.

An independent input or follow-up owns its own Run intent. While a Run is
active, supported steering contributes to that existing Run. Set
`run_delivery: "next_run"` to request separate work instead of steering; it may
remain queued after acceptance. Without that field, native mid-Run delivery
depends on the Run's recorded [capabilities](/concepts/harness-extensions).
Read the delivery and Run relationships in the log; `run.input_delivered`
acknowledges delivery, not completion.

`connected_runtime` belongs to connected Run admission; this route rejects a
`user.message` carrying it with `400 validation.malformed`.

### Interrupt the current turn

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "user.interrupt",
  "payload": {
    "caused_by": { "kind": "api" }
  }
}
```

`target_event` optionally names an API-authored `user.message`. Before
consumption, cancellation withdraws only that input. After the target has
contributed to shared work, cancellation also interrupts the shared group;
consumed peers remain recorded as interrupted and are not automatically
replayed. Unrelated queued input remains available. An ID naming another kind
of event is `400 validation.malformed`.

`mode: "cooperative"` asks the running work to drain and is honored only when
its recorded capability set realizes `interrupt: cooperative`. Observe the
accepted cancellation outcome. A cancellation request does not roll back
completed effects or prove every process has stopped.

### Return a custom tool result

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "user.custom_tool_result",
  "payload": {
    "tool_use_id": "toolu_123",
    "result": { "approved": true },
    "caused_by": { "kind": "api" }
  }
}
```

The result must match the original pending `tool_use_id`. It continues that
Run with your application's result. An exact keyed replay returns the same
event; a conflicting result or stale wait is refused. Do not repeat an
external action merely because its result submission lost a response.

### Confirm a tool action

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "user.tool_confirmation",
  "payload": {
    "tool_use_id": "toolu_123",
    "result": "allow",
    "caused_by": { "kind": "api" }
  }
}
```

`result` is `allow` or `deny`; only `deny` may include a non-empty
`deny_message`. `tool_use_id` identifies the exact current wait. The public
request does not accept `session_thread_id` or a caller-selected author.
ActionApproval policy and responder authority still apply. Approval grants
neither missing permission nor authority for changed arguments. Allowing a
governed Custom tool emits `run.action_authorized`; the original Run still
waits for the matching `user.custom_tool_result`.

### Answer an agent question

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "user.question_answer",
  "payload": {
    "question": "qst_0123456789abcdef0123456789abcdef",
    "answers": [{ "id": "target_branch", "values": ["main"] }],
    "caused_by": { "kind": "api" }
  }
}
```

A Question asks for ordinary input and grants no authority. `question` names
the active Question; `answers` must cover every item from its `agent.question`
exactly once, honoring each item's `allow_multiple`. Partial, duplicate, and
unknown item IDs are rejected. A matching answer continues the original Run.
See [ActionApprovals](/concepts/action-approvals) for review decisions.

Textual answers cross the same deceptive-Unicode boundary as `user.message`: a flagged value returns `400 validation.prompt_integrity_review_required`, and the resubmission carries a payload-level `prompt_integrity_disposition` of `accepted_normalization` or `preserved_original`.

### Define an outcome

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "user.define_outcome",
  "payload": {
    "description": "Write a release note for the completed change.",
    "rubric": "- explains user impact\n- cites verification evidence",
    "max_iterations": 3,
    "deliverables": ["/workspace/release-note.md"],
    "caused_by": { "kind": "api" }
  }
}
```

`user.define_outcome` starts an outcome-driven Run from a `pending` or `idle` Session. Only one outcome may remain nonterminal at a time. `max_iterations` is optional and must be between 1 and 20. `deliverables` is optional and accepts 1–32 absolute paths; a zero-data-retention Workspace rejects outcome definitions that request deliverables.

## Answering a parked run

A Run does not fail when it needs something from you. It parks. This is the loop most integrations have to implement, and it is driven entirely by events.

When a Run needs an answer it emits `run.requires_action` and the Session moves to `session.status_waiting`. The payload carries `run`, `attempt`, `cost`, and an `action` that discriminates on `kind`:

| `action.kind` | Additional fields | How you answer |
| - | - | - |
| `action_approval` | `approval`, `tool_use_id` | A reviewer responds through the ActionApprovals API. |
| `custom_tool` | `tool_use_id` | Post `user.custom_tool_result` with the matching `tool_use_id`. |
| `question` | `question`, `items[]` (no `tool_use_id`) | Post `user.question_answer` naming that Question and answering every item. |
| `outcome_evaluation` | `outcome`, `iteration` | Nothing: the platform grades and resumes. |

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "run.requires_action",
  "payload": {
    "run": "run_0123456789abcdef0123456789abcdef",
    "attempt": 1,
    "action": {
      "kind": "action_approval",
      "approval": "approval_0123456789abcdef0123456789abcdef",
      "tool_use_id": "toolu_123"
    },
    "cost": { "tokens": 4120 }
  }
}
```

A wait suspends progress without completing the public Run. The matching
answer resumes the original work with its recorded inputs and resolved context.
It does not create another Run or silently resolve newer mounts. Current
resource and action authority is checked again. Missing original state or
revoked access causes a refusal, not a substitute input or blind replay.
Canceling the wait does not authorize its pending action.

For an ActionApproval wait, `action_approval.pending` follows with the frozen decision context. A ToolInvocation ActionApproval carries the Connection, Tool identity, argument hash, and acting Principal:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "action_approval.pending",
  "payload": {
    "approval": "approval_0123456789abcdef0123456789abcdef",
    "frozen_context": {
      "connection": "conn_0123456789abcdef0123456789abcdef",
      "tool": { "tool_source_id": "ts_0123456789abcdef0123456789abcdef", "name": "create_issue" },
      "arg_hash": "sha256:9f2c1a4b7d3e5f6081920a3b4c5d6e7f8091a2b3c4d5e6f70819a2b3c4d5e6f7",
      "principal": "prin_0123456789abcdef0123456789abcdef"
    }
  }
}
```

<Note>
  The ActionApproval **resource** exposes these same frozen facts under different names: `context_summary` and `arguments_hash` (see [ActionApprovals](/concepts/action-approvals)). Event consumers match on `frozen_context`/`arg_hash`; REST readers match on the resource names.
</Note>

A Custom-tool ActionApproval instead carries `kind: "custom_tool"` plus the exact Session, Run, tool-use ID, admitted Agent, logical harness tool, argument hash, and acting Principal. Its exact argument values remain in the preceding `agent.tool_use` event:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "action_approval.pending",
  "payload": {
    "approval": "approval_0123456789abcdef0123456789abcdef",
    "frozen_context": {
      "kind": "custom_tool",
      "session": "sess_0123456789abcdef0123456789abcdef",
      "run": "run_0123456789abcdef0123456789abcdef",
      "tool_use_id": "toolu_123",
      "agent_id": "agent_0123456789abcdef0123456789abcdef",
      "tool": "harness:create_issue",
      "arg_hash": "sha256:9f2c1a4b7d3e5f6081920a3b4c5d6e7f8091a2b3c4d5e6f70819a2b3c4d5e6f7",
      "principal": "prin_0123456789abcdef0123456789abcdef"
    }
  }
}
```

Once a reviewer decides, `action_approval.resolved` records the decision (`approve` or `deny`), the `tool_use_id` it answers, who resolved it, and any reviewer instructions. Custom-tool denials emit `run.resumed` and continue the original Run with the denial. A Custom-tool approval instead emits `run.action_authorized`; the same Run remains parked until the application submits its matching `user.custom_tool_result`.

For a connected Tool invoked through platform MCP, resolving ActionApproval records the decision without dispatching the operation or automatically resuming or creating a Run. Retry the identical original `tools/call`, preserving its original ToolInvocationId and the returned `requestState` where applicable. Checkfu rechecks current Connection, grant, policy, approval, and other required authority before execution. A completed identical retry is refused as `already_executed` rather than dispatched again.

Accepting the Custom-tool result continues the same Run. The accepted
`user.custom_tool_result` records the application result for the original
`tool_use_id`; an exact keyed retry returns the same event. Follow the Run's
recorded outcome. An accepted response, an Agent message, or an idle event with
`stop_reason: "requires_action"` does not establish completion. A provider
effect receipt remains separate from both input acceptance and Run settlement.

<Tip>
  Key the wait loop off `run.requires_action`, not off `session.status_waiting`. The status event tells you the Session is parked; only the action payload tells you **what** it is waiting for and which approval, `tool_use_id`, or Question to answer.
</Tip>

See [ActionApprovals](/concepts/action-approvals) for the review side of this flow.

## Event catalog

Every event type the current contract can append. Consumers must tolerate unknown types: the catalog grows with the platform, and a new event type is not a breaking change.

### `user.*`

* `user.message`: a human input; starts a turn (or steers a running one)
* `user.interrupt`: ends work without making the Session terminal
* `user.custom_tool_result`: your app returning the result of a custom tool the Run parked on
* `user.tool_confirmation`: allows or denies the exact current tool wait
* `user.question_answer`: answers one active `agent.question` and continues the original Run
* `user.define_outcome`: attaches a rubric-graded definition of done to the next turn

### `system.*`

* `system.message`: mid-conversation system-privileged context appended *beside* the Agent's frozen system prompt, which it never replaces. Unlike a title or the metadata bag, the content is the point — an agent that cannot read it is not steered by it — so the text lives in the event and is retained content like a user message (up to 8,192 characters). No instruction changes and no release hash moves. It is admitted only from a control-plane role, never a runtime credential, and only where the admitted model route can carry a mid-conversation system turn

### `agent.*`

* `agent.message`: the agent's reply text (`payload.content` is what you show users)
* `agent.thinking`: reasoning content, when the harness surfaces it
* `agent.tool_use`: the agent invoked a tool
* `agent.tool_result`: what the tool returned
* `agent.question`: a structured request for ordinary human input: one `question` ID and one to four uniquely identified `items`, each with bounded `options` and single- or multi-value semantics. Prose that reads like a question stays an ordinary `agent.message`
* `agent.subagent_started` / `agent.subagent_completed`: a harness-internal subagent, observed but not governed

### `message.*`

* `message.screened`: a delivery screen passed or withheld an outbound agent message; screens never rewrite content

For a withheld message, current verdicts persist `withheld_reason`:
`screen_block` means the screen blocked delivery; `screen_failed` means a
screen failure withheld delivery under `fail_closed`. Passing verdicts omit
this field, and screened-message readback returns `null` for delivered messages.
The separate `reason` is bounded explanatory text, not a closed error-code enum;
do not classify a block or failure from that text or from the presence of a model route.

Historical verdicts may omit `withheld_reason`. Readback preserves its legacy
route-based interpretation for those records, but the original distinction
between a route-less block and a screen failure was not retained. That inferred
value is not evidence of the original outcome; inspect the source verdict before
using it for evaluation or automation.

### `flow_review.*`

* `flow_review.requested`: one passive post-settlement review was frozen for an Automation-origin Turn
* `flow_review.started`: the bounded evidence and one-attempt review lease were acquired
* `flow_review.completed`: a cited typed report settled; it never changes the Outcome, delivery, or Session state
* `flow_review.unavailable`: retained evidence or reviewer authority could not produce a report after the bounded policy

### `session.*`

* `session.status_pending` … `session.status_canceled`: the ten status transitions, including compute-closed `session.status_reconciling`; see the [state machine](/reference/sessions#session-status)
* `session.admitted`: admission settled, carrying the frozen release, requester, and (if installed) placement
* `session.forked`: appended after the inherited source prefix as the forked Session's first new event. **Private alpha** for committed-head, exact-`at_event`, and live-Checkpoint forks. Its payload names `source_session`, `through_event`, `through_sequence`, the nullable target `checkpoint`, and `mode: "event_log_rebuild" | "checkpoint_clone"`. A Checkpoint fork preserves the inherited log prefix and independently clones the provider snapshot; it never copies a live Run, lease, wait, or ActionApproval.
* `session.updated`: mount declarations changed at a turn boundary
* `session.title_updated`: content-free `{ present }` marker for a material title projection change
* `session.metadata_updated`: the correlation bag settles like a title — a content-free `{ pairs }` count of how many pairs the Session now carries. Keys and values are your text and never enter the log
* `session.resources_resolved`: the exact revisions and trees this Run received
* `session.compacted`: context was compacted; the log itself is never rewritten
* `session.context_condensed`: a context-window boundary was recorded; later conversation rebuilds serve an elision marker plus the tail after it, and each boundary also projects into the optional `condensations` array of `GET /v1/sessions/{id}/ledger`
* `session.capture_acknowledged`: the platform accepted one bounded working-state capture, naming the capture sequence, the manifest digest, and the file count
* `session.budget_reached`: one platform-witnessed fact in this Session for each reached applicable Budget cap generation, including an Agent-scoped Budget — the Budget id, the configuration version and limit that crossed, the priced metric, and the decided breach action. The durable generation is (budget id, limit, window start): raising the limit or a rolling-window reset starts a new generation, while pause/unpause and delivery retries never repeat the fact, so consumers dedupe on the globally unique event id
* `session.delivery_ambiguous`: reserved by the built next-version Session delivery protocol for an outbound WebhookEndpoint POST whose acknowledgement may have been lost. The currently selected Session architecture does not emit it; the event names only structural delivery, endpoint, and event coordinates and is excluded from the webhook subscription catalog to prevent recursive delivery
* `session.archived`: content-free marker that the Session left the working directory; its timestamp is the resource's `archived_at` (see [Archive a session](/reference/sessions#archive-a-session))

For model-usage Budget settlement, `session.budget_reached` announces the reached cap; it does not by itself identify the turn's terminal state. With `breach_action: "interrupt_turn"`, the active Run emits `run.failed` with `error.type: "budget.exceeded"`, followed by `session.status_failed` with `status: "failed"`, `stop_reason: "error"`, and `reason: "budget.exceeded"`. With `breach_action: "pause_session"`, it emits `run.canceled` with `reason: "budget.reached"`, followed by `session.status_idle` with `status: "idle"` and `stop_reason: "budget_reached"`. A `429 budget.exceeded` response alone proves neither transition; use the accepted Session events to observe the selected policy outcome.

A reached-cap event or terminal Run event does not acknowledge physical cleanup or final Usage, charge, or billing-delivery settlement. Those obligations can remain pending after execution stops; retries must reconcile the original obligation rather than dispatch replacement work.

### `watch.*`

A [Watch](/concepts/sessions-and-runs#watches-and-wakeups) is a Session-owned subscription to external provider state.

* `watch.created` / `watch.updated` / `watch.removed`: the Watch's lifecycle, each naming the `watch`
* `watch.woken`: a verified change in the watched state admitted an ordinary next Run; carries the `source_state_hash` that settled the wake
* `watch.suppressed`: a hint that woke nothing, with `reason: duplicate | self_authored | stale_hint | unsafe_content`

### `memory.*`

* `memory.mount_resolved`: the Memory mount admitted when the Session is initialized, naming the store, path, `requested` access and clamped `effective` access. Per-Run resolved resources are recorded separately in `session.resources_resolved`.
* `memory.accessed`: records a Memory read. Under standard retention it names the store, real MemoryVersion ID, method, and document paths and hashes. Under zero-data-retention it records mounted-store and document counts without store, version, path, or digest fields.
* `memory.change_checkpointed`: a settlement seal naming `store`, `working_ref`, `base_version`, `new_version`, `tree_hash`, `diff_hash`, and `changed_paths`. The version fields are real `memver_` MemoryVersion IDs; the hash fields are content digests. Principal-owned working revisions remain isolated from canonical. The event carries metadata, never document bodies, and does not extend their retention.
* `memory.proposal_created` / `memory.proposal_resolved`: the shared-store review flow
* `memory.curated`: a curation run landed curated content
* `memory.redacted`: an erasure, recorded as history rather than hidden

### `file_tree.*`

* `file_tree.writeback_captured`: the Session accepted one exact sealed changed manifest and entered compute-closed reconciliation
* `file_tree.writeback_started`: the compute-free reconciler acquired the durable publication generation
* `file_tree.writeback_no_changes`: a complete tree enumeration proved the mounted FileTree was unchanged
* `file_tree.writeback_succeeded`: the owned branch, pull request, exact frozen `base_revision`, published immutable `revision`, capture retirement, and, when eligible, a reused Watch settled
* `file_tree.writeback_conflicted`: the target, owned branch, or pull-request identity diverged; Checkfu did not force-push or retry over human work
* `file_tree.writeback_failed`: strict capture, authority, provider, or publication failed rather than silently dropping edits

### `model.*`

* `model.routed`: which provider/model candidate answered this call
* `model.usage_recorded`: the authoritative token receipt for one model call

### `wire.*`

* `wire.call`: one gateway-witnessed model request and response, retained as evidence

The event row carries no model bytes. It names the Run, attempt, request ID, binding, provider, model, and usage, plus typed references to the exact material: the admitted request body, each ordered request message, the response body, and the provider frames. Every reference is `{ source, content_digest, bytes }`, where `content_digest` is a `sha256:<hex>` digest, so repeated history resolves to objects that already exist and only an operator can [resolve one back to bytes](#resolve-retained-wire-content). `wire.call` is evidence, not display: it sits in the ordinary Session sequence and appears in exports at its original position, but the Harness's narration remains the conversational projection you render.

A zero-data-retention Workspace constructs no wire evidence at all. The missing `wire.call` events *are* the visible capability difference. Retained wire bytes also carry a mandatory 30-day absolute retention ceiling, even where the Workspace's standard policy has no TTL at all; a shorter Workspace or `wire_frame` TTL still wins. After expiry the references remain structurally in the event, and resolving one returns `404`.

### `exec.*`

* `exec.tool_invocation`: one governed ToolInvocation, settled and witnessed by the platform itself

The payload is structural only: Run and attempt identities, `tool_invocation_id`, the associated resource identities and logical tool name, `execution_backend` (`builtin`, `executor`, or `connector`), `disposition`, an `argument_fingerprint` that hashes the canonical logical arguments rather than carrying them, and claim-to-settlement duration. `connector` means the pinned tool ran through the provider-neutral Integration Gateway; it does not identify the selected runtime adapter. No argument or result body ever appears, so unlike `wire.call` this event persists unchanged under zero data retention.

`disposition` is `completed`, `denied`, or `indeterminate`. At most two `exec.tool_invocation` events exist for one `tool_invocation_id`: the settlement, plus at most one later corrective event carrying `upgrade: true`, which marks an already-settled outcome `indeterminate`.

### `run.*`

* `run.created`: admission accepted work; carries the trigger
* `run.started`: an executor actually began
* `run.claim_denied`: execution admission refused one Runner claim. The payload names the typed `reason` (placement conformance missing or not advertised by that Runner, allowlist security boundary unavailable, sandbox policy not admitted, workload review not current, CollaborationSpace not authorized, frozen admission no longer matching), the Environment's `network_mode`, and whether the refusal `settled` the Run. A settled denial is followed by `run.failed` with `policy.denied`; an unsettled one leaves the Run queued for another Runner in the pool
* `run.start_timed_out`: a claimed attempt missed its ready deadline and was requeued before it could start
* `run.attempt_requeued`: a *started* attempt released its native Checkpoint and was replaced, without changing the customer-visible logical Run. `recovery` is one-based and the current aggregate admits exactly one, so this appears at most once per Run
* `run.requires_action`: parked on an approval, custom tool, Question, or outcome evaluation (see `action.kind`)
* `run.action_authorized`: a Custom-tool ActionApproval authorized customer execution; the same Run now awaits its exact application result
* `run.resumed`: the awaited action arrived; a continuation is executing
* `run.input_delivered`: the harness answered a steering `user.message` delivered into the running turn, naming that `event` and a `disposition` of `delivered`, `queued`, or `refused`. This record, not the transport acknowledgement, is the delivery authority: `delivered` and `queued` fence the message to this Run, and `refused` leaves it to be promoted into the next one
* `run.lifecycle_hook`: one reviewed sandbox lifecycle-hook command settled — phase, ordinal, exit class, duration, the declared `on_failure`, and a bounded tail of its output; the event, not the Runner's memory, is the settlement authority, so a failed verification hook is recorded rather than passed silently
* `run.completed` / `run.failed` / `run.canceled`: terminal settlement; the usage receipt keys off this event

### `action_approval.*`

* `action_approval.pending`: a frozen call awaits a human decision
* `action_approval.resolved`: the decision, responder, and any instructions

### `outcome.*`

* `outcome.evaluation_started`: the grader began judging a settled revision
* `outcome.evaluation_completed`: the verdict, one of five results, with rationale and cost

### `span.*`

Telemetry adapters mint their own span names, so `span.<name>` is an open family rather than a fixed list. Spans are best-effort telemetry and never carry enforcement meaning; a consumer that does not recognize a span type should ignore it.

## List persisted events

```http theme={"theme":{"light":"github-light","dark":"github-dark"}}
GET /v1/sessions/{id}/events?limit=100&page=<cursor>
```

`limit` defaults to 100. Values greater than 1000 are capped at 1000; zero, negative, fractional, or nonnumeric values return `400 validation.malformed`. The response contains readable envelopes in `data`, content positions that cannot be produced in `unavailable`, and `next_page`. An unavailable entry names the original `seq`, content class, and `shredded` or `zdr` reason. A reviewed ZDR prompt may additionally expose only its analyzer version and author disposition, never the prompt, source digest, or findings.

This endpoint budgets a page by owned-log positions, not only readable events. A page can therefore have an empty `data` array and non-empty `unavailable` array. Its cursor is the last processed per-Session sequence number; continue until `next_page` is `null`.

## Replay and follow over SSE

```http theme={"theme":{"light":"github-light","dark":"github-dark"}}
GET /v1/sessions/{id}/events/stream
Accept: text/event-stream
```

On connection, the current alpha replays every persisted event after the cursor in internal pages and then stays live. Send the last processed sequence in `Last-Event-ID`, or pass the same cursor as `page`:

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl --no-buffer \
    "https://api.checkfu.com/v1/sessions/$CHECKFU_SESSION_ID/events/stream" \
    --header "Authorization: Bearer $CHECKFU_API_KEY" \
    --header "Checkfu-Version: 2026-08-31" \
    --header "Last-Event-ID: 12"
  ```

  ```ts TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  // `lastEventId` is the last fully processed `seq`, the same value the
  // `Last-Event-ID` header carries. Omit it to start from the beginning of the
  // Session; the client reconnects and resumes from its own cursor either way.
  for await (const event of checkfu.sessions.events.stream(sessionId, {
    lastEventId: 12,
  })) {
    handle(event)
  }
  ```
</CodeGroup>

`CHECKFU_API_KEY` and `CHECKFU_WORKSPACE_ID` come from
[Get access](/reference/access); `CHECKFU_SESSION_ID` is the Session you are
tailing.

Reconnect after the connection closes or a network failure and provide the last fully processed sequence. Pull-driven backpressure keeps at most one internal page in flight for a stalled consumer; reconnecting from `Last-Event-ID` resumes from the durable log. The SDK rejects replayed or reordered durable sequences instead of yielding duplicates.

If retention or ZDR leaves unreadable positions before the next readable event, the server emits a cursor-neutral `checkfu.cursor_gap` control frame. The SDK stops with `EventStreamCursorGapError`, whose `from`, `through`, and `next` fields identify the omitted range. Record or resolve that omission through the paged event ledger's `unavailable` entries before explicitly starting a new iterator from `through`; the SDK never skips evidence loss automatically.

### Opt in to live previews

Repeat `event_deltas` to request best-effort previews for the currently running turn:

```http theme={"theme":{"light":"github-light","dark":"github-dark"}}
GET /v1/sessions/{id}/events/stream?event_deltas=agent.message&event_deltas=agent.thinking
```

The stream first replays durable events through the requested cursor, then may emit these additional frames while it follows live work:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
event: event_start
data: {"type":"event_start","event":{"id":"evt_0123456789abcdef0123456789abcdef","type":"agent.message"}}

event: event_delta
data: {"type":"event_delta","event_id":"evt_0123456789abcdef0123456789abcdef","delta":{"type":"content_delta","content":"The repo","index":0}}
```

`agent.message` emits `event_start` followed by content deltas. `agent.thinking` emits `event_start` only; its content remains available in the final durable event when the harness provides it. If the corresponding observation commits, the `agent.message` or `agent.thinking` envelope uses the same event ID, so a client can replace its preview with the committed result.

Preview frames carry no SSE `id:` line, no `seq`, and no `provenance`. They never advance `Last-Event-ID`, never appear in event lists or replay, and may be dropped for a disconnected or stalled consumer. A failed, interrupted, or lease-lost turn can leave a preview without a final event; discard unmatched previews when the turn settles. After a transparent SDK reconnect, the iterator yields `{ type: "preview_reset", reason: "reconnect" }`; clear every unmatched preview before applying subsequent frames. A non-SDK client clears them at its observable HTTP reconnect. Always render the final durable event as authoritative. A DeliveryScreen agent does not expose `agent.message` previews because content has not yet passed its release-pinned screen.

## Resolve retained wire content

```http theme={"theme":{"light":"github-light","dark":"github-dark"}}
GET /v1/sessions/{id}/events/{event_id}/content?digest=sha256:<hex>
```

Returns the exact retained bytes as `application/octet-stream`. This route is the only way wire material crosses HTTP, and it is deliberately narrow: the key must hold an `admin` or `root` role (anything else is `403 policy.denied`), and the digest must be one the named `wire.call` event actually references. A digest that the event does not name, or whose bytes have passed their retention ceiling, is `404`. There is no browsable index of retained content, and internal storage identifiers never appear on the wire.

## Next steps

<CardGroup cols={2}>
  <Card title="Resume a stream reliably" icon="arrows-rotate" href="/guides/resume-a-stream">
    Handle disconnects, duplicates, and backpressure without losing or double-processing events.
  </Card>

  <Card title="Fold a session into named views" icon="code-branch" href="/guides/project-a-session">
    Fold the stream into named views: messages, tool calls, ActionApprovals, the subagent tree, and the context ledger.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.