Skip to main content
Causeloop exposes a real-time event stream over WebSocket. Connect once and receive pushed frames as things happen in your workspace — no polling required.

Connection URL

wss://api.causeloop.ai/v1/stream?token=<jwt>&workspace=<workspace_id>
EnvironmentURL
Productionwss://api.causeloop.ai/v1/stream
Local developmentws://localhost:4000/v1/stream

Authentication

Browsers cannot set the Authorization header on a WebSocket handshake. Instead, pass your Causeloop Bearer JWT as a query parameter:
?token=<access_token>
Also pass the workspace parameter — the server validates it matches the workspace claim in the token and rejects cross-tenant subscriptions:
?workspace=<workspace_id>
Keep WebSocket URLs out of logs. The token query parameter is a full Bearer JWT. In production, construct the connection URL in memory and never expose it in URLs, analytics, or log pipelines.

Connection lifecycle

1

Open the connection

Connect to the WebSocket endpoint with token and workspace query params.
2

Receive the connected frame

Immediately after accepting, the server sends a connected frame confirming your workspace and user:
{
  "type": "connected",
  "payload": {
    "workspace_id": "ws_01abc123",
    "user_id": "usr_01abc456"
  },
  "ts": "2024-01-15T10:00:00.000Z"
}
3

Receive event frames

The server pushes domain event frames as they occur. See Event types below.
4

Heartbeat

Every 5 seconds the server sends a heartbeat frame. Use this to detect stale connections:
{
  "type": "heartbeat",
  "payload": { "ok": true },
  "ts": "2024-01-15T10:00:05.000Z"
}

Frame format

All frames share a common envelope:
{
  "type": "<event_type>",
  "payload": { ... },
  "ts": "2024-01-15T10:00:00.000Z"
}
type
string
required
The event type identifier (see table below).
payload
object
required
Event-specific data. Shape varies by event type.
ts
string (ISO 8601)
required
UTC timestamp of when the event was emitted by the server.

Event types

TypeTrigger
connectedSent once immediately after the connection is accepted.
heartbeatSent every 5 seconds to keep the connection alive.
issue.createdA new issue was ingested from a connector or created via API.
issue.status.changedAn issue’s status changed (e.g. newanalyzingresolved).
pattern.detectedA new pattern was identified or an existing pattern updated.
prediction.alert.firedA prediction alert rule triggered.
recommendation.status.changedA recommendation was accepted, implemented, or dismissed.
notification.createdA workspace notification was created for the connected user.
job.updatedA background report or export job changed status or progress.

issue.created payload

{
  "id": "iss_01abc123",
  "title": "Payment gateway timeout on checkout",
  "status": "new",
  "severity": "high",
  "risk_score": 74,
  "source": "pagerduty",
  "created_at": "2024-01-15T10:00:00.000Z"
}

issue.status.changed payload

{
  "id": "iss_01abc123",
  "previous_status": "new",
  "status": "analyzing",
  "changed_at": "2024-01-15T10:00:05.000Z"
}

pattern.detected payload

{
  "id": "pat_01xyz789",
  "name": "Payment gateway timeout under load",
  "risk_score": 82,
  "status": "emerging",
  "issue_count": 7,
  "detected_at": "2024-01-15T10:00:10.000Z"
}

job.updated payload

{
  "job_id": "rpt_job_01abc789",
  "status": "running",
  "progress": 0.65,
  "result_url": null,
  "error": null,
  "created_at": "2024-01-15T09:55:00.000Z",
  "finished_at": null
}
When status is "succeeded", result_url is populated with the download path.

Close codes

CodeMeaning
4401Missing, invalid, or expired token. Re-authenticate and reconnect.
4003Workspace mismatch — the workspace param does not match the token’s workspace claim.
1001Server going away — graceful drain (e.g. deploy). Reconnect immediately.
1011Unexpected server error. Reconnect with backoff.

Example client

function connectCauseloopStream({ accessToken, workspaceId, onEvent }) {
  const url = new URL('wss://api.causeloop.ai/v1/stream');
  url.searchParams.set('token', accessToken);
  url.searchParams.set('workspace', workspaceId);

  const ws = new WebSocket(url.toString());
  let reconnectDelay = 1000;

  ws.addEventListener('open', () => {
    console.log('[WS] connected');
    reconnectDelay = 1000; // reset backoff on successful open
  });

  ws.addEventListener('message', (event) => {
    const frame = JSON.parse(event.data);

    if (frame.type === 'heartbeat') return; // internal keepalive, ignore
    if (frame.type === 'connected') {
      console.log('[WS] authenticated as workspace', frame.payload.workspace_id);
      return;
    }

    onEvent(frame);
  });

  ws.addEventListener('close', ({ code, reason }) => {
    console.warn(`[WS] closed: ${code} ${reason}`);

    if (code === 4401) {
      // Token expired — refresh before reconnecting
      console.error('[WS] invalid token, re-authenticate before reconnecting');
      return;
    }

    if (code === 4003) {
      console.error('[WS] workspace mismatch, check your workspace_id');
      return;
    }

    // Reconnect with exponential backoff for all other close codes
    const delay = reconnectDelay + Math.random() * 500;
    console.log(`[WS] reconnecting in ${Math.round(delay)}ms`);
    setTimeout(() => connectCauseloopStream({ accessToken, workspaceId, onEvent }), delay);
    reconnectDelay = Math.min(reconnectDelay * 2, 30_000);
  });

  ws.addEventListener('error', (err) => {
    console.error('[WS] error', err);
  });

  return ws;
}

// Usage
const ws = connectCauseloopStream({
  accessToken: '<your_access_token>',
  workspaceId: 'ws_01abc123',
  onEvent: (frame) => {
    switch (frame.type) {
      case 'issue.created':
        console.log('New issue:', frame.payload.title);
        break;
      case 'pattern.detected':
        console.log('Pattern:', frame.payload.name, '— risk:', frame.payload.risk_score);
        break;
      case 'job.updated':
        console.log('Job progress:', frame.payload.progress, frame.payload.status);
        break;
    }
  },
});

Event bus integration

Server-side routers can push frames directly into the stream using the publish function (for workspace-wide events) or publish_user (for user-scoped events). This allows any API mutation — creating an issue, updating a recommendation — to fan out a real-time event to all connected clients in that workspace without polling.
WebSocket messages do not count against the REST rate limit. For real-time use cases such as live dashboards, notification badges, and job progress bars, prefer the WebSocket stream over polling REST endpoints.