The three rules
Order by seq
seq is the per-Session sequence. It is the cursor you persist and the order you process in.Skip what you have
Delivery is at-least-once. In one Session stream the cursor filters replays; across unordered consumers, deduplicate on the globally unique
id.Settle on events
A closed connection is not a finished workflow. Read both the status event and its
stop_reason before deciding to stop following.Connecting and resuming
CHECKFU_API_KEY and CHECKFU_WORKSPACE_ID come from Get access; CHECKFU_SESSION_ID is the sess_… you are following. The TypeScript tab uses the TypeScript SDK with this client:
Last-Event-ID:
seq in id:, the event type in event:, and the full envelope in data:.
- Persist the cursor after the side effect, never before. At-least-once delivery makes a replayed event harmless; a skipped one is lost permanently.
- Deduplicate on
seq, not on a growing set of IDs. Within one Session streamseqis monotonic, so the cursor you already persist is a complete and bounded dedupe filter. Keep an ID set only where ordering is not guaranteed, such as across webhook deliveries, whereidis the right key.
Live previews do not move the cursor
The stream is durable-only unless you repeat theevent_deltas query parameter. An opted-in stream may also receive event_start and event_delta frames for live agent.message output, plus event_start for agent.thinking. These frames have no SSE id: line and no seq, are never replayed, and may be dropped. Do not save a cursor for them.
If the corresponding observation commits, the final persisted agent.message or agent.thinking event uses the preview’s event ID. Reconcile the temporary rendering to that event, perform durable side effects from the final envelope, and advance Last-Event-ID only from its numeric seq. A failed, interrupted, or lease-lost turn can leave no matching final event, so discard unmatched previews when the turn settles. A non-SDK client may also clear them at an observable HTTP reconnect; the SDK reconnects transparently, so recreating its iterator is the observable reconnect boundary. The TypeScript SDK enforces cursor neutrality automatically when you pass eventDeltas to events.stream.
Settlement
Use both the event type andstop_reason to decide whether your workflow follower can return:
session.status_idle settles one Run boundary; it does not end the Session. The requires_action variant is deliberately intermediate: stopping there can hide the continuation Run that follows an approval, custom-tool result, or outcome evaluation. session.status_waiting is not settlement at all: the Session is parked until the named action advances. See Handle an approval.
Backpressure
A stalled consumer does not lose events. Checkfu keeps at most one internal page in flight and lets the rest wait in the durable log, so a slow reader applies backpressure rather than dropping data. Reconnecting fromLast-Event-ID resumes from that log.
This means a consumer that falls far behind is fine. It also means the stream is not a substitute for a queue: if your processing is genuinely slow, read with the list endpoint on your own schedule instead.
Catching up without streaming
GET /v1/sessions/{id}/events pages the same log. limit defaults to 100 and caps at 1000.
The list supports two mutually exclusive entry points. Pass a decimal event seq as after to begin immediately after that durable position—the same coordinate carried by SSE Last-Event-ID. Or start without after and continue an ordinary paginated walk by echoing each opaque next_page value as page. Never send after and page together, and never try to construct an opaque page token from a sequence.
Use it for backfill, reconciliation after downtime, or rebuilding a projection from scratch. The stream and the list return the same persisted envelopes.
Next steps
Events
The envelope, the full event catalog, and the drive events you can post.
Project a session
Fold the stream into named views: messages, tool calls, ActionApprovals, the subagent tree, and the context ledger.