Skip to main content

Periscope WebSocket API

Real-time browser context streaming via channel subscriptions

The WebSocket API enables agents to receive browser context updates in real time as users browse. Instead of polling the REST API, agents subscribe to channels and receive push notifications whenever context changes.

For conceptual background on the streaming architecture, see Real-Time Streaming. For a higher-level client that handles connection management automatically, see the SDKs.

Connection

EnvironmentURL
Developmentws://localhost:4501
Productionwss://periscope.uselovelace.com/ws

Connection Lifecycle

Every WebSocket session follows this sequence:

Loading diagram…

The server checks for heartbeats every 30 seconds. If a client hasn't sent a heartbeat within 60 seconds, the server terminates the connection. See Heartbeat for details.

Message Envelope

All messages are JSON objects with a type field. Client-to-server messages may include an optional id for request/response correlation. Server responses include success: boolean.

json
// Client → Server
{ "type": "auth", "id": "msg_1", "data": { "token": "...", "userId": "..." } }

// Server → Client
{ "type": "auth_response", "id": "msg_1", "success": true, "data": { ... } }
FieldTypeDescription
typestringMessage type identifier (see sections below)
idstring?Optional correlation ID for request/response matching
successbooleanPresent on server responses — whether the operation succeeded
dataobject?Message-specific payload
errorstring?Error description when success is false

Authentication

Authentication is required before subscribing to any channels. Send an auth message within 10 seconds of connecting, or the server will close the connection.

Client → Server:

json
{
  "type": "auth",
  "id": "msg_1",
  "data": {
    "token": "YOUR_API_KEY",
    "userId": "user_456"
  }
}

Server → Client (success):

json
{
  "type": "auth_response",
  "id": "msg_1",
  "success": true,
  "data": {
    "connectionId": "550e8400-e29b-41d4-a716-446655440000"
  }
}

Server → Client (failure):

json
{
  "type": "auth_response",
  "id": "msg_1",
  "success": false,
  "error": "Invalid token or userId"
}

Both token and userId must be non-empty strings. The token is validated against the same API keys used for the REST API.

Channels

Subscribe to channels to receive specific types of context updates. Each channel delivers different events at different frequencies.

user-contexts

All context changes for the authenticated user. This is the most comprehensive channel — it fires whenever any context is created, updated, or deleted, regardless of type.

Fires whenTypical frequency
Any context is synced, updated, or deleted1-5 events/minute during active browsing

Best for: Building agents that need complete awareness of all browsing activity.

current-context

Updates to the user's current active context only. Fires when the user navigates to a new page or switches to a different tab. This is the most commonly used channel for agent integrations.

Fires whenTypical frequency
User navigates to a new page or switches tabsEvery page navigation

Best for: Agents that need to know what the user is currently looking at without tracking full history.

tab-events

Tab lifecycle events. Fires when tabs are opened, closed, activated, or updated. Useful for understanding multitasking patterns and tracking focus.

Fires whenTypical frequency
Tab opened, closed, activated, or title/URL updatedSeveral events/minute during multitasking

Event payload includes:

json
{
  "type": "event",
  "success": true,
  "data": {
    "type": "tab_activated",
    "userId": "user_456",
    "tabId": "tab_123",
    "url": "https://docs.example.com",
    "title": "Example Docs",
    "timestamp": "2026-03-02T12:00:00Z"
  }
}

global-events

System-wide events. Fires for service maintenance notifications, configuration changes, and broadcast messages. Subscribe to this channel to handle service disruptions gracefully.

Fires whenTypical frequency
Maintenance windows, config updates, system broadcastsRare (hours to days between events)

Subscribe / Unsubscribe

Subscribe

Requires authentication. Subscribing to an already-subscribed channel is a no-op (returns success).

Client → Server:

json
{
  "type": "subscribe",
  "id": "msg_2",
  "data": { "channel": "current-context" }
}

Server → Client (success):

json
{
  "type": "subscribe_response",
  "id": "msg_2",
  "success": true,
  "data": { "channel": "current-context" }
}

Server → Client (failure — not authenticated):

json
{
  "type": "subscribe_response",
  "id": "msg_2",
  "success": false,
  "error": "Authentication required before subscribing"
}

Unsubscribe

Client → Server:

json
{
  "type": "unsubscribe",
  "id": "msg_3",
  "data": { "channel": "current-context" }
}

Server → Client:

json
{
  "type": "unsubscribe_response",
  "id": "msg_3",
  "success": true,
  "data": { "channel": "current-context" }
}

Heartbeat

The server checks connections every 30 seconds and terminates any connection that has not sent a heartbeat within 60 seconds. Clients should send heartbeats every 25–30 seconds.

Client → Server:

json
{
  "type": "heartbeat",
  "id": "msg_4"
}

Server → Client:

json
{
  "type": "heartbeat_response",
  "id": "msg_4",
  "success": true,
  "data": { "timestamp": "2026-03-02T12:00:30.000Z" }
}

The SDK handles heartbeats automatically. If you're using raw WebSocket, set up a 25-second interval:

typescript
const heartbeatInterval = setInterval(() => {
  ws.send(JSON.stringify({ type: "heartbeat" }));
}, 25_000);

ws.onclose = () => clearInterval(heartbeatInterval);

Event Messages

Events are pushed to subscribed clients when context changes occur. The data field contains the event payload — its structure depends on the channel and event type.

json
{
  "type": "event",
  "success": true,
  "data": {
    "type": "context_updated",
    "userId": "user_456",
    "timestamp": "2026-03-02T12:31:00.000Z",
    "payload": {
      "contextId": "ctx_abc123",
      "url": "https://docs.example.com/new-page",
      "title": "New Page Title",
      "type": "page",
      "privacyLevel": "public"
    }
  }
}

For the full list of event payload types, see Event Types Reference.

Connection Management

Multiple connections per user: A single user can have multiple active WebSocket connections (e.g., agent running on a server + another in a CLI tool). Each connection has a unique connectionId. Events are broadcast to all connections for that user.

Connection cleanup: When a connection closes (gracefully or due to error), the server automatically removes it from all channel subscriptions and user tracking. No explicit cleanup is required.

Reconnection Strategy

The server does not persist connection state. On reconnection, you must re-authenticate and re-subscribe to all channels.

Recommended approach:

  1. Establish a new WebSocket connection
  2. Re-authenticate with auth message
  3. Re-subscribe to previously subscribed channels (track them client-side)
  4. Resume consuming events

Exponential backoff:

AttemptWait time
11 second
22 seconds
34 seconds
48 seconds
5+30 seconds (cap)

Add random jitter (±500ms) to prevent thundering herd when many clients reconnect simultaneously.

The TypeScript SDK handles reconnection, re-authentication, and re-subscription automatically.

Error Messages

Sent for unknown message types, malformed JSON, or protocol violations.

json
{
  "type": "error",
  "success": false,
  "error": "Unknown message type: foo"
}
ScenarioServer behavior
Invalid JSONReturns error message, increments error count
Unknown message typeReturns error message with the type name
Subscribe without authReturns subscribe_response with success: false
Invalid channel nameReturns subscribe_response with success: false
Connection timeout (60s)Server terminates connection
Send failureLogged server-side, connection cleaned up

Complete Example

Full connection lifecycle using raw WebSocket:

typescript
const ws = new WebSocket("wss://periscope.uselovelace.com/ws");

// Track subscriptions for reconnection
const channels = ["current-context", "tab-events"];

ws.onopen = () => {
  // Step 1: Authenticate
  ws.send(
    JSON.stringify({
      type: "auth",
      id: "auth_1",
      data: { token: "YOUR_API_KEY", userId: "user_456" },
    }),
  );
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);

  switch (msg.type) {
    case "welcome":
      console.log("Connected:", msg.data.connectionId);
      break;

    case "auth_response":
      if (msg.success) {
        // Step 2: Subscribe to channels
        for (const channel of channels) {
          ws.send(
            JSON.stringify({
              type: "subscribe",
              data: { channel },
            }),
          );
        }
      } else {
        console.error("Auth failed:", msg.error);
      }
      break;

    case "heartbeat_response":
      // Heartbeat acknowledged
      break;

    case "event":
      // Step 3: Handle events
      console.log(`[${msg.data.type}]`, msg.data.payload);
      break;

    case "error":
      console.error("Server error:", msg.error);
      break;
  }
};

// Step 4: Send heartbeats
const heartbeat = setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: "heartbeat" }));
  }
}, 25_000);

ws.onclose = () => {
  clearInterval(heartbeat);
  // Implement reconnection with exponential backoff
};

For a simpler approach, use the TypeScript SDK which handles all of this automatically.

Next Steps