WebSocket and recovery
Connect to:
wss://trading-api.testnet.novrinex.com/v1/wsThe first server message is a challenge:
{ "version": 1, "type": "challenge", "challenge": "V6zL...", "expires_at_ms": 1789038858366}Public market updates
Section titled “Public market updates”Public channels do not require authentication.
{ "op": "subscribe", "request_id": "sub-market-1", "channels": ["market:BTC-PERP:ticker"]}The server confirms the subscription, then sends a snapshot:
{ "version": 1, "channel": "market:BTC-PERP:ticker", "type": "ticker.snapshot", "occurred_at": "2026-09-10T11:33:27.843553Z", "data": { "market_id": "BTC-PERP", "mark_price": "68240.50", "index_price": "68237.10", "last_price": "68241.00", "data_status": "live" }}Private updates
Section titled “Private updates”Private channels require authentication with the connection challenge. The TypeScript client can create the authentication frame:
import { readFile } from "node:fs/promises";import { NovrinexClient } from "@novrinex/trading-api";
const keyFile = JSON.parse(await readFile("novrinex-key.json", "utf8"));const client = NovrinexClient.fromKeyFile(keyFile);const socket = new WebSocket("wss://trading-api.testnet.novrinex.com/v1/ws");
let lastSequence = 0;
socket.addEventListener("message", async ({ data }) => { const message = JSON.parse(String(data));
if (message.type === "challenge") { const authentication = await client.websocketAuthentication( message.challenge, ["account:read", "orders:read", "fills:read"], lastSequence, ); socket.send(JSON.stringify({ ...authentication, request_id: "auth-1", })); return; }
if (message.type === "authenticated") { socket.send(JSON.stringify({ op: "subscribe", request_id: "sub-private-1", channels: ["account", "orders", "positions", "fills"], })); return; }
if (typeof message.sequence === "number") { lastSequence = Math.max(lastSequence, message.sequence); }
console.log(message);});Private channels and required scopes:
| Channel | Scope |
|---|---|
account |
account:read |
positions |
account:read |
risk |
account:read |
orders |
orders:read |
fills |
fills:read |
Example order event:
{ "version": 1, "channel": "orders", "sequence": 4183, "event_id": "9323e925-24cf-4e34-aee1-5e953e182e8d", "type": "authoritative_status", "occurred_at": "2026-09-10T11:33:28.120000Z", "data": { "entity_type": "execution_intent", "entity_id": "6de51f30-7687-47d3-88b3-20e4a5a9cb34", "from_state": "SUBMITTED", "to_state": "OPEN" }}Other operations
Section titled “Other operations”Unsubscribe:
{ "op": "unsubscribe", "request_id": "unsub-1", "channels": ["fills"]}Check the connection:
{ "op": "ping", "request_id": "ping-1"}The server replies with pong and sends periodic heartbeat frames.
Recover missed events
Section titled “Recover missed events”Store the highest private sequence processed by the client. After reconnecting, or when a sequence is missing:
- Call
GET /v1/events?after_sequence=<last_sequence>. - Continue until
has_moreisfalse. - Refresh the affected REST resource.
- Authenticate the new socket with the recovered
after_sequence.
page = await client.events(after_sequence=last_sequence, limit=200)
for event in page["data"]: handle(event) last_sequence = event["sequence"]