Skip to main content

WebSocket Protocol

Both multiplexed endpoints speak the same JSON protocol over one TLS connection: a request envelope from the client, a frame envelope from the server, and a per-channel sequence you must verify.

Connection

EndpointPurpose
wss://api.sentico-labs.xyz/api/v1/ws/publicPublic market channels
wss://api.sentico-labs.xyz/api/v1/ws/private/{account}Private account channels; /api/v1/ws/mm/{account} and /api/v1/ws/bsl/{account} are aliases

Inbound messages are capped at 64 KiB. Private connections must authenticate within authDeadlineMs from the session frame or they are closed.

Session frame

After the upgrade the server sends the schema announcement and then the session frame. Everything a client needs to size itself is in it:

{
"event": "session",
"stream": "senticore_private",
"streamVersion": 1,
"schemaId": "senticore_private_ws_json",
"schemaVersion": 1,
"schemaEncoding": "json",
"cursor": null,
"resumeFromSeq": null,
"rotateByMs": 1786259008273,
"heartbeatMs": 20000,
"idleTimeoutMs": 90000,
"authDeadlineMs": 5000,
"maxIdleBeforeSubscribeMs": 30000,
"controlMessageWindowMs": 60000,
"maxControlMessagesPerWindow": 60,
"maxSubscriptionsPerConnection": 32,
"publicMaxMarketsPerConnection": 16,
"reconnectPolicy": { "supportsStreamResume": true, "supportsCursorResume": true, "baseBackoffMs": 250, "maxBackoffMs": 5000 },
"signals": { "mempoolLen": 0, "commitQueueDepth": 0 }
}

The numbers above are examples; the deployed values are whatever the frame says. stream is senticore_public or senticore_private.

Request envelope

Every client message is a JSON object with a method. id is optional and is echoed on the acknowledgement so you can correlate requests.

{ "id": "sub-1", "method": "subscribe", "subscription": { "type": "l2Book", "marketId": 3, "depth": 20 } }
methodBodyReply
authauthorization: "Bearer spws1…" (or token)ack on channel auth
subscribesubscription object (below)ack on channel subscription, then replay/snapshot, then snapshot_end
unsubscribesame subscription objectack on channel subscription with "method": "unsubscribe"
pingack on channel pong with data.ts
resendchannel, fromSeq, toSeq (private, at most 10,000 frames)ack on channel resend with deliveredThroughSeq, then the replayed frames

A shorthand without the nested object is accepted: {"method": "subscribe", "channel": "l2Book", "marketId": 3}; the gateway lifts marketId, depth, limit, cursor, fromSeq and account into the subscription.

Subscription object

FieldPublic channelsPrivate channels
typechannel name (marketSnapshot, l2Book, trades, bbo, activeAssetCtx); snake_case and the aliases snapshot, book, assetCtx are acceptedchannel name (account, userEvents, orderUpdates, userFills, dropCopy); fills is an alias for userFills
marketIdnumeric market idnot used
depth, limitbook depth / trade limit for the initial snapshotnot used
fromSeq (aliases cursor, resumeFrom)resume the market stream after this sequenceresume the account stream after this sequence
accountnot usedoptional; must equal the account in the URL (error 1004 otherwise)

Subscribing to a private channel on the public endpoint, or a public channel on the private endpoint, is rejected with a message pointing at the right endpoint. Public subscriptions are capped by maxSubscriptionsPerConnection and publicMaxMarketsPerConnection; control messages are rate-limited per controlMessageWindowMs / maxControlMessagesPerWindow, and exceeding that closes the socket with code 4409.

Frame envelope

Every server frame after the session frame has this shape:

{
"type": "event",
"channel": "l2Book",
"seq": 184273,
"prev_seq": 184272,
"ts": 1788558400818,
"data": { "marketId": 3, "seq": 184273, "prevSeq": 184272, "stateVersion": 545718, "topOfBookChecksum": "…", "book": { "spot": { "bids": [["2513937400", "40"]], "asks": [] } } },
"id": null
}
FieldMeaning
typeevent for data; ack, error, warning, snapshot_end, resume_required, gap_fill, rotate for control
channelChannel name; control frames use auth, subscription, pong, resend, error, warning
seq / prev_seqMonotonic per channel scope; null on control frames
tsServer time in unix milliseconds
dataChannel payload
idThe request id this frame answers, when applicable

Acknowledgements

Subscription ack, with the replay plan the server chose:

{ "type": "ack", "channel": "subscription", "seq": null, "prev_seq": null, "ts": 1788558400818,
"data": { "method": "subscribe", "subscription": { "type": "orderUpdates", "account": "0x…", "fromSeq": 4810 },
"channel": "orderUpdates", "fromSeq": 4810, "snapshotSeq": 4812, "liveFromSeq": 4813 }, "id": "sub-1" }

Frames with seq in (fromSeq, snapshotSeq] are replayed history; a snapshot_end frame on that channel marks the switch to live delivery.

Auth ack:

{ "type": "ack", "channel": "auth", "data": { "ok": true, "account": "0x…", "scope": "read", "tokenType": "ws_session_token", "agentId": "agt_…", "credentialId": "…" }, "id": "auth-1" }

A failed auth returns ok: false with status (401/403), an error text and code AuthFailed (105); subscribing before a successful auth returns error 1001.

Sequence handling

  • Verify seq == prev_seq + 1 per channel scope (market for public, account for private).
  • On a gap, stop applying, fetch a REST snapshot (public) or gap-fill from /api/v1/bsl/accounts/{account}/executions?cursor=… and account reads (private), then resubscribe with fromSeq.
  • resume_required or gap_fill means the requested cursor is outside the retention window: data carries requestedFromSeq, oldestAvailableSeq, newestAvailableSeq and replayRetentionMs. Rebuild from REST and resume from oldestAvailableSeq or later.
  • A rotate frame ("event": "rotate") precedes a planned disconnect. Its payload includes the resubscribe journal (subscriptions[] with cursors); reconnect and replay it.

Heartbeat and liveness

The server emits heartbeats every heartbeatMs and closes idle connections after idleTimeoutMs. A client may send {"method": "ping"} at any time and receives {"type": "ack", "channel": "pong", "data": {"ts": …}}. A slow consumer first receives a warning frame (data.reason = "slow_consumer") and is then closed with 4408.

Errors and close codes

Errors are frames of type error:

{ "type": "error", "channel": "error", "data": { "message": "subscription.type is required", "code": 1004 }, "id": "sub-1" }
data.codeMeaning
1000Malformed message
1001Authentication required
1002Permission denied (credential lacks the channel's scope)
1003Unsupported operation
1004Invalid subscription
1005Replay gap
1006Invalid resend range
1007Unsupported channel
103 / 104 / 105Reused order-entry reject codes: trading halted, ledger degraded, auth failed
Close codeMeaning
4401Auth required and not provided before the deadline
4408Policy violation (slow consumer, protocol misuse)
4409Limit exceeded (subscriptions, markets, control-message rate)

Resilience

See Reconnect Strategy for production-grade client behavior and the SDK helpers that implement this protocol.