WebSocket API — Connection & Trading
GoDark exposes a single multiplexed WebSocket endpoint. Clients authenticate once, establish an HPKE session, then send encrypted trading frames and/or subscribe to channels — all on the same socket.
the WebSocket origin/ws/v1
the configured host: the WebSocket origin/ws/v1. the configured host: ``. See Base URLs.
See the Orders Channel and Positions Channel docs for channel subscription detail. Prefer an official SDK for login + HPKE + order encryption.
Message format
Client → Server (JSON control / subscribe):
{ "id": "c-123", "op": "subscribe", "args": { "channel": "orders" } }
| Field | Description |
|---|---|
id | Client-supplied correlation id. The server echoes it on the matching response. |
op | Operation name (see op reference below). |
args | Op-specific payload. |
Server → Client (response to a JSON request):
{ "id": "c-123", "op": "subscribe", "code": 0, "data": { /* ... */ } }
Generic WebSocket request failures use code: 1. When the failure maps to a stable client product error, the response also includes error_code; for example unauthenticated private access uses error_code: 7001.
Server → Client (unsolicited stream event — channel push):
{ "channel": "orders", "type": "update", "seq": 42, "data": { /* ... */ } }
Encrypted trading requests, encrypted pushes, and HPKE setup use binary TradingWsBinaryFrame messages — not clear JSON order.place args. SDKs hide that framing. See Encryption & Transport.
Control Ops
op | Purpose |
|---|---|
login | Authenticate the socket |
logout | Cleanly end the session |
ping / pong | Heartbeat |
subscribe / unsubscribe | Manage channel subscriptions |
op: login
Login is required before encrypted trading or private-channel subscriptions. It need not be the first frame when the client only subscribes to public channels (volume, open_interest, or funding_rate).
Official SDKs mint POST /api/v1/auth/token (client_credentials) and send the returned access_token — not id:secret:passphrase. A legacy opaque api_key (for example a custom host test-key-*) may still be sent as-is:
{
"id": "c-1",
"op": "login",
"args": {
"token": "<access_token>"
}
}
Response:
{
"id": "c-1",
"op": "login",
"code": 0,
"data": {
"account_id": "acct_<32-lowercase-hex-uuid>",
"session_id": "sess_<32-lowercase-hex-uuid>",
"conn_id": "123456789",
"token_expires_at": "2026-04-20T10:45:00Z",
"cancel_on_disconnect": false
}
}
| Field | Notes |
|---|---|
conn_id | Non-zero u64; binds the HPKE session. |
account_id | acct_ plus the authenticated account UUID as 32 lowercase hex digits (no hyphens). It is an identifier string, not the 32-byte HPKE account field. |
session_id | sess_ plus a fresh session UUID as 32 lowercase hex digits (no hyphens). Re-login as the same account rotates it. |
After login, SDKs bind the authenticated account identity to an HPKE Base setup against the pinned sequencer public key, then encrypt place/cancel/modify.
Cancel-on-Disconnect
Opt in when logging in to have resting orders auto-cancelled if the socket drops:
{ "id": "c-1", "op": "login", "args": { "token": "...", "cancel_on_disconnect": true } }
op: logout
{ "id": "c-9", "op": "logout" }
Server responds with code: 0 and closes the socket.
op: ping / pong
Client pings:
{ "id": "c-hb-1", "op": "ping" }
Server responds:
{ "id": "c-hb-1", "op": "pong", "code": 0, "data": { "server_time_ns": 1839975000123000000 } }
The server also pings the client periodically; clients should reply with pong or the server will close the socket.
op: subscribe / unsubscribe
{ "id": "c-2", "op": "subscribe", "args": { "channel": "orders" } }
{ "id": "c-3", "op": "unsubscribe", "args": { "channel": "orders" } }
Channel reference:
| Channel | Scope | Auth |
|---|---|---|
orders | Authenticated account | Via op: login |
positions | Authenticated account | Via op: login |
volume | Public | Not required |
open_interest | Public | Not required |
funding_rate | Public | Not required |
If a subscribe request mixes public and private channels before login, the public subscriptions may be established but the private portion fails with code: 1, error_code: 7001.
Account Ops
op: account.info is unsupported. Use encrypted POST /api/v1/account (get_account → account_margin_update).
Trading Ops (logical)
Logical place / cancel / modify fields match the REST schemas (Place Order, etc.). On the wire they are HPKE-sealed inner protobuf messages (PlaceOrderInput, …) selected by clear request_type — not an EdgeSequencerRequest envelope. Use GodarkClient (or equivalent) rather than inventing binary frames.
Public time-in-force strings: GTC, IOC, FOK, GTD. Optional slippage_bps on market / stop-market.
Sequence Numbers and Gap Recovery
Every channel emits a monotonically increasing seq per subscription per session. Clients should track the last seq they processed and, on any gap, unsubscribe + resubscribe to receive a fresh snapshot. Full protocol detail lives on each channel's page.
SDK example (JavaScript)
import { Environment, GodarkClient } from '@godark/sdk';
const client = new GodarkClient({
apiKeyId: process.env.GODARK_API_KEY_ID,
apiSecret: process.env.GODARK_API_SECRET,
passphrase: process.env.GODARK_PASSPHRASE,
environment: Environment.the configured host,
});
await client.connect(); // login + HPKE
await client.subscribe(['orders']);
const ack = await client.placeOrder({
symbol: 'BTC-USDC-PERP',
side: 'BUY',
orderType: 'LIMIT',
price: 95000,
quantity: 0.1,
timeInForce: 'GTC',
});
console.log(ack.orderId);
await client.disconnect();
REST vs WebSocket
| Aspect | REST | WebSocket |
|---|---|---|
| Latency | Higher (TLS + HTTP per request) | Lower (persistent multiplexed socket) |
| Auth | Bearer from POST /auth/token | SDK mints access_token via POST /auth/token, then login with that JWT |
| Order bodies | One-shot HPKE per request | Sealed HPKE session (conn_id) |
| Streaming | On-demand encrypted POST snapshots | Authoritative snapshots + live encrypted updates |
| Best for | Scripted one-offs via GodarkRestClient | Long-running trading clients |
Rate limits are shared with REST (see Rate Limits).
Reconnect and reconciliation
Reconnect establishes a new login and HPKE session; there is no public replay or resume cursor. After reconnect:
- fail or resolve any locally pending command promises according to the SDK;
- resubscribe to private channels;
- replace local orders and positions with the fresh authoritative snapshots;
- only then apply new live updates.
Do not retry a timed-out place blindly. Reconcile the open-orders snapshot and the original correlation/order identifiers first. Sequence gaps are recovered by resubscribing for a fresh snapshot, not by requesting historical replay.