Subscribe to events over WebSocket
A read-only WebSocket on the events stream address: handshake auth, frame format, close codes, limits and backfill.
Besides pulling session events with Stream events (SSE), you can receive them over a WebSocket on the same address: one frame per event, with exactly the same event data as SSE. This is an access mode the platform offers in addition to the official SSE, suited to a server that keeps a long-lived connection and forwards events into your own system.
wss://api.deepraxis.com/agent/v1/sessions/{session_id}/events/streamThe connection is read-only: it only pushes events and accepts no messages. Sending messages and interrupting still go through Send events.
Handshake and authentication
The handshake carries the same auth headers as the HTTP endpoints: Authorization: Bearer <API Key> or x-api-key: <API Key>.
A browser's native WebSocket cannot set request headers, so this is a server-side surface. In a browser, use SSE, or let your server open the connection and relay it to the front end.
When the handshake fails, no connection is opened and the server answers with an HTTP error (body format in Errors):
| Case | Response |
|---|---|
| No credential, or the key is invalid or disabled | 401 |
| The session does not exist, or is not in this key's workspace | 404 |
| In-flight requests exceed the key's concurrency limit | 429, see Rate limits |
An open WebSocket, like an in-progress SSE, holds one of the key's concurrency slots until it closes.
Frames
Every frame is one JSON text. After connecting, the first frame is a ready notice:
{"type":"session.events.ready"}After that, each event produced in the session arrives as one frame, and its event field is the SSE event data:
{"type":"session.event","event":{"id":"d4dc3d96-…","type":"agent.message","processed_at":"2026-09-26T13:35:04.906Z","content":[{"type":"text","text":"OK"}]}}| Field | Meaning |
|---|---|
type | session.events.ready (ready, sent once) or session.event (one event) |
event | Only on session.event frames; same shape as a single event from List events |
The same event can arrive more than once. A user.message, for example, first appears with processed_at: null (accepted, pending) and again with a timestamp once processed. Deduplicate by event.id and keep the latest.
Close codes
| Code | Meaning | What to do |
|---|---|---|
1000 | The server ended the subscription normally | Reconnect if you still need events |
1008 | The client sent a message on the connection | The connection is read-only; send no frames |
1011 | Internal server interruption | Reconnect and backfill with List events |
1013 | A limit was exceeded for now (see below) | Reconnect and backfill with List events |
Limits
- One connection pushes at most about 512 KB of events in total, then closes with
1013. Be ready to reconnect on long sessions. - The server retries its internal connection to the session automatically; if repeated retries fail, it also closes with
1013.
Disconnects and backfill
Events produced while you are disconnected are not replayed. After reconnecting, fill the gap with List events by time:
GET /agent/v1/sessions/{session_id}/events?order=asc&created_at[gt]=<processed_at of the last event you received>Backfilled events and newly pushed ones can overlap; deduplicate by event.id.
Examples
Bun (the built-in WebSocket supports headers):
const ws = new WebSocket(
`wss://api.deepraxis.com/agent/v1/sessions/${sessionId}/events/stream`,
{ headers: { Authorization: `Bearer ${process.env.PRAXIS_API_KEY}` } },
);
ws.addEventListener("message", (message) => {
const frame = JSON.parse(String(message.data));
if (frame.type === "session.events.ready") console.log("ready");
if (frame.type === "session.event") console.log(frame.event.type, frame.event);
});
ws.addEventListener("close", (close) => console.log("closed", close.code));Node (the ws package):
import WebSocket from "ws";
const ws = new WebSocket(
`wss://api.deepraxis.com/agent/v1/sessions/${sessionId}/events/stream`,
{ headers: { Authorization: `Bearer ${process.env.PRAXIS_API_KEY}` } },
);
ws.on("message", (data) => {
const frame = JSON.parse(String(data));
if (frame.type === "session.event") console.log(frame.event.type, frame.event);
});
ws.on("close", (code) => console.log("closed", code));Python (the websockets package):
import asyncio, json, os
import websockets
async def main(session_id: str):
url = f"wss://api.deepraxis.com/agent/v1/sessions/{session_id}/events/stream"
headers = {"Authorization": f"Bearer {os.environ['PRAXIS_API_KEY']}"}
async with websockets.connect(url, additional_headers=headers) as ws:
async for message in ws:
frame = json.loads(message)
if frame["type"] == "session.event":
print(frame["event"]["type"], frame["event"])
asyncio.run(main("sesn_123"))