Documentation menu

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

paramtyperequireddescription
orgstringyesOrg slug
projectstringyesProject slug

Handshake errors (returned as plain JSON, not a WebSocket close frame — the upgrade never happens):

  • 401{ "error": "Unauthorized" } — missing or invalid session cookie
  • 426{ "error": "Expected WebSocket upgrade" } — request didn’t send an Upgrade: websocket header
  • 400{ "error": "Missing org/project" }org or project query param absent
  • 404{ "error": "Not found" } — org slug doesn’t resolve, or the project slug doesn’t exist within it
  • 403{ "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 fieldmatch
levelexact match
typeexact match
environmentexact match
serviceexact match
device_typeexact match
qcase-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.