Skip to content

WebSocket and recovery

Connect to:

wss://trading-api.testnet.novrinex.com/v1/ws

The first server message is a challenge:

{
"version": 1,
"type": "challenge",
"challenge": "V6zL...",
"expires_at_ms": 1789038858366
}

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 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"
}
}

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.

Store the highest private sequence processed by the client. After reconnecting, or when a sequence is missing:

  1. Call GET /v1/events?after_sequence=<last_sequence>.
  2. Continue until has_more is false.
  3. Refresh the affected REST resource.
  4. 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"]