curl --request GET \
--url https://app.hookie.ai/admin/api/stream \
--cookie hookie_session=const options = {method: 'GET', headers: {cookie: 'hookie_session='}};
fetch('https://app.hookie.ai/admin/api/stream', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://app.hookie.ai/admin/api/stream"
headers = {"cookie": "hookie_session="}
response = requests.get(url, headers=headers)
print(response.text)"<string>"{
"error": "<string>",
"retry_after": 123,
"upgrade_url": "<string>"
}{
"error": "<string>",
"retry_after": 123,
"upgrade_url": "<string>"
}{
"error": "<string>",
"limit": 123,
"retry_after": 123,
"upgrade_url": "<string>"
}Tail the current workspace's live event stream over SSE or WebSocket
The same live tail as /v1/stream, for the console and the CLI (hookie tail, hookie listen) rather than for a server-side integration: it authenticates with the browser session or a connected agent’s bearer token instead of an ingest key, and the tenant comes from that credential rather than from the key. GET only — the route resolves before the admin CSRF guard, which only inspects mutating methods.
The stream is BEST-EFFORT by contract. Durable delivery is the outbound-delivery queue; a tail is for watching, and the absence of an event here never proves the event was not received. The resume buffer holds 1000 events and 5 minutes, and a resume outside that window succeeds but is preceded by a stream-gap control event.
An agent token needs hookie:read. Its 429 is the plan’s concurrent-connection cap, counted across SSE responses and WebSockets together and across the console and the CLI alike — a console Observability tab left open counts as one.
curl --request GET \
--url https://app.hookie.ai/admin/api/stream \
--cookie hookie_session=const options = {method: 'GET', headers: {cookie: 'hookie_session='}};
fetch('https://app.hookie.ai/admin/api/stream', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://app.hookie.ai/admin/api/stream"
headers = {"cookie": "hookie_session="}
response = requests.get(url, headers=headers)
print(response.text)"<string>"{
"error": "<string>",
"retry_after": 123,
"upgrade_url": "<string>"
}{
"error": "<string>",
"retry_after": 123,
"upgrade_url": "<string>"
}{
"error": "<string>",
"limit": 123,
"retry_after": 123,
"upgrade_url": "<string>"
}Authorizations
The console's sealed session cookie, set by WorkOS AuthKit. Mutating requests also require the X-Requested-With CSRF header.
Headers
Standard SSE resume header, sent automatically by EventSource on reconnect. Takes precedence over after_seq when present. Same semantics and same 1000-event / 5-minute buffer window.
x >= 0Send exactly 'websocket' (compared case-sensitively) to get a 101 WebSocket upgrade instead of an SSE stream. Any other value, or its absence, yields the SSE response.
websocket Query Parameters
Resume cursor: replay buffered events with seq greater than this value. Ignored when a Last-Event-ID header is present. Non-numeric or negative values are treated as absent. The resume buffer holds at most 1000 events and drops anything older than 5 minutes; a request for evicted events still succeeds but is preceded by a stream-gap control event.
x >= 0Only deliver events belonging to this project. Applied by the stream Durable Object, not by the client, because the plan caps CONCURRENT CONNECTIONS (one, on Free) — a subscriber that attached to everything and filtered locally would spend its whole allowance to watch one project. STRICT: an event with no project (a workflow instance outside one, or anything buffered before events carried a project at all) does NOT match a project filter. The per-tenant seq cursor is unaffected, so a filtered stream skips numbers by design; a gap in seq is not a lost event. This is not a security boundary — the tenant gate is; an id from another tenant simply matches nothing.
Only deliver events in this dataset. Combines with project_id — both have to match. Applied server-side for the same reason.
Response
WebSocket upgrade accepted (the request carried 'Upgrade: websocket'). Each live event arrives as one JSON text frame: {seq, id, dataset, received_at, data, project_id, webhook_id}. When a resume cursor was supplied and part of the requested window had been evicted, a {"type":"stream-gap","missed_from":,"resume_from":} frame is sent first, followed by the replayable buffered events. The socket is hibernatable and answers a 'ping' text frame with 'pong'. The live path is lossy by contract — durable delivery is the outbound-delivery queue, not this stream.