Live Tail
Stream newly-ingested events for a project in real time over WebSocket.
Live Tail pushes events to connected clients the moment they’re ingested — before they’re durably written and queryable via GET /api/logs. Each project streams through its own isolated connection; a client only ever receives events for the project it’s authorized for.
GET /ws/tail
Upgrade to a WebSocket connection.
wss://api.scrywatch.com/ws/tail?org=<orgSlug>&project=<projectSlug>
Auth: session cookie only — not a Bearer API key. The session=<token> cookie is read directly off the Cookie header (this is a dashboard-only endpoint; there’s no API-key path).
Query params
| param | type | required | description |
|---|---|---|---|
| org | string | yes | Org slug |
| project | string | yes | Project slug |
Handshake errors (returned as plain JSON, not a WebSocket close frame — the upgrade never happens):
401—{ "error": "Unauthorized" }— missing or invalidsessioncookie426—{ "error": "Expected WebSocket upgrade" }— request didn’t send anUpgrade: websocketheader400—{ "error": "Missing org/project" }—orgorprojectquery param absent404—{ "error": "Not found" }— org slug doesn’t resolve, or the project slug doesn’t exist within it403—{ "error": "Forbidden" }— the session user is not a member of the org that owns the project
Setting a filter
Once connected, send a JSON text frame to scope which events you receive. All fields are optional and combine with AND; omit the message entirely (or send "filter": {}) to receive every event for the project.
{ "type": "filter", "filter": { "level": "error", "service": "checkout-api", "q": "timeout" } }
| filter field | match |
|---|---|
| level | exact match |
| type | exact match |
| environment | exact match |
| service | exact match |
| device_type | exact match |
| q | case-insensitive substring match against message |
Sending a new filter message replaces the previous filter (it is not merged). There is no acknowledgement frame — the filter takes effect on the next broadcast.
Receiving events
The server pushes a message per ingested batch that has at least one matching event:
{
"type": "events",
"events": [
{
"id": "evt_01hxyz",
"project_id": "proj_01abc",
"timestamp": 1720392000000,
"level": "error",
"type": "crash",
"message": "Unhandled exception in checkout flow",
"user_id": "user_42",
"session_id": "sess_abc123",
"environment": "production",
"service": "checkout-api",
"device_type": "ios",
"metadata": {},
"trace_id": "trace_01",
"span_id": "span_01"
}
]
}
Event objects have the same shape as an item in GET /api/logs’s results array. Batches with zero events matching your current filter are not sent at all.
Common responses do not apply to this endpoint (it isn’t gated by requireApiKey/requireSession middleware, doesn’t accept API keys, and has no usage-limit or rate-limit behavior) — see the handshake errors above instead.