Every endpoint requires authentication and is workspace-scoped via the session context.
workspace_id is always pulled from the session, never from query params — cross-workspace reads are impossible.journal.Writer. The one writable surface is the operator-facing priority marker (POST /api/v1/journal/{id}/priority), which updates only the priority column on an existing row and emits its own memory.priority_changed audit entry.
Endpoints
List entries
The primary read surface — page through entries with a rich, AND-combined filter set.IN (?, ?, ...) predicates.
Response:
200 OK
Errors:
Free-text search
q is split on whitespace and the terms are AND-combined, so word order does
not change the result set: q=session%20morgan returns exactly what
q=morgan%20session returns. The last term is a prefix match — q=morg
reaches morgan — while earlier terms match whole words.
Each term is wrapped in an FTS5 phrase literal before it reaches SQLite, so
operators inside the input (AND, OR, NOT, NEAR(...), *, column:, a
bare ") are literal search text. They can neither restructure the query nor
produce a 500. Terms containing no letter or digit are dropped; input made up
entirely of such terms matches nothing rather than everything.
The index covers summary and payload only — never entry_type.
References
crew_id, agent_id, crew_ids and agent_ids accept an id, a slug, or a
display name (case-insensitive), resolved within the caller’s workspace. A
value that resolves to nothing is used as-is against the id column, so an id
whose row has since been deleted still reaches that entity’s history and a typo
returns zero rows instead of silently widening the filter. A display name shared
by several agents matches all of them.
Stream entries (SSE)
Live tail of the journal over Server-Sent Events, seeded with a recent batch then driven by the emitter’s flush notifications.- Seed: sends the most recent 50 entries matching filters, then goes live.
- Wake-up: notification-driven. The writer signals the stream as soon as a batch commits, so a new entry is on the wire within milliseconds of the flush rather than on a fixed clock. A 20 s fallback ticker re-polls regardless, covering the case where a notification is missed or the wired emitter does not support them. (This used to be a flat 1 s poll per connected client, which is what older revisions of this page described.)
-
Heartbeat:
: heartbeatcomment every 15s to keep proxies from closing idle connections. Event handlers ignore comments. - Watermark: compound (ts, id) so bursts sharing a ms timestamp aren’t partially dropped.
-
Reconnect: clients send the SSE
Last-Event-IDheader; the server looks the entry up, treats itstsas the lower bound, and pages through the gap (up to 500 entries, 10 × 50-row pages) before going live. A disconnect that produced more than 500 matching entries truncates the older end of the gap and the server logs awarn— clients should reconcile by re-fetching/api/v1/journal?since=<last-known-ts>if they need a longer window. Any client that can set request headers should resume this way, andcrewship journal --followdoes:internal/cli/sse.gosetsLast-Event-IDandfollowJournalthreads it across its own bounded-backoff reconnects. A browserEventSourcecannot set request headers. It sendsLast-Event-IDonly on its built-in automatic retry — a web client that closes the connection and constructs a newEventSource(which is what the web UI does, so it can control the backoff and stop polling on success) starts a fresh stream with noLast-Event-IDand must close its own gap with/api/v1/journal?since=<last-known-ts>. The UI’s journal hook does exactly that: while the stream is down it pollssinceon a rolling watermark, walking the cursor up to 4 pages per tick, and reports “Entries missing” if the backlog was deeper than that.
Get single entry
Fetch one entry by ID, workspace-scoped.id is the journal entry ID. The request has no body;
the workspace is always taken from the authenticated session.
Returns one entry by ID, workspace-scoped. The response body is a single object whose shape matches one element of the entries[] array on List — same fields, same omitempty rules.
Response: 200 OK
Count entries
Total matching a filter, for result-set badges that stay honest under filter changes.cursor and limit. The UI uses this to render result-set badges that stay honest under filter changes — without it, the only way to know the total was to page through every entry.
Query parameters: identical to List, except cursor and limit are silently ignored.
Response: 200 OK
Set entry priority
The only operator-mutable surface — flag an entry’s importance without touching its payload.normal, high, pin, permanent). Marker affects compaction and recall — see the Crew Journal guide.
Authorization: caller must hold
OWNER or ADMIN on the workspace. MEMBER and below get 403.
Response:
200 OK
memory.priority_changed audit entry to the journal whose payload carries target_entry_id, previous_priority, new_priority, and the supplied reason. The audit emit is best-effort — the priority update is durable even if the audit write fails (a warning is logged in that case).
Errors:
Workspace lookup table
The join surface for journal cards — entries store stable IDs only, this resolves them to display strings.internal/api/journal_lookup.go.
Response: 200 OK
null (the JSON arrays are always present), so the frontend can .find() without nil guards.
Soft-deleted crews and agents (
deleted_at IS NOT NULL) are filtered out. Missions have no soft-delete column; all rows are returned, ordered by created_at DESC within the cap.
The journal entries themselves never carry display strings — they store stable IDs only. This endpoint is the join surface used by every UI card.
Errors: 401 on missing workspace; 500 on DB error.
Spend rollup
Cost rollup overcost.incurred journal entries and pipeline_runs.cost_usd (#1404) — a separate, journal-native surface from Paymaster’s /api/v1/paymaster/spend/*.
Response:
200 OK
Errors:
CLI:
crewship spend --window 7d --top 10 — see crewship spend.
Tenancy
workspace_idis always pulled from the session context — not from query params.- Cross-workspace reads are impossible:
journal.List/Get/Countrefuse to run without a workspace filter. - Unknown or cross-tenant IDs return
404with the same body as “not found”.