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:
Stream entries (SSE)
Live tail of the journal over Server-Sent Events, seeded with a recent batch then switched to polling.- Seed: sends the most recent 50 entries matching filters, then switches to live polling.
- Poll interval: 1 second.
- 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 switching to live polling. 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.
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”.