Documentation

WebSocket Streaming

Receive real-time session events as your test runs via WebSocket.

Connection

Connect to the stream for a test by opening a WebSocket to the following URL. Replace {test_id} with the ID returned when you created the test.

wss://stream.simutest.dev/tests/{test_id}

Authenticate by including your API key in the Authorization header as a Bearer token. The connection will be rejected with 4401 if the key is invalid or lacks the required scope.

Tip: The stream_url field in the create test response already contains the fully-formed WebSocket URL for that test.

Event Types

All messages are JSON-encoded. Each message includes a type field, a timestamp (ISO 8601), and a data object.

Event TypeDescription
session_startedA new AI session has started; includes persona details
session_actionThe session performed a navigation action with thinking snippet
session_completedA session finished; includes outcome, scores, and summary
test_progressPeriodic progress update for the overall test
test_completedAll sessions finished; includes aggregate scores and report URL

Event Payloads

session_started

{
  "type": "session_started",
  "timestamp": "2025-01-15T10:02:34Z",
  "data": {
    "session_id": "sess_xyz789",
    "persona_name": "Busy professional, 35, mobile-first",
    "viewport": "mobile"
  }
}

session_action

{
  "type": "session_action",
  "timestamp": "2025-01-15T10:02:41Z",
  "data": {
    "session_id": "sess_xyz789",
    "action": "click",
    "target": "Pricing link in nav",
    "thinking_snippet": "I need to find the pricing page. The navigation looks standard — let me check if there's a Pricing link..."
  }
}

session_completed

{
  "type": "session_completed",
  "timestamp": "2025-01-15T10:03:18Z",
  "data": {
    "session_id": "sess_xyz789",
    "outcome": "success",
    "duration_seconds": 44,
    "scores": {
      "task_completion": 1.0,
      "cta_findability": 0.87,
      "overall": 0.91
    },
    "summary": "User found the pricing page quickly via the nav link and completed sign-up without friction."
  }
}

test_progress

{
  "type": "test_progress",
  "timestamp": "2025-01-15T10:05:00Z",
  "data": {
    "sessions_completed": 42,
    "sessions_total": 200,
    "success_rate": 0.81,
    "estimated_completion": "2025-01-15T10:22:00Z"
  }
}

test_completed

{
  "type": "test_completed",
  "timestamp": "2025-01-15T10:25:12Z",
  "data": {
    "test_id": "test_abc123",
    "sessions_completed": 200,
    "aggregate_scores": {
      "task_completion": 0.84,
      "cta_findability": 0.79,
      "overall": 0.82
    },
    "report_url": "https://app.simutest.dev/reports/rpt_def456"
  }
}

Connection Handling

For production use, implement reconnection logic and heartbeat handling:

Reconnection

If the connection drops, reconnect with exponential backoff starting at 1 second, capped at 30 seconds. The stream replays the last 60 seconds of events on reconnect, so you won't miss events from a brief disconnect.

Heartbeat

The server sends a WebSocket ping every 30 seconds. Respond with a pong to keep the connection alive. Connections with no pong response for 60 seconds are closed with code 1001.

Error codes

4401 — unauthorized; 4404 — test not found; 4410 — test already completed (connect to the REST API instead).

const MAX_RETRIES = 5;
let retries = 0;

function connect(testId: string, token: string) {
  const ws = new WebSocket(`wss://stream.simutest.dev/tests/${testId}`, {
    headers: { Authorization: `Bearer ${token}` },
  });

  ws.on('open', () => {
    retries = 0;
    console.log('Connected');
  });

  ws.on('close', (code) => {
    if (code !== 1000 && retries < MAX_RETRIES) {
      const delay = Math.min(1000 * 2 ** retries, 30000);
      retries++;
      setTimeout(() => connect(testId, token), delay);
    }
  });

  ws.on('ping', () => ws.pong());

  return ws;
}

JavaScript Example

A complete example handling all event types:

const ws = new WebSocket('wss://stream.simutest.dev/tests/test_abc123', {
  headers: { 'Authorization': 'Bearer st_live_abc123...' }
});

ws.on('message', (data) => {
  const event = JSON.parse(data);
  switch (event.type) {
    case 'session_started':
      // { session_id, persona_name }
      break;
    case 'session_completed':
      // { session_id, scores, summary, duration }
      break;
    case 'test_completed':
      // { aggregate_scores, report_url }
      break;
  }
});