Incremental ingest
Use the Watcher API to upload Claude Code or Codex transcripts in chunks. The server stores normalized messages and skips content that it already holds. A successful response identifies both the end of your chunk and the live branch of the session.
Prerequisites
Deploy server support before updating uploaders. Authenticate with an API key that grants
sessions:ingest or sessions:write:any, or with a signed-in user who can
write the session. See Authentication and permissions.
Send a chunk
Send HTTP requests directly or pass the same trajectory fields to the
Python SDK ingest method. Your uploader must choose chunks,
check their serialized size, and persist acknowledged progress.
Send one Claude Code or Codex trajectory to POST /v1/ingest. Include the
same source conversation ID on every request. Put append_after and fork
on the item in trajectories, not on the request itself. Omit append_after
for an opening chunk. To continue, set it to results[0].chunk_end from the
previous response. Using an anchor or fork=true requires exactly one
trajectory in the request.
Use /v1/ingest for this protocol; the older per-session append route keeps
its existing contract.
For example, this opening request uploads one Claude Code message:
{
"source": "my-uploader",
"trajectories": [{
"format": "claude-code-jsonl",
"session_id": "my-conversation",
"timestamp": "2026-10-30T12:00:00Z",
"entries": [{
"type": "user",
"uuid": "message-1",
"parentUuid": null,
"sessionId": "my-conversation",
"timestamp": "2026-10-30T12:00:00Z",
"message": {"role": "user", "content": "Check the test configuration."}
}]
}]
}
For the next chunk, keep the conversation ID and add append_after to the
trajectory, using the returned {session_id, trajectory_id} unchanged.
Send only the new records and any context needed to parse them independently.
Do not split an assistant message or separate a tool result from the call it needs.
Read the response
The response has one results entry per input, in the same order:
results[i] describes trajectories[i]. Duplicate, empty, and skipped inputs
keep their slots. Each result contains these fields:
| Field | Meaning |
|---|---|
chunk_end | The {session_id, trajectory_id} at the end of this chunk. It can lie inside an already-stored chain. Use it as the next request's append_after. |
leaf | The session's live branch endpoint. It can differ from chunk_end on replay. Use it to read the current trajectory, not to prove this chunk's coverage. |
status | stored for new content, already_stored for a replay or message-free input, or partially_stored for an input containing both known and new content. |
A chunk with no messages can return a null chunk_end. Track its source
position locally and include any context a later chunk needs.
There is no server receipt for deferred raw records.
The existing aggregate session_id, trajectory_ids, counts,
resolved_tool_calls, and append_anchor keep their batch meanings.
append_anchor follows the older continuation rules and can be null for an
interior replay. Use chunk_end for incremental upload progress.
Resume, rewind, and retry
Save each successful chunk's source position and chunk_end together before
sending the next one. Keep progress separate for each API URL, capture
identity, conversation, and source file. Only reuse an acknowledgement when
the corresponding source prefix still matches.
For a rewind, set fork=true on the trajectory. Each branch can start at its
shared parent's anchor without resending that parent's raw record. One
request may also include both branches whose raw parentUuid identifies the
same omitted parent. Without the flag, new content branching from an occupied
message is rejected with 409 anchor_conflict. Previously stored branches
remain available.
On a lost response or a transport failure, retry the same chunk. Stored
overlap is harmless, including a chunk that extends beyond previously stored
content. A missing session (404) or unknown_anchor permits replay from an
earlier acknowledgement or the start. A content conflict does not prove
that earlier content is missing. Correct the refused input before retrying.
Each chunk must parse independently. For Codex, include the session metadata and active instruction context needed by the new records. Start an unanchored chunk at a compaction boundary and include the retained summary and prompts.
Bounds and refusals
Keep the entire serialized request within 8,388,608 bytes. If no independently parseable chunk fits, stop and reduce the input rather than splitting a message or sending an oversized whole-file fallback.
A 400 content refusal or 409 anchor_conflict does not acknowledge the
chunk. Correct the input or anchor before retrying. A
409 session_owner_conflict requires correcting the capture identity or
conversation selection. Do not bypass it with a new offset.