Connect & authenticate

Open the WebSocket, send an auth frame, and subscribe to channels.

Endpoints

Open a WebSocket to /ws on the same host as the REST API:

wss://api.drazill.com/ws # production (live)
wss://staging.drazill.com/ws # staging (paper) — use this with a drzl_test_ key

Authenticate

Authentication is the first message you send, not a query parameter or header. The socket accepts either a first-party JWT or an API key:

{ "type": "auth", "token": "<jwt-access-token>" }
{ "type": "auth", "api_key": "<your-api-key>" }

An API key used on the WebSocket must carry the websocket scope. Both key classes authenticate the feed and read the same real market data, but the key class does govern what you can subscribe to: a drzl_test_ key is refused the private user:{your_id} stream, because that channel carries live-money activity. See Test keys on the WebSocket below. Use a drzl_test_ key against staging for the Playground.

You have a few seconds to send the auth frame after the socket opens. An anonymous connection (no auth frame, or an auth frame with no credentials) is allowed — it can subscribe to the public market-data channels but not to your private stream.

On success the server sends a connected frame; if you authenticated with a JWT or a drzl_live_ key, your private user:{your_id} channel is auto-subscribed:

{ "type": "connected", "session": "…", "authenticated": true, "channels": ["user:018f…"] }

Test keys on the WebSocket

Live and test credentials are separated on the realtime surface the same way they are on REST. With a drzl_test_ key:

  • Public market-data channels work unchanged — prices:, market:, category:, and (subject to your feed tier) orderbook: and trades:. The sandbox reads the same real market data as production.
  • user:{your_id} is refused with the error reason TEST_KEY_LIVE_CHANNEL in the subscribed frame’s errors map. That channel carries real fills, real cancels and real wallet movements; a sandbox credential does not observe live money.
  • There is no auto-subscribe on connect, so the connected frame carries an empty channels array. That is the sandbox boundary, not a failed handshake.
{
"type": "subscribed",
"channels": ["prices:018f…"],
"rejected": ["user:018f…"],
"errors": { "user:018f…": "TEST_KEY_LIVE_CHANNEL" }
}

If you re-authenticate an existing live-key session with a test key, any user: channel that connection already held is dropped at that point — the downgrade takes effect immediately rather than at the next subscribe.

Paper (sandbox) order activity is delivered by webhooks with livemode: false, not over the WebSocket. For a paper order lifecycle, wire the webhook; for market data, use this feed with your test key.

JWT sessions and drzl_live_ keys are unaffected by any of the above.

Subscribe

Send a subscribe frame with up to 50 channels. Public channels need no auth; private channels are authorized on subscribe and rejected (never silently dropped) if you may not read them.

{ "type": "subscribe", "channels": ["prices:018f-outcome", "trades:018f-market"] }

The server acknowledges with the accepted channels, any rejects with a reason, and a replay summary per channel:

{
"type": "subscribed",
"channels": ["prices:018f-outcome", "trades:018f-market"],
"rejected": [],
"errors": {},
"replayed": {}
}

A rejected private channel comes back in errors with a structured reason (AUTH_REQUIRED, FORBIDDEN, UNKNOWN_CHANNEL, TIER_NOT_ENTITLED, or TEST_KEY_LIVE_CHANNEL).

A minimal client (Python)

The official Python SDK ships a tested async WebSocket client that defaults to staging and handles the auth frame, ping/pong, and reconnects for you (from sdks/python/drazill/_websocket.py, exercised by sdks/python/tests/test_websocket.py):

import asyncio
from drazill import DrazillWebSocket
async def main():
# Defaults to wss://staging.drazill.com/ws — pass url=... for production.
ws = DrazillWebSocket(api_key="drzl_test_...") # key must have the websocket scope
await ws.subscribe("prices:018f-outcome")
async for message in ws.listen(): # auto-reconnects; replies to pings
print(message["type"], message.get("data"))
asyncio.run(main())

The client replies to the server’s ping with a pong for you. If you write your own client, you must do the same — a missed pong past two heartbeat intervals closes the socket. See Limits & retention.

Never paste a drzl_live_ key into a browser or client-side app. Use a drzl_test_ key against staging for anything interactive, including the Playground.