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

# Export and own your logs

> Pull a session's complete history as one self-describing document, read its context ledger, and export the audit trail.

Every log you create is exportable in a stable, self-describing format you can archive, replay, or take elsewhere. This guide covers the three export surfaces (the session log, the context ledger, and the audit trail) and what each one answers.

## Export one session, completely

`GET /v1/sessions/{id}/export` returns the whole log as a single self-contained document. `CHECKFU_API_KEY` and `CHECKFU_WORKSPACE_ID` come from [Get access](/reference/access#the-three-variables-ready); `SESSION` is the `sess_…` you are exporting.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -s "https://api.checkfu.com/v1/sessions/$SESSION/export" \
  --header "Authorization: Bearer $CHECKFU_API_KEY" \
  --header "Checkfu-Version: 2026-08-27" \
```

The API sends that one JSON document incrementally: it freezes `end_seq`, reads
bounded pages in order, and stops reading when the client disconnects. `curl`
therefore writes large exports directly to disk without holding the complete
history in memory.

The TypeScript SDK exposes the same response as an owned byte stream:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createWriteStream } from "node:fs"
import { Readable } from "node:stream"
import { pipeline } from "node:stream/promises"
import { Checkfu } from "@checkfu/sdk"

const checkfu = new Checkfu({
  apiKey: process.env.CHECKFU_API_KEY,
})

const body = await checkfu.sessions.exportStream(process.env.SESSION!)
await pipeline(Readable.fromWeb(body), createWriteStream("session-export.json"))
```

Read the SDK stream to EOF or cancel it if you stop early; cancellation
propagates to the export request. The Checkfu console uses the browser's
streaming file API when available. Browsers without it use a compatibility
Blob capped at 32 MiB and show an error above that limit. There is no
unbounded in-memory fallback.

The response identifies itself so an offline reader needs no out-of-band context:

| Field                                       | Meaning                                                                                                    |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `format`                                    | Always `checkfu.session-export`, the owned event log — not a Session Bundle                                |
| `schema_version`                            | The export format's own version (`1`)                                                                      |
| `wire_version`                              | The dated wire contract the events were serialized under                                                   |
| `session_id`, `workspace_id`, `exported_at` | Provenance                                                                                                 |
| `events`                                    | Every persisted event envelope, in order                                                                   |
| `unavailable`                               | Every position whose content is gone, each with its `seq`, `content_class`, and `shredded` or `zdr` reason |
| `end_seq`                                   | The last sequence number included. Compare against a later export to see what's new                        |

Because envelopes keep their `seq`, `id`, and `schema_version`, an export is **replayable** as an event tape: you can rebuild the transcript and account for every unavailable position, or diff two exports by `end_seq`. It does not carry the actor graph, recovery frontier, or capsules of a Session Bundle. For incremental sync instead of snapshots, page `GET /v1/sessions/{id}/events` with a cursor. See [Resume a stream](/guides/resume-a-stream) and [Session execution graph](/concepts/session-execution-graph).

Erasure stays honest in exports: content expired, crypto-shredded, or omitted under zero-data-retention is absent, never silently reconstructed. Every owned sequence position is still accounted for: readable envelopes appear in `events`, while unavailable positions identify their `seq`, optional content class, and `shredded` or `zdr` reason. An explicit Session erasure is stronger and makes the export endpoint return `404`.

## Read an export offline

The [`session-export-inspector`](https://github.com/checkfu/checkfu/tree/main/examples/session-export-inspector) example opens one exported `.json` file directly from your machine with no Checkfu credential and no network connection. Drop the file you exported above onto the page: it parses the unknown JSON, decodes it as a `SessionExport`, and proves every sequence position is accounted for before rendering a readable transcript with the same `@checkfu/ui` kit the console uses. Unavailable positions stay explicit — grouped by reason, content class, and structural-replay status — and are never fabricated as readable events.

The inspector validates schema validity and exact position coverage only. It does not claim event authenticity, audit-chain integrity, provider truth, or a full session replay, and `structural_replay: incomplete` stays visible. Files larger than 32 MiB are rejected before they are read, and malformed documents never reveal their contents in an error. Real exports may contain sensitive session content, so do not commit them; the example ships synthetic fixtures only.

<Note>
  This endpoint exports what *happened*, not what the agent *knows*. Memory stores are separate and export separately, through `GET /v1/memory-stores/{id}/export-pages`: a resumable, normalized graph stream of revisions, document versions, and refs. If you are answering "get all my data out", you want both halves. See [Memory](/concepts/memory#portable-exports).
</Note>

## Read the context ledger

`GET /v1/sessions/{id}/ledger` answers a different question: **"what is loaded and what has it cost"** rather than "what happened". It is a rebuildable session-level view carrying the admitted definition version, current and cumulative context-token counts, the loaded capabilities (tools and skills by reference), and every compaction. Compactions come back as `compactions`, each with its `event_id`, `seq`, `strategy`, `tokens_before`, `tokens_after`, and `created_at`. When the platform has bounded the retained conversation itself, a second `condensations` array reports each of those: `event_id`, `seq`, the `through_sequence` it covered, how many user and agent messages it folded into an elision marker (`condensed_user_messages` and `condensed_agent_messages`), and `created_at`. `condensations` is absent rather than empty when nothing has been condensed, so read it defensively. The ledger deliberately contains no prompts, messages, or argument content, so it is shareable where transcripts are not. (For which mounts a specific Run received, read that Run's `session.mounts_resolved` event instead.)

## Export the audit trail

Governance actions (PermissionAssignment changes, approval responses with responder identity, policy decisions, credential access) live in an append-only audit log, queryable and exportable from day one:

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  # Query interactively
  curl -s --get "https://api.checkfu.com/v1/audit" \
    --header "Authorization: Bearer $CHECKFU_API_KEY" \
    --header "Checkfu-Version: 2026-08-27" \
    --data-urlencode "action=permission_assignment.created"

  # Bulk export for your SIEM: filter by actor, action, resource, or position
  curl -s --request POST https://api.checkfu.com/v1/audit/export \
    --header "Authorization: Bearer $CHECKFU_API_KEY" \
    --header "Checkfu-Version: 2026-08-27" \
    --header "Content-Type: application/json" \
    --data '{ "after": 0, "limit": 1000, "resource_kind": "permission_assignment" }'
  ```

  ```ts TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import {
    createAuditChainVerificationState,
    verifyAuditChain,
    verifyAuditChainPage,
  } from "@checkfu/sdk"

  // Query interactively
  const records = await checkfu.audit.list({ action: "permission_assignment.created" })

  // Bulk export for your SIEM: filter by actor, action, resource, or position
  const bulk = await checkfu.audit.export({ after: 0, limit: 1000, resource_kind: "permission_assignment" })

  // Verify one complete, unfiltered export page, including each content hash.
  const page = await checkfu.audit.export({ after: 0, limit: 1000 })
  const verification = await verifyAuditChain(page.records)
  if (!verification.valid) console.error(JSON.stringify(verification))

  // Continue without buffering the full export. Persist `state` beside
  // `next_after`, then supply both after a poller restart.
  let after = 0
  let state = createAuditChainVerificationState()
  for (;;) {
    const current = await checkfu.audit.export({ after, limit: 1000 })
    const result = await verifyAuditChainPage(current.records, state)
    if (!result.valid) {
      console.error(JSON.stringify(result))
      break
    }
    state = result.state
    if (current.next_after === null) break
    after = current.next_after
  }
  ```
</CodeGroup>

`after` is a position cursor: feed the last record's position back in to export incrementally. Audit records are append-only and survive subject erasure with the subject link destroyed. The fact that an action happened is permanent even when the actor's identity is gone.

### Verify the trail is untampered

Each record carries two store-assigned fields that make the trail tamper-evident rather than merely append-only: `content_hash`, a SHA-256 over the record's `checkfu.audit.v1` canonical immutable bytes, and `prev_hash`, the `content_hash` of the Workspace's previous chained record (`null` at the chain's genesis). Both appear in list responses and the SIEM export. `verifyAuditChain` and `verifyAuditChainPage` recompute every content hash as well as checking every predecessor link: `content_hash_mismatch` means immutable record content changed, while `prev_hash_mismatch` means the chain was broken or reordered. Their failure values also carry the failing `broken_at_sequence` and a stable `reason`, so a poller can record them without parsing exception text.

Verify unfiltered pages in ascending `sequence`; a filtered export omits predecessors and is not independently chain-verifiable. `sequence` orders verifier input and detects duplicate or out-of-order pages, but it is deliberately excluded from the hashed bytes and may have gaps because its counter is global across Workspaces. Cryptographic adjacency—and therefore a skipped chained page—is detected by `prev_hash`, not by trusting the cursor column being audited. Pass the successful page's serializable state to the next page, together with `next_after`. An empty page preserves that state. Records written before chaining was enabled carry `null` in both hash fields and are accepted only as an unhashed legacy prefix; their content and completeness cannot be proven retrospectively. A null `content_hash` after chaining begins is a failure.

A valid result proves that the exported canonical records are mutually consistent. It does not prove that an actor's narrated action was true, nor that a filtered or otherwise incomplete export contains every Workspace record.

### Verify with the CLI

`checkfu audit verify` is the first-party command that walks an unfiltered ascending export through the canonical SDK verifier, so an operator does not have to write and secure their own poller:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
# One-shot verification, state kept in memory only
CHECKFU_API_KEY=… CHECKFU_WORKSPACE_ID=… checkfu audit verify

# Resumable: checkpoint the verified prefix to a protected file
checkfu audit verify --state ~/.checkfu/audit-verifier.json

# Start over from the beginning, replacing the old checkpoint
checkfu audit verify --state ~/.checkfu/audit-verifier.json --restart

# JSON summary for a SIEM pipeline
checkfu audit verify --state ./verifier.json --json
```

The command always sends an unfiltered `POST /v1/audit/export` ascending by the endpoint's contract. It offers no actor, action, resource, or time filters, because a filtered export omits predecessors and cannot prove chain completeness.

With `--state` on POSIX, one renewable lock is held before the command reads or resets the checkpoint and remains held through the complete audit walk and every page publication. A competing verifier for the same directory waits at lock acquisition, so it cannot fetch from a stale prefix or publish an older valid cursor after the active owner. Lock acquisition is bounded to roughly 30 seconds; timeout occurs before checkpoint reads, writes, or audit requests. Stateful verification is unavailable on Windows until native owner-SID, ACL, reparse-point, and real-lane enforcement can uphold the same protection; omit `--state` there to run the unchanged stateless verifier.

The verified prefix is checkpointed atomically after each successful page: the checkpoint is a mode-0600, current-user-owned regular file (never a symlink), bounded to 64 KiB, bound to the API origin and Workspace id, and written through an unpredictable same-directory temporary that is renamed over the prior file only after the new page verifies. Every export envelope's Workspace id must agree with configuration, the checkpoint, its records, and adjacent pages. When configuration omits a Workspace, the first envelope establishes the durable binding, including for an empty export. A missing file starts a new chain; an existing file resumes at its cursor. `--restart` begins from zero but replaces the old checkpoint only after the first new page verifies, so a restart that fails early preserves it. Pass `--page-size` (1–1000, default 100) to lower the page weight for large detail records.

On success the output carries only counts: pages and records processed, Workspace, last sequence, and chained versus legacy totals. On a broken chain it prints the stable `reason` and `broken_at_sequence` and exits 6 (`invalid`); transport, auth, and decode failures keep their existing exit classes and leave the last good checkpoint intact. The output never includes an audit record, actor, detail JSON, hash, or raw checkpoint — validity proves chain consistency, not narrated truth or provider completeness.

<Note>
  Audit `action` names are their own vocabulary (`permission_assignment.created`, `api_key.revoked`, `session.deleted`, …), a **different namespace** from event types, even where they describe related moments. To discover the values present in your Workspace, list unfiltered first and filter on what you see; the generated API reference documents the query parameters.
</Note>

## Answer the per-credential question

`GET /v1/audit?connection_id=…` answers the single most important vault question — who and which agent used one Connection, when, and for what — in one read. The CapabilityGateway attributes every connection-related fact to `connection_id`, **including a pre-authorization denial it records against the Run** (a ToolInvocation refused because the Connection was revoked), so a revoked credential's last refused use is visible in the same filtered read. Each record carries the acting Principal, the requester, the tool, and the argument-hash in its `detail_json` — never custody material. The `connection_id` filter is a queryable projection outside the D91 hash chain, so it changes no `content_hash` byte and every export still verifies.

## The three questions these answer

Exports carry no references a reader can't resolve from the document itself, so they remain readable outside Checkfu. Day to day, the three endpoints map to the three compliance questions: "show me everything this agent did" (export), "what was loaded and what has it cost" (ledger), "who approved that" (audit).

## Next steps

<CardGroup cols={2}>
  <Card title="Resume a stream" icon="rotate" href="/guides/resume-a-stream">
    Incremental paging over the same log the export snapshots.
  </Card>

  <Card title="Events reference" icon="list-timeline" href="/reference/events">
    The envelope and catalog every exported event conforms to.
  </Card>
</CardGroup>
