Appearance
Live socket
One WebSocket per terminal carries everything live: prices, account changes, CRM notifications and the dealer queue. The gateway sends frames; you listen to them by type. You send a few small messages, mainly watch to choose which prices you want.
How it works
- Sign in over REST (
staff.auth.login/trader.auth.login). - Connect. The gateway answers with a
welcomeframe. - Watch symbols for prices. Their latest prices arrive at once, then every change.
- Listen to the events you care about: account
trade,money, CRMclient_created, and so on. Which frames you get depends on your session, rights and group scope. - After a reconnect, reload over REST. The SDK reconnects by itself and re-sends your watch list, but frames sent while you were offline are not replayed.
Limits
20 messages per second and 4096 bytes per message from you · 1000 watched symbols per socket (fewer if the group sets a limit) · 8 sockets per login.
ts
import { trader } from "@/lib/noviqa"
const socket = trader.socket()
socket.on("welcome", (f) => console.log("signed in as", f.login))
socket.on("prices", (prices) => renderQuotes(prices))
socket.on("trade", (e) => applyTrade(e.data))
socket.on("money", (e) => renderMoney(e.data))
socket.on("open", ({ reconnect }) => reconnect && reloadAccount())
await socket.connect()
socket.watch([1, 2, 7])ts
import { staff } from "@/lib/noviqa"
const socket = staff.socket()
socket.on("client_created", (e) => leadQueue.add(e.data))
socket.on("balance", (e) => toast(`Deposit on ${e.login}`))
socket.on("trade", (e) => dealerBoard.apply(e.login, e.data))
socket.on("close", ({ reason }) => reason === "access changed" && reloadRights())
await socket.connect()Connect
Sign in over REST first: the socket uses the same session. Then open the socket for your terminal:
Where to connect
wss://<gateway>/admin/v1/wsbrowser · staffAdmin and manager web. The session cookie signs it; the page must be served from an allowed origin.
wss://<gateway>/trader/v1/wsbrowser · traderTrader web. The session cookie signs it.
wss://<gateway>/wsapps · serversDesktop, mobile, API clients and backend services. Send the access token in the
Authorization: Bearer header, or, where headers are impossible, as the subprotocol pair ["bearer", token].When it closes
session endedclose 1008Signed out, kicked or signed in elsewhere. Do not reconnect: sign in again. The SDK stops here.
session expiredclose 1008The access token ran out. Refresh it, then reconnect. The SDK does both.
access changedclose 1008Rights or group scope changed. Reconnect at once and reload what the user may see.
too slow to keep upclose 1008The client read too slowly. Reconnect and reload.
too many open connections for this loginclose 1008More than 8 sockets for one login. Close unused tabs.
live stream resync requiredclose 1008The server lost events. Reconnect and reload your lists.
decodePrices is exported by the SDK; without it, read the records as the prices frame below describes.
Any other close (network drop, server restart): wait 1 s, then double up to 30 s with jitter, reconnect, send watch again and reload over REST. The SDK's LiveSocket does all of this; without the SDK you write it yourself.
ts
import { Staff } from "@hybrid-solutions-09/noviqa-sdk"
const staff = new Staff({
baseUrl: "https://api.example.com",
auth: { type: "session" }, // cookies
})
await staff.auth.login({
login: 1000, password, terminal: "admin",
})
// opens wss://api.example.com/admin/v1/ws
const socket = staff.socket()
socket.on("welcome", (f) => {
console.log("connected as", f.login)
})
socket.on("close", ({ reason, willReconnect }) => {
console.log(reason, willReconnect)
})
await socket.connect()
// later
socket.close()ts
import { Trader } from "@hybrid-solutions-09/noviqa-sdk"
// Node < 22 only: Node 22+ and React Native
// have WebSocket built in
import WebSocket from "ws"
const trader = new Trader({
baseUrl: "https://api.example.com",
auth: { type: "jwt" }, // bearer tokens
deviceId: "install-7f3c",
})
await trader.auth.login({ login: 1001, password, terminal: "api" })
// opens wss://api.example.com/ws with the token
const socket = trader.socket({ WebSocket })
socket.on("trade", (e) => console.log(e.data))
await socket.connect()js
// after POST /trader/v1/auth/login
// with credentials: "include"
const ws = new WebSocket("wss://api.example.com/trader/v1/ws")
ws.binaryType = "arraybuffer"
ws.onmessage = (e) => {
// binary = prices: 48-byte records
if (typeof e.data !== "string") return decodePrices(e.data)
const frame = JSON.parse(e.data)
if (frame.type === "welcome") {
ws.send(JSON.stringify({ type: "watch", symbol_ids: [1, 2, 7] }))
}
if (frame.type === "trade") applyTrade(frame.data)
}
// reconnect with backoff yourself
ws.onclose = (e) => console.log(e.code, e.reason)js
import WebSocket from "ws"
const ws = new WebSocket("wss://api.example.com/ws", {
headers: { Authorization: `Bearer ${accessToken}` },
})
// or, where headers are impossible:
// new WebSocket("wss://api.example.com/ws", ["bearer", accessToken])
ws.on("message", (data, isBinary) => {
if (isBinary) return decodePrices(data)
console.log(JSON.parse(data.toString()))
})Connection
error
RECEIVE
socket.on("error")Your last message was refused; code says why (e.g. unknown_message, rate_limited). The socket stays open.
Who receives itEveryone
Fields
codeenumrequiredtoo_many_requests: more than 20 messages in a second (the message is dropped); invalid_message: not JSON or over 4096 bytes (financial_view allows 8192); unknown_message: an unknown type.
too_many_requests invalid_message unknown_message forbidden unavailablets
const socket = trader.socket()
socket.on("error", (frame) => {
console.warn("refused:", frame.code)
})
await socket.connect()json
{
"code": "unknown_message",
"type": "error"
}pong
RECEIVE
socket.on("pong")The answer to your ping. Optional: browsers already answer the gateway's own pings.
Who receives itEveryone
Fields
dataanyServer notes
With gateway_metrics_v1, staff holding config.servers receive optional data {heap_bytes,sampled_at}. Heap bytes describe this gateway's Go heap objects (the same scope as GET /monitor heap_bytes), not platform, host or container memory. A single process sample is shared and refreshed at most once per five seconds when requested; no database, Redis or NATS reads. Staff without this right and traders retain the type-only pong. Send ping after welcome for the initial sample, then use the existing heartbeat. Clear samples on reconnect/session stop, reject older timestamps, and treat absent data as unavailable; an older gateway without the capability may retain bounded HTTP fallback.
ts
const socket = trader.socket()
socket.on("pong", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "pong"
}welcome
RECEIVE
socket.on("welcome")The first frame after the socket opens. It says who the socket is signed in as. Wait for it before you send anything.
Who receives itEveryone
Fields
capabilitiesstring[]staff_live_v1 means the implemented staff lifecycle/value/recovery contract; staff_data_live_v1 enables that contract for both Admin and Manager presentations; journal_live_v1 enables scoped Journal tail/recovery; gateway_metrics_v1 adds rights-checked gateway heap samples to pong; raw_prices_v1 supports independent rights-checked raw quote subscriptions; dealing_log_live_v1 supports post-commit scoped dealing log invalidation; accounts_window_v1 admits additive exact stored Accounts window/count snapshots; absent on older gateways and traders
loginintegerrequiredthe signed-in login Login is the numeric identity of every account, trader or staff.
staffbooleanrequireda staff session
ts
const socket = trader.socket()
socket.on("welcome", (frame) => {
console.log("signed in as", frame.login)
})
await socket.connect()json
{
"login": 1001,
"staff": false,
"type": "welcome"
}Prices
depth
RECEIVE
socket.on("depth")The order book (market depth) of a watched symbol whose depth setting is on.
Who receives itTrader · Staff
Fields
asksdepthLevel[]requiredbidsdepthLevel[]requiredsymbol_idintegerrequiredtimestringrequiredUTC time, RFC 3339.
Server notes
For watched symbols whose book depth setting is on (for the trader's group); staff default to group-resolved prices; raw:true needs prices.raw. Normal mode gets levels shifted by the group's spread markup. Prices are decimal strings with the symbol's digits; the newest book per symbol goes out as soon as it changes, after the binary price frame of the same flush.
ts
const socket = trader.socket()
socket.on("depth", (frame) => {
console.log(frame)
})
await socket.connect()json
{
"asks": [
{
"price": "1.08460",
"volume": "1.5"
}
],
"bids": [
{
"price": "1.08450",
"volume": "2"
}
],
"symbol_id": 1,
"time": "2026-09-25T10:15:00.120Z",
"type": "depth"
}prices
RECEIVE
socket.on("prices")binaryBid and ask of the symbols you watch, the moment they change. Sent as compact binary; the SDK decodes it into Price objects with exact decimal strings.
Who receives itTrader · Staff
Server notes
Binary WebSocket frames, sent as soon as prices change (coalesced while the socket's send queue is backed up), each a run of 48-byte records (the newest price of each watched symbol that changed). All numbers are little-endian.
| offset | size | field |
|---|---|---|
| 0 | 1 | layout version, 1 |
| 1 | 1 | flags: bit 0 = the price jumped (gap), bit 1 = thrown in by staff (manual) |
| 2 | 1 | digits: prices are whole points, price = points / 10^digits |
| 3 | 1 | reserved |
| 4 | 4 | symbol id, uint32 |
| 8 | 8 | time, int64 unix nanoseconds, UTC |
| 16 | 8 | bid in points, int64 |
| 24 | 8 | ask in points, int64 |
| 32 | 8 | last deal price in points, int64 (0 = none) |
| 40 | 4 | datafeed id, uint32 (0 = thrown in by staff) |
| 44 | 4 | reserved |
Decode with a DataView: bid = view.getBigInt64(o+16, true), then format as a decimal string with digits places (never divide in floating point for money). All sockets default to their group's spread markup. Staff must explicitly request raw:true and hold prices.raw for raw prices.
ts
const socket = trader.socket()
socket.on("prices", (prices) => {
for (const p of prices) {
console.log(p.symbolId, p.bid, p.ask) // exact strings: "1.08452"
}
})
await socket.connect()
socket.watch([1, 2, 7])ts
{
symbolId: 1,
time: "2026-09-25T10:15:00.120Z",
bid: "1.08452",
ask: "1.08460",
last: null,
digits: 5,
gap: false,
manual: false,
datafeedId: 2
}raw_prices
RECEIVE
socket.on("raw_prices")Prices straight from the feed, before the group's spread markup. Only after watch_raw.
Who receives itStaff with prices.raw
Fields
dataQuote[]requiredexact decimal strings and UTC timestamps
Server notes
Only for watch_raw subscribers holding prices.raw. Exact decimal strings and UTC timestamps match GET /market/quotes?raw=true. The independent JSON frame permits one symbol to receive raw and group-adjusted prices on the same socket without mixing audiences. No group scope or configuration capability is granted; access withdrawal and symbol archival are rechecked.
ts
const socket = staff.socket()
socket.on("raw_prices", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "raw_prices",
"data": [
{
"ask": "1.08460",
"bid": "1.08452",
"digits": 5,
"last": "1.08452",
"symbol": "EURUSD",
"symbol_id": 1,
"time": "2026-09-25T10:15:00Z"
}
]
}Account events
alert_triggered
RECEIVE
socket.on("alert_triggered")One of the trader's price alerts crossed its level.
Who receives itTrader (own account)
Fields
groupstringThe account's group; staff frames only.
loginintegerrequiredThe account the event is about.
data.atstringrequiredUTC time, RFC 3339.
data.commentstringrequireddata.conditionstringrequireddata.idintegerrequireddata.levelstringrequiredExact decimal as a string. Never parse it into a binary float for money or prices.
data.modestringrequireddata.pricestringrequiredExact decimal as a string. Never parse it into a binary float for money or prices.
data.sidestringrequireddata.symbolstringrequiredServer notes
The trader's alert crossed its level; mode once alerts are switched off afterwards.
ts
const socket = trader.socket()
socket.on("alert_triggered", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "alert_triggered",
"group": "real\\standard",
"login": 1001,
"data": {
"at": "2026-09-25T10:15:00Z",
"comment": "Deposit via bank",
"condition": "string",
"id": 1,
"level": "1.08452",
"mode": "string",
"price": "1.08452",
"side": "string",
"symbol": "EURUSD"
}
}balance
RECEIVE
socket.on("balance")A money operation: deposit, withdrawal, credit, correction or bonus. Carries the balance deal and the money after it.
Who receives itTrader (own account) · Staff (in scope)
Fields
groupstringThe account's group; staff frames only.
loginintegerrequiredThe account the event is about.
data.closedinteger[]position ids that no longer exist
data.command_hashstringdata.dealsDeal[]data.deals_deletedinteger[]data.deals_editedDeal[]Corrections by staff: deals rewritten in place and deals removed from history.
data.groupstringrequireddata.loginintegerrequiredLogin is the numeric identity of every account, trader or staff.
data.margin_checkanyrejected proposal evidence, not applied account state
data.moneyanydata.ordersOrder[]data.orders_deletedinteger[]OrdersDeleted are order ids removed by staff, open (also sent in Orders as cancelled, so terminals drop them) or from the history.
data.placedinteger[]pending order ids this change created (the rest in Orders changed)
data.positionsPosition[]state after the change
data.quietbooleanbookkeeping only (e.g. a provider's order id arrived): nothing to tell the client
Server notes
Deposit, withdrawal, credit, correction or bonus: a balance deal in deals and money after it. Sent to the account's own trader sockets, and to staff whose group scope covers the account's group and who hold trades.view for trade/order/position/balance/day_end/correction, accounts.view for money, or risk.view for margin_call/risk_changed. Money-only readers receive no position payload. Unknown event names are denied; staff frames carry group.
ts
const socket = trader.socket()
socket.on("balance", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "balance",
"group": "real\\standard",
"login": 1001,
"data": {
"closed": [
1
],
"command_hash": "string",
"deals": [
{
"action": "buy",
"calc_type": "forex",
"comment": "Deposit via bank",
"commission": "1.08452",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"entry": "in",
"external_id": "string",
"fee": "1.08452",
"gateway_id": 1,
"id": 1
}
],
"deals_deleted": [
1
],
"deals_edited": [
{
"action": "buy",
"calc_type": "forex",
"comment": "Deposit via bank",
"commission": "1.08452",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"entry": "in",
"external_id": "string",
"fee": "1.08452",
"gateway_id": 1,
"id": 1
}
],
"group": "real\\standard",
"login": 1001,
"margin_check": {
"additional_margin": "1.08452",
"before": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"group": "real\\standard",
"projected": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"shortfall": "1.08452",
"symbol": "EURUSD",
"virtual_credit": "1.08452"
},
"money": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"orders": [
{
"activated_at": "2026-09-25T10:15:00Z",
"by_position_id": 1,
"comment": "Deposit via bank",
"contract_size": "1.08452",
"created_at": "2026-09-25T10:15:00Z",
"dealer": 1,
"deviation_pts": 1,
"digits": 5,
"done_at": "2026-09-25T10:15:00Z",
"expiration": "string",
"expires_at": "2026-09-25T10:15:00Z",
"external_id": "string"
}
],
"orders_deleted": [
1
],
"placed": [
1
],
"positions": [
{
"comment": "Deposit via bank",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"external_id": "string",
"id": 1,
"login": 1001,
"margin": "108.45",
"opened_at": "2026-09-25T10:15:00Z",
"origin": {},
"price_current": "1.08452",
"price_open": "1.08452"
}
],
"quiet": true,
"request_id": "3f2a9c1e-8b7d-4e6f-a5c4-1b2d3e4f5a6b",
"seq": 1,
"time": "2026-09-25T10:15:00Z",
"type": "string"
}
}correction
RECEIVE
socket.on("correction")Staff corrected the account's records (deals edited or removed, positions or balance fixed). Reload the lists you show.
Who receives itTrader (own account) · Staff (in scope)
Fields
groupstringThe account's group; staff frames only.
loginintegerrequiredThe account the event is about.
data.closedinteger[]position ids that no longer exist
data.command_hashstringdata.dealsDeal[]data.deals_deletedinteger[]data.deals_editedDeal[]Corrections by staff: deals rewritten in place and deals removed from history.
data.groupstringrequireddata.loginintegerrequiredLogin is the numeric identity of every account, trader or staff.
data.margin_checkanyrejected proposal evidence, not applied account state
data.moneyanydata.ordersOrder[]data.orders_deletedinteger[]OrdersDeleted are order ids removed by staff, open (also sent in Orders as cancelled, so terminals drop them) or from the history.
data.placedinteger[]pending order ids this change created (the rest in Orders changed)
data.positionsPosition[]state after the change
data.quietbooleanbookkeeping only (e.g. a provider's order id arrived): nothing to tell the client
Server notes
Deals rewritten in place (deals_edited) or removed (deals_deleted), positions, balance or orders fixed. Reload the affected lists. Sent to the account's own trader sockets, and to staff whose group scope covers the account's group and who hold trades.view for trade/order/position/balance/day_end/correction, accounts.view for money, or risk.view for margin_call/risk_changed. Money-only readers receive no position payload. Unknown event names are denied; staff frames carry group.
ts
const socket = trader.socket()
socket.on("correction", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "correction",
"group": "real\\standard",
"login": 1001,
"data": {
"closed": [
1
],
"command_hash": "string",
"deals": [
{
"action": "buy",
"calc_type": "forex",
"comment": "Deposit via bank",
"commission": "1.08452",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"entry": "in",
"external_id": "string",
"fee": "1.08452",
"gateway_id": 1,
"id": 1
}
],
"deals_deleted": [
1
],
"deals_edited": [
{
"action": "buy",
"calc_type": "forex",
"comment": "Deposit via bank",
"commission": "1.08452",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"entry": "in",
"external_id": "string",
"fee": "1.08452",
"gateway_id": 1,
"id": 1
}
],
"group": "real\\standard",
"login": 1001,
"margin_check": {
"additional_margin": "1.08452",
"before": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"group": "real\\standard",
"projected": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"shortfall": "1.08452",
"symbol": "EURUSD",
"virtual_credit": "1.08452"
},
"money": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"orders": [
{
"activated_at": "2026-09-25T10:15:00Z",
"by_position_id": 1,
"comment": "Deposit via bank",
"contract_size": "1.08452",
"created_at": "2026-09-25T10:15:00Z",
"dealer": 1,
"deviation_pts": 1,
"digits": 5,
"done_at": "2026-09-25T10:15:00Z",
"expiration": "string",
"expires_at": "2026-09-25T10:15:00Z",
"external_id": "string"
}
],
"orders_deleted": [
1
],
"placed": [
1
],
"positions": [
{
"comment": "Deposit via bank",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"external_id": "string",
"id": 1,
"login": 1001,
"margin": "108.45",
"opened_at": "2026-09-25T10:15:00Z",
"origin": {},
"price_current": "1.08452",
"price_open": "1.08452"
}
],
"quiet": true,
"request_id": "3f2a9c1e-8b7d-4e6f-a5c4-1b2d3e4f5a6b",
"seq": 1,
"time": "2026-09-25T10:15:00Z",
"type": "string"
}
}day_end
RECEIVE
socket.on("day_end")The end-of-day charges: swaps, period commissions, interest, and the day's money snapshot.
Who receives itTrader (own account) · Staff (in scope)
Fields
groupstringThe account's group; staff frames only.
loginintegerrequiredThe account the event is about.
data.closedinteger[]position ids that no longer exist
data.command_hashstringdata.dealsDeal[]data.deals_deletedinteger[]data.deals_editedDeal[]Corrections by staff: deals rewritten in place and deals removed from history.
data.groupstringrequireddata.loginintegerrequiredLogin is the numeric identity of every account, trader or staff.
data.margin_checkanyrejected proposal evidence, not applied account state
data.moneyanydata.ordersOrder[]data.orders_deletedinteger[]OrdersDeleted are order ids removed by staff, open (also sent in Orders as cancelled, so terminals drop them) or from the history.
data.placedinteger[]pending order ids this change created (the rest in Orders changed)
data.positionsPosition[]state after the change
data.quietbooleanbookkeeping only (e.g. a provider's order id arrived): nothing to tell the client
Server notes
Swaps, period commissions, interest and the day's money snapshot. Sent to the account's own trader sockets, and to staff whose group scope covers the account's group and who hold trades.view for trade/order/position/balance/day_end/correction, accounts.view for money, or risk.view for margin_call/risk_changed. Money-only readers receive no position payload. Unknown event names are denied; staff frames carry group.
ts
const socket = trader.socket()
socket.on("day_end", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "day_end",
"group": "real\\standard",
"login": 1001,
"data": {
"closed": [
1
],
"command_hash": "string",
"deals": [
{
"action": "buy",
"calc_type": "forex",
"comment": "Deposit via bank",
"commission": "1.08452",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"entry": "in",
"external_id": "string",
"fee": "1.08452",
"gateway_id": 1,
"id": 1
}
],
"deals_deleted": [
1
],
"deals_edited": [
{
"action": "buy",
"calc_type": "forex",
"comment": "Deposit via bank",
"commission": "1.08452",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"entry": "in",
"external_id": "string",
"fee": "1.08452",
"gateway_id": 1,
"id": 1
}
],
"group": "real\\standard",
"login": 1001,
"margin_check": {
"additional_margin": "1.08452",
"before": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"group": "real\\standard",
"projected": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"shortfall": "1.08452",
"symbol": "EURUSD",
"virtual_credit": "1.08452"
},
"money": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"orders": [
{
"activated_at": "2026-09-25T10:15:00Z",
"by_position_id": 1,
"comment": "Deposit via bank",
"contract_size": "1.08452",
"created_at": "2026-09-25T10:15:00Z",
"dealer": 1,
"deviation_pts": 1,
"digits": 5,
"done_at": "2026-09-25T10:15:00Z",
"expiration": "string",
"expires_at": "2026-09-25T10:15:00Z",
"external_id": "string"
}
],
"orders_deleted": [
1
],
"placed": [
1
],
"positions": [
{
"comment": "Deposit via bank",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"external_id": "string",
"id": 1,
"login": 1001,
"margin": "108.45",
"opened_at": "2026-09-25T10:15:00Z",
"origin": {},
"price_current": "1.08452",
"price_open": "1.08452"
}
],
"quiet": true,
"request_id": "3f2a9c1e-8b7d-4e6f-a5c4-1b2d3e4f5a6b",
"seq": 1,
"time": "2026-09-25T10:15:00Z",
"type": "string"
}
}margin_call
RECEIVE
socket.on("margin_call")The account's margin level fell to its group's margin call level.
Who receives itTrader (own account) · Staff with risk.view
Fields
groupstringThe account's group; staff frames only.
loginintegerrequiredThe account the event is about.
data.closedinteger[]position ids that no longer exist
data.command_hashstringdata.dealsDeal[]data.deals_deletedinteger[]data.deals_editedDeal[]Corrections by staff: deals rewritten in place and deals removed from history.
data.groupstringrequireddata.loginintegerrequiredLogin is the numeric identity of every account, trader or staff.
data.margin_checkanyrejected proposal evidence, not applied account state
data.moneyanydata.ordersOrder[]data.orders_deletedinteger[]OrdersDeleted are order ids removed by staff, open (also sent in Orders as cancelled, so terminals drop them) or from the history.
data.placedinteger[]pending order ids this change created (the rest in Orders changed)
data.positionsPosition[]state after the change
data.quietbooleanbookkeeping only (e.g. a provider's order id arrived): nothing to tell the client
Server notes
The account's margin level fell to its group's margin call level. money has the levels. Sent to the account's own trader sockets, and to staff whose group scope covers the account's group and who hold trades.view for trade/order/position/balance/day_end/correction, accounts.view for money, or risk.view for margin_call/risk_changed. Money-only readers receive no position payload. Unknown event names are denied; staff frames carry group.
ts
const socket = trader.socket()
socket.on("margin_call", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "margin_call",
"group": "real\\standard",
"login": 1001,
"data": {
"closed": [
1
],
"command_hash": "string",
"deals": [
{
"action": "buy",
"calc_type": "forex",
"comment": "Deposit via bank",
"commission": "1.08452",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"entry": "in",
"external_id": "string",
"fee": "1.08452",
"gateway_id": 1,
"id": 1
}
],
"deals_deleted": [
1
],
"deals_edited": [
{
"action": "buy",
"calc_type": "forex",
"comment": "Deposit via bank",
"commission": "1.08452",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"entry": "in",
"external_id": "string",
"fee": "1.08452",
"gateway_id": 1,
"id": 1
}
],
"group": "real\\standard",
"login": 1001,
"margin_check": {
"additional_margin": "1.08452",
"before": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"group": "real\\standard",
"projected": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"shortfall": "1.08452",
"symbol": "EURUSD",
"virtual_credit": "1.08452"
},
"money": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"orders": [
{
"activated_at": "2026-09-25T10:15:00Z",
"by_position_id": 1,
"comment": "Deposit via bank",
"contract_size": "1.08452",
"created_at": "2026-09-25T10:15:00Z",
"dealer": 1,
"deviation_pts": 1,
"digits": 5,
"done_at": "2026-09-25T10:15:00Z",
"expiration": "string",
"expires_at": "2026-09-25T10:15:00Z",
"external_id": "string"
}
],
"orders_deleted": [
1
],
"placed": [
1
],
"positions": [
{
"comment": "Deposit via bank",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"external_id": "string",
"id": 1,
"login": 1001,
"margin": "108.45",
"opened_at": "2026-09-25T10:15:00Z",
"origin": {},
"price_current": "1.08452",
"price_open": "1.08452"
}
],
"quiet": true,
"request_id": "3f2a9c1e-8b7d-4e6f-a5c4-1b2d3e4f5a6b",
"seq": 1,
"time": "2026-09-25T10:15:00Z",
"type": "string"
}
}money
RECEIVE
socket.on("money")Live balance, equity, margin and floating profit. Sent whenever a value moves (at most every 10 ms) plus once a second.
Who receives itTrader (own account) · Staff (in scope)
Fields
groupstringThe account's group; staff frames only.
loginintegerrequiredThe account the event is about.
data.closedinteger[]position ids that no longer exist
data.command_hashstringdata.dealsDeal[]data.deals_deletedinteger[]data.deals_editedDeal[]Corrections by staff: deals rewritten in place and deals removed from history.
data.groupstringrequireddata.loginintegerrequiredLogin is the numeric identity of every account, trader or staff.
data.margin_checkanyrejected proposal evidence, not applied account state
data.moneyanydata.ordersOrder[]data.orders_deletedinteger[]OrdersDeleted are order ids removed by staff, open (also sent in Orders as cancelled, so terminals drop them) or from the history.
data.placedinteger[]pending order ids this change created (the rest in Orders changed)
data.positionsPosition[]state after the change
data.quietbooleanbookkeeping only (e.g. a provider's order id arrived): nothing to tell the client
Server notes
Not logged: pushed for watched accounts as soon as a value moves (prices move it at most once per 10 ms per account; ticks in between are coalesced), plus a once-a-second heartbeat for any other change (balance, equity, margin, free margin, level, floating profit). money.version advances on durable account changes (trades, money operations, day end), not on price or settings revaluations. Do not discard equal-version updates: order socket valuations by data.time within the same version and reject older versions. A REST trade answer has no valuation timestamp: apply its deals/orders/positions once, but do not overwrite an already received equal-version socket valuation with its money. Resync from REST after reconnecting. Sent to the account's own trader sockets, and to staff whose group scope covers the account's group and who hold trades.view for trade/order/position/balance/day_end/correction, accounts.view for money, or risk.view for margin_call/risk_changed. Money-only readers receive no position payload. Unknown event names are denied; staff frames carry group.
ts
const socket = trader.socket()
socket.on("money", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "money",
"group": "real\\standard",
"login": 1001,
"data": {
"closed": [
1
],
"command_hash": "string",
"deals": [
{
"action": "buy",
"calc_type": "forex",
"comment": "Deposit via bank",
"commission": "1.08452",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"entry": "in",
"external_id": "string",
"fee": "1.08452",
"gateway_id": 1,
"id": 1
}
],
"deals_deleted": [
1
],
"deals_edited": [
{
"action": "buy",
"calc_type": "forex",
"comment": "Deposit via bank",
"commission": "1.08452",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"entry": "in",
"external_id": "string",
"fee": "1.08452",
"gateway_id": 1,
"id": 1
}
],
"group": "real\\standard",
"login": 1001,
"margin_check": {
"additional_margin": "1.08452",
"before": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"group": "real\\standard",
"projected": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"shortfall": "1.08452",
"symbol": "EURUSD",
"virtual_credit": "1.08452"
},
"money": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"orders": [
{
"activated_at": "2026-09-25T10:15:00Z",
"by_position_id": 1,
"comment": "Deposit via bank",
"contract_size": "1.08452",
"created_at": "2026-09-25T10:15:00Z",
"dealer": 1,
"deviation_pts": 1,
"digits": 5,
"done_at": "2026-09-25T10:15:00Z",
"expiration": "string",
"expires_at": "2026-09-25T10:15:00Z",
"external_id": "string"
}
],
"orders_deleted": [
1
],
"placed": [
1
],
"positions": [
{
"comment": "Deposit via bank",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"external_id": "string",
"id": 1,
"login": 1001,
"margin": "108.45",
"opened_at": "2026-09-25T10:15:00Z",
"origin": {},
"price_current": "1.08452",
"price_open": "1.08452"
}
],
"quiet": true,
"request_id": "3f2a9c1e-8b7d-4e6f-a5c4-1b2d3e4f5a6b",
"seq": 1,
"time": "2026-09-25T10:15:00Z",
"type": "string"
}
}notification
RECEIVE
socket.on("notification")A message to show in the terminal. A stored copy is at trader.notifications.list().
Who receives itTrader (own account)
Fields
groupstringThe account's group; staff frames only.
loginintegerrequiredThe account the event is about.
data.companystringrequiredthe signature: the account group's company
data.created_atstringrequiredUTC time, RFC 3339.
data.idintegerrequireddata.loginintegerrequiredLogin is the numeric identity of every account, trader or staff.
data.messagestringrequireddata.senderintegerrequiredLogin is the numeric identity of every account, trader or staff.
Server notes
Show it in the terminal; the stored copy is at GET /trader/v1/notifications.
ts
const socket = trader.socket()
socket.on("notification", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "notification",
"group": "real\\standard",
"login": 1001,
"data": {
"company": "HS Markets Ltd",
"created_at": "2026-09-25T10:15:00Z",
"id": 1,
"login": 1001,
"message": "string",
"sender": 1
}
}order
RECEIVE
socket.on("order")A pending order was placed, changed, cancelled or expired.
Who receives itTrader (own account) · Staff (in scope)
Fields
groupstringThe account's group; staff frames only.
loginintegerrequiredThe account the event is about.
data.closedinteger[]position ids that no longer exist
data.command_hashstringdata.dealsDeal[]data.deals_deletedinteger[]data.deals_editedDeal[]Corrections by staff: deals rewritten in place and deals removed from history.
data.groupstringrequireddata.loginintegerrequiredLogin is the numeric identity of every account, trader or staff.
data.margin_checkanyrejected proposal evidence, not applied account state
data.moneyanydata.ordersOrder[]data.orders_deletedinteger[]OrdersDeleted are order ids removed by staff, open (also sent in Orders as cancelled, so terminals drop them) or from the history.
data.placedinteger[]pending order ids this change created (the rest in Orders changed)
data.positionsPosition[]state after the change
data.quietbooleanbookkeeping only (e.g. a provider's order id arrived): nothing to tell the client
Server notes
Placed (placed lists new order ids), modified, cancelled or expired. orders carries each order's state after the change. Sent to the account's own trader sockets, and to staff whose group scope covers the account's group and who hold trades.view for trade/order/position/balance/day_end/correction, accounts.view for money, or risk.view for margin_call/risk_changed. Money-only readers receive no position payload. Unknown event names are denied; staff frames carry group.
ts
const socket = trader.socket()
socket.on("order", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "order",
"group": "real\\standard",
"login": 1001,
"data": {
"closed": [
1
],
"command_hash": "string",
"deals": [
{
"action": "buy",
"calc_type": "forex",
"comment": "Deposit via bank",
"commission": "1.08452",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"entry": "in",
"external_id": "string",
"fee": "1.08452",
"gateway_id": 1,
"id": 1
}
],
"deals_deleted": [
1
],
"deals_edited": [
{
"action": "buy",
"calc_type": "forex",
"comment": "Deposit via bank",
"commission": "1.08452",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"entry": "in",
"external_id": "string",
"fee": "1.08452",
"gateway_id": 1,
"id": 1
}
],
"group": "real\\standard",
"login": 1001,
"margin_check": {
"additional_margin": "1.08452",
"before": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"group": "real\\standard",
"projected": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"shortfall": "1.08452",
"symbol": "EURUSD",
"virtual_credit": "1.08452"
},
"money": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"orders": [
{
"activated_at": "2026-09-25T10:15:00Z",
"by_position_id": 1,
"comment": "Deposit via bank",
"contract_size": "1.08452",
"created_at": "2026-09-25T10:15:00Z",
"dealer": 1,
"deviation_pts": 1,
"digits": 5,
"done_at": "2026-09-25T10:15:00Z",
"expiration": "string",
"expires_at": "2026-09-25T10:15:00Z",
"external_id": "string"
}
],
"orders_deleted": [
1
],
"placed": [
1
],
"positions": [
{
"comment": "Deposit via bank",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"external_id": "string",
"id": 1,
"login": 1001,
"margin": "108.45",
"opened_at": "2026-09-25T10:15:00Z",
"origin": {},
"price_current": "1.08452",
"price_open": "1.08452"
}
],
"quiet": true,
"request_id": "3f2a9c1e-8b7d-4e6f-a5c4-1b2d3e4f5a6b",
"seq": 1,
"time": "2026-09-25T10:15:00Z",
"type": "string"
}
}position
RECEIVE
socket.on("position")A position changed without a trade: stop loss / take profit edited, or swap charged.
Who receives itTrader (own account) · Staff (in scope)
Fields
groupstringThe account's group; staff frames only.
loginintegerrequiredThe account the event is about.
data.closedinteger[]position ids that no longer exist
data.command_hashstringdata.dealsDeal[]data.deals_deletedinteger[]data.deals_editedDeal[]Corrections by staff: deals rewritten in place and deals removed from history.
data.groupstringrequireddata.loginintegerrequiredLogin is the numeric identity of every account, trader or staff.
data.margin_checkanyrejected proposal evidence, not applied account state
data.moneyanydata.ordersOrder[]data.orders_deletedinteger[]OrdersDeleted are order ids removed by staff, open (also sent in Orders as cancelled, so terminals drop them) or from the history.
data.placedinteger[]pending order ids this change created (the rest in Orders changed)
data.positionsPosition[]state after the change
data.quietbooleanbookkeeping only (e.g. a provider's order id arrived): nothing to tell the client
Server notes
Stop loss / take profit changed, swap charged. Sent to the account's own trader sockets, and to staff whose group scope covers the account's group and who hold trades.view for trade/order/position/balance/day_end/correction, accounts.view for money, or risk.view for margin_call/risk_changed. Money-only readers receive no position payload. Unknown event names are denied; staff frames carry group.
ts
const socket = trader.socket()
socket.on("position", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "position",
"group": "real\\standard",
"login": 1001,
"data": {
"closed": [
1
],
"command_hash": "string",
"deals": [
{
"action": "buy",
"calc_type": "forex",
"comment": "Deposit via bank",
"commission": "1.08452",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"entry": "in",
"external_id": "string",
"fee": "1.08452",
"gateway_id": 1,
"id": 1
}
],
"deals_deleted": [
1
],
"deals_edited": [
{
"action": "buy",
"calc_type": "forex",
"comment": "Deposit via bank",
"commission": "1.08452",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"entry": "in",
"external_id": "string",
"fee": "1.08452",
"gateway_id": 1,
"id": 1
}
],
"group": "real\\standard",
"login": 1001,
"margin_check": {
"additional_margin": "1.08452",
"before": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"group": "real\\standard",
"projected": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"shortfall": "1.08452",
"symbol": "EURUSD",
"virtual_credit": "1.08452"
},
"money": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"orders": [
{
"activated_at": "2026-09-25T10:15:00Z",
"by_position_id": 1,
"comment": "Deposit via bank",
"contract_size": "1.08452",
"created_at": "2026-09-25T10:15:00Z",
"dealer": 1,
"deviation_pts": 1,
"digits": 5,
"done_at": "2026-09-25T10:15:00Z",
"expiration": "string",
"expires_at": "2026-09-25T10:15:00Z",
"external_id": "string"
}
],
"orders_deleted": [
1
],
"placed": [
1
],
"positions": [
{
"comment": "Deposit via bank",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"external_id": "string",
"id": 1,
"login": 1001,
"margin": "108.45",
"opened_at": "2026-09-25T10:15:00Z",
"origin": {},
"price_current": "1.08452",
"price_open": "1.08452"
}
],
"quiet": true,
"request_id": "3f2a9c1e-8b7d-4e6f-a5c4-1b2d3e4f5a6b",
"seq": 1,
"time": "2026-09-25T10:15:00Z",
"type": "string"
}
}risk_changed
RECEIVE
socket.on("risk_changed")An account moved between normal, margin call and stop out (either way). Reload its risk view.
Who receives itStaff with risk.view
Fields
groupstringThe account's group; staff frames only.
loginintegerrequiredThe account the event is about.
data.timestringrequiredUTC time, RFC 3339.
Server notes
Staff-only identity/time notification on authoritative OK, margin-call or stop-out transition, including recovery. Requires risk.view and current account visibility. Reload the affected risk query; carries no financial or personal payload and is not logged. Sent to the account's own trader sockets, and to staff whose group scope covers the account's group and who hold trades.view for trade/order/position/balance/day_end/correction, accounts.view for money, or risk.view for margin_call/risk_changed. Money-only readers receive no position payload. Unknown event names are denied; staff frames carry group.
ts
const socket = staff.socket()
socket.on("risk_changed", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "risk_changed",
"group": "real\\standard",
"login": 1001,
"data": {
"time": "2026-09-25T10:15:00Z"
}
}session_connected
RECEIVE
socket.on("session_connected")A terminal of this account signed in.
Who receives itTrader (own account)
Fields
groupstringThe account's group; staff frames only.
loginintegerrequiredThe account the event is about.
data.ipstringrequireddata.sessionstringrequireddata.staffbooleanrequireddata.terminalTerminalrequiredTerminal is the kind of client a session was opened from.
Server notes
Sent to the account's own trader sockets, including the newly opened socket after welcome, and to staff with accounts.online and the session's group in scope when a trader or staff socket connects. login names the account; data carries session, terminal, ip and staff. Staff frames also carry group. Not logged.
ts
const socket = trader.socket()
socket.on("session_connected", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "session_connected",
"group": "real\\standard",
"login": 1001,
"data": {
"ip": "string",
"session": "string",
"staff": true,
"terminal": "web"
}
}session_disconnected
RECEIVE
socket.on("session_disconnected")A terminal of this account disconnected (sign-out, kick, expiry or network drop).
Who receives itTrader (own account)
Fields
groupstringThe account's group; staff frames only.
loginintegerrequiredThe account the event is about.
data.ipstringrequireddata.sessionstringrequireddata.staffbooleanrequireddata.terminalTerminalrequiredTerminal is the kind of client a session was opened from.
Server notes
The counterpart of session_connected, sent when a socket closes (sign-out, kick, expiry, network drop). Not logged.
ts
const socket = trader.socket()
socket.on("session_disconnected", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "session_disconnected",
"group": "real\\standard",
"login": 1001,
"data": {
"ip": "string",
"session": "string",
"staff": true,
"terminal": "web"
}
}trade
RECEIVE
socket.on("trade")Something traded: a fill, a close, a stop loss / take profit hit or a stop out. Carries the new deals, the positions after the change, the ids of closed positions and the money after it.
Who receives itTrader (own account) · Staff (in scope)
Fields
groupstringThe account's group; staff frames only.
loginintegerrequiredThe account the event is about.
data.closedinteger[]position ids that no longer exist
data.command_hashstringdata.dealsDeal[]data.deals_deletedinteger[]data.deals_editedDeal[]Corrections by staff: deals rewritten in place and deals removed from history.
data.groupstringrequireddata.loginintegerrequiredLogin is the numeric identity of every account, trader or staff.
data.margin_checkanyrejected proposal evidence, not applied account state
data.moneyanydata.ordersOrder[]data.orders_deletedinteger[]OrdersDeleted are order ids removed by staff, open (also sent in Orders as cancelled, so terminals drop them) or from the history.
data.placedinteger[]pending order ids this change created (the rest in Orders changed)
data.positionsPosition[]state after the change
data.quietbooleanbookkeeping only (e.g. a provider's order id arrived): nothing to tell the client
Server notes
Fills, closes, stop loss / take profit hits, stop outs. deals are the new deals, positions the positions after the change, closed the position ids that no longer exist, orders the orders it touched, money the account's money after it. Sent to the account's own trader sockets, and to staff whose group scope covers the account's group and who hold trades.view for trade/order/position/balance/day_end/correction, accounts.view for money, or risk.view for margin_call/risk_changed. Money-only readers receive no position payload. Unknown event names are denied; staff frames carry group.
ts
const socket = trader.socket()
socket.on("trade", (frame) => {
for (const id of frame.data.closed ?? []) removePosition(id)
for (const p of frame.data.positions ?? []) upsertPosition(p)
})
await socket.connect()json
{
"type": "trade",
"group": "real\\standard",
"login": 1001,
"data": {
"closed": [
1
],
"command_hash": "string",
"deals": [
{
"action": "buy",
"calc_type": "forex",
"comment": "Deposit via bank",
"commission": "1.08452",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"entry": "in",
"external_id": "string",
"fee": "1.08452",
"gateway_id": 1,
"id": 1
}
],
"deals_deleted": [
1
],
"deals_edited": [
{
"action": "buy",
"calc_type": "forex",
"comment": "Deposit via bank",
"commission": "1.08452",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"entry": "in",
"external_id": "string",
"fee": "1.08452",
"gateway_id": 1,
"id": 1
}
],
"group": "real\\standard",
"login": 1001,
"margin_check": {
"additional_margin": "1.08452",
"before": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"group": "real\\standard",
"projected": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"shortfall": "1.08452",
"symbol": "EURUSD",
"virtual_credit": "1.08452"
},
"money": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"orders": [
{
"activated_at": "2026-09-25T10:15:00Z",
"by_position_id": 1,
"comment": "Deposit via bank",
"contract_size": "1.08452",
"created_at": "2026-09-25T10:15:00Z",
"dealer": 1,
"deviation_pts": 1,
"digits": 5,
"done_at": "2026-09-25T10:15:00Z",
"expiration": "string",
"expires_at": "2026-09-25T10:15:00Z",
"external_id": "string"
}
],
"orders_deleted": [
1
],
"placed": [
1
],
"positions": [
{
"comment": "Deposit via bank",
"contract_size": "1.08452",
"dealer": 1,
"digits": 5,
"external_id": "string",
"id": 1,
"login": 1001,
"margin": "108.45",
"opened_at": "2026-09-25T10:15:00Z",
"origin": {},
"price_current": "1.08452",
"price_open": "1.08452"
}
],
"quiet": true,
"request_id": "3f2a9c1e-8b7d-4e6f-a5c4-1b2d3e4f5a6b",
"seq": 1,
"time": "2026-09-25T10:15:00Z",
"type": "string"
}
}CRM
client_changed
RECEIVE
socket.on("client_changed")A client was saved (by staff or by a re-application). Refresh it if it is on screen.
Who receives itStaff with clients.view (in scope)
Fields
data.approved_atstringrequiredUTC time, RFC 3339.
data.approved_byintegerrequiredLogin is the numeric identity of every account, trader or staff.
data.assigned_managerintegerrequired0 = none Login is the numeric identity of every account, trader or staff.
data.citystringrequireddata.commentstringrequireddata.company_namestringrequireddata.countrystringrequireddata.created_atstringrequiredUTC time, RFC 3339.
data.created_byintegerrequiredLogin is the numeric identity of every account, trader or staff.
data.detailsDetailsrequiredDetails is the rest of the profile. Dates are YYYY-MM-DD, amounts are decimals.
data.emailstringrequireddata.first_namestringrequireddata.idintegerrequireddata.kyc_statusstringrequiredServer notes
The counterpart of client_created, sent on every save (staff edit or self-registration re-application). Same routing.
ts
const socket = staff.socket()
socket.on("client_changed", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "client_changed",
"data": {
"approved_at": "2026-09-25T10:15:00Z",
"approved_by": 1,
"assigned_manager": 1,
"city": "London",
"comment": "Deposit via bank",
"company_name": "Smith Trading Ltd",
"country": "United Kingdom",
"created_at": "2026-09-25T10:15:00Z",
"created_by": 1,
"details": {
"address": "1 High St",
"allocation_id": 1,
"annual_deposit": "1.08452",
"annual_income": "1.08452",
"birth_date": "string",
"citizenship": "string",
"close_date": "string",
"contact_preferred": "string",
"document_comment": "string",
"document_expires": "string",
"document_issued": "string",
"document_number": "string"
},
"email": "ann@example.com",
"first_name": "Ann",
"id": 1,
"kyc_status": "string",
"last_name": "Smith",
"origin": "string",
"phone": "+44 20 7946 0000",
"preferred_group": "real\\standard",
"status": "active",
"type": "string",
"updated_at": "2026-09-25T10:15:00Z",
"version": 1
}
}client_created
RECEIVE
socket.on("client_created")A new client: created by staff or a trader's self sign-up. Use it for a live lead / application queue.
Who receives itStaff with clients.view (in scope)
Fields
data.approved_atstringrequiredUTC time, RFC 3339.
data.approved_byintegerrequiredLogin is the numeric identity of every account, trader or staff.
data.assigned_managerintegerrequired0 = none Login is the numeric identity of every account, trader or staff.
data.citystringrequireddata.commentstringrequireddata.company_namestringrequireddata.countrystringrequireddata.created_atstringrequiredUTC time, RFC 3339.
data.created_byintegerrequiredLogin is the numeric identity of every account, trader or staff.
data.detailsDetailsrequiredDetails is the rest of the profile. Dates are YYYY-MM-DD, amounts are decimals.
data.emailstringrequireddata.first_namestringrequireddata.idintegerrequireddata.kyc_statusstringrequiredServer notes
To staff with clients.view whose group scope covers a non-empty preferred or visible linked account group, or who created it or is its assigned manager. Personal fields are projected by each recipient's clients.field.* rights. Empty groups never widen visibility. Not logged; resync over GET /admin/v1/clients after a reconnect.
ts
const socket = staff.socket()
socket.on("client_created", (frame) => {
addToLeadQueue(frame.data) // a new client to review
})
await socket.connect()json
{
"type": "client_created",
"data": {
"approved_at": "2026-09-25T10:15:00Z",
"approved_by": 1,
"assigned_manager": 1,
"city": "London",
"comment": "Deposit via bank",
"company_name": "Smith Trading Ltd",
"country": "United Kingdom",
"created_at": "2026-09-25T10:15:00Z",
"created_by": 1,
"details": {
"address": "1 High St",
"allocation_id": 1,
"annual_deposit": "1.08452",
"annual_income": "1.08452",
"birth_date": "string",
"citizenship": "string",
"close_date": "string",
"contact_preferred": "string",
"document_comment": "string",
"document_expires": "string",
"document_issued": "string",
"document_number": "string"
},
"email": "ann@example.com",
"first_name": "Ann",
"id": 1,
"kyc_status": "string",
"last_name": "Smith",
"origin": "string",
"phone": "+44 20 7946 0000",
"preferred_group": "real\\standard",
"status": "active",
"type": "string",
"updated_at": "2026-09-25T10:15:00Z",
"version": 1
}
}mail
RECEIVE
socket.on("mail")A new internal mail message arrived.
Who receives itMailbox owner
Fields
data.attachmentsMailAttachment[]requireddata.bodystringrequireddata.created_atstringrequiredUTC time, RFC 3339.
data.draftanydrafts: the addressing as saved
data.folderstringrequireddata.fromintegerrequired0 = the platform Login is the numeric identity of every account, trader or staff.
data.from_namestringrequireddata.idintegerrequireddata.message_idstringrequireddata.readbooleanrequireddata.recipientsstringrequiredsent copies: who it went to
data.subjectstringrequireddata.thread_countintegerfolder lists: messages of the thread in the folder
data.thread_idstringrequiredshared by a message and every reply to it
Server notes
To the mailbox owner; staff additionally require mailbox.
ts
const socket = staff.socket()
socket.on("mail", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "mail",
"data": {
"attachments": [
{
"id": 1,
"name": "Ann Smith",
"size": 1
}
],
"body": "string",
"created_at": "2026-09-25T10:15:00Z",
"draft": {
"email": "ann@example.com",
"reply_to": 1,
"to": 1,
"to_group": "string",
"to_logins": [
1
]
},
"folder": "string",
"from": 1,
"from_name": "string",
"id": 1,
"message_id": "3f2a9c1e-8b7d-4e6f-a5c4-1b2d3e4f5a6b",
"read": true,
"recipients": "string",
"subject": "string",
"thread_count": 1,
"thread_id": "3f2a9c1e-8b7d-4e6f-a5c4-1b2d3e4f5a6b",
"to": 1
}
}mail_changed
RECEIVE
socket.on("mail_changed")The mailbox changed (a message was deleted). Re-read the list.
Who receives itMailbox owner
Server notes
Identifier-free invalidation after deletion of the owner's copy. Staff readers require mailbox; traders receive only their own mailbox change. Re-read the canonical mailbox list; staff use the existing staff snapshot socket.
ts
const socket = staff.socket()
socket.on("mail_changed", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "mail_changed",
"data": {}
}news
RECEIVE
socket.on("news")A news item was published.
Who receives itStaff with news · Traders as their group allows
Fields
data.bodystringrequireddata.categorystringrequireddata.datafeed_idintegerdata.external_idstringthe datafeed's id of the item
data.idintegerrequireddata.published_atstringrequiredUTC time, RFC 3339.
data.sourcestringrequireddata.subjectstringrequireddata.symbolsstring[]symbols the item is about; empty = every symbol
Server notes
Staff with the news right get it whole; traders as their group's news mode allows (headlines only: body empty; off: nothing).
ts
const socket = staff.socket()
socket.on("news", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "news",
"data": {
"body": "string",
"category": "string",
"datafeed_id": 1,
"external_id": "string",
"id": 1,
"published_at": "2026-09-25T10:15:00Z",
"source": "string",
"subject": "string",
"symbols": [
"string"
]
}
}news_changed
RECEIVE
socket.on("news_changed")The news list changed (an item was deleted). Re-read it.
Who receives itStaff with news · Traders
Server notes
Identifier-free invalidation after a persisted deletion. Staff readers require news; traders require a group news mode other than off. Re-read the authorized news catalogue; staff use staff snapshots.
ts
const socket = staff.socket()
socket.on("news_changed", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "news_changed",
"data": {}
}report_job
RECEIVE
socket.on("report_job")Progress of a report you started: queued, running, done or failed.
Who receives itStaff who ran the report
Fields
data.attemptsintegerrequiredruns tried
data.errorstringrequiredwhy it failed, when it did
data.idintegerrequiredthe report job
data.rowsintegerrequiredrows written so far
data.statestringrequiredqueued, running, done, failed or cancelled
Server notes
Only to the staff owner holding reports or config.reports. Missed ones: poll GET /admin/v1/reports/jobs/{id}.
ts
const socket = staff.socket()
socket.on("report_job", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "report_job",
"data": {
"attempts": 1,
"error": "string",
"id": 1,
"rows": 1,
"state": "string"
}
}Dealing
dealing_heartbeat
RECEIVE
socket.on("dealing_heartbeat")The answer to your dealing heartbeat: your dealer connection is still alive.
Who receives itSigned-in dealer
Fields
bodyanyrequiredexisting JSON envelope, decimals and pagination unchanged
request_idstringrequiredstatusintegerrequiredoriginal response status, including denial or validation errors
ts
const socket = staff.socket()
socket.on("dealing_heartbeat", (frame) => {
console.log(frame)
})
await socket.connect()json
{
"type": "dealing_heartbeat",
"body": {
"code": "ok",
"data": null,
"message": "string",
"request_id": "string",
"success": true
},
"request_id": "string",
"status": "active"
}dealing_log_changed
RECEIVE
socket.on("dealing_log_changed")The dealing log has new lines. Re-read it.
Who receives itStaff with dealing
Server notes
Advertised by dealing_log_live_v1. Identifier-free staff invalidation after the canonical SQL projection commits. Requires dealing, current ordinary account group/hidden visibility and either the caller as dealer or dealing.supervisor. Queue-only moves and replay/no-op projections produce no notice. Coalesce and reread the current filtered log over the shared socket; no healthy polling. A database listener gap closes staff sockets, and the next welcome must resync. Both staff presentations use the same read rights and scope.
ts
const socket = staff.socket()
socket.on("dealing_log_changed", (frame) => {
console.log(frame)
})
await socket.connect()json
{
"type": "dealing_log_changed"
}desk
RECEIVE
socket.on("desk")A request in the dealer queue changed: waiting, taken, requoted or done.
Who receives itStaff with dealing (in scope)
Fields
groupstringThe client's group.
loginintegerAlways 0 on desk frames: the client's login is in data.request.login.
data.askstringrequiredExact decimal as a string. Never parse it into a binary float for money or prices.
data.bidstringrequiredrequote Exact decimal as a string. Never parse it into a binary float for money or prices.
data.deadlinestringrequiredUTC time, RFC 3339.
data.dealersinteger[]offer order; empty = any online dealer
data.holderintegerrequiredthe dealer it is offered to now Login is the numeric identity of every account, trader or staff.
data.last_actionanyLastAction is what made this change: a dealer's or client's answer, or queue, offer, timeout or restored; the dealing log is built from it.
data.last_actoranyauthenticated actor of LastAction, absent for server actions
data.queued_atstringrequiredUTC time, RFC 3339.
data.requestTradeRequestrequiredTradeRequest travels gateway -> validation -> routing -> execution. The gateway fills Login, Group and Leverage from the session, never from the client.
data.reservedstringrequiredmargin held while it waits Exact decimal as a string. Never parse it into a binary float for money or prices.
data.resultanywhen done
data.returnedinteger[]data.stateDeskStaterequiredDeskState is where a queued request stands. -
waiting: offered to Holder (0 = nobody eligible is online yet) - taken: Holder locked it to work on it - requoted: the client may accept Bid/Ask until Deadline - done: decided; Result says howdata.symbol_idintegerrequiredServer notes
To staff with the dealing right whose group scope covers the client's group, whether connected as a dealer or not. group is the client's group. The item's state (waiting, taken, requoted, done) and last_action tell what happened; answer through POST /admin/v1/dealing/{request_id}/take|confirm|requote|reject|return.
ts
const socket = staff.socket()
socket.on("desk", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "desk",
"group": "real\\standard",
"login": 1001,
"data": {
"ask": "1.08460",
"bid": "1.08452",
"deadline": "2026-09-25T10:15:00Z",
"dealers": [
1
],
"holder": 1,
"last_action": "list",
"last_actor": {
"channel": "string",
"ip": "string",
"login": 1001,
"platform": "string"
},
"queued_at": "2026-09-25T10:15:00Z",
"request": {
"account_policy_revision": 1,
"action": "open",
"actor": {
"channel": "string",
"ip": "string",
"login": 1001,
"platform": "string"
},
"by_position_id": 1,
"command_hash": "string",
"comment": "Deposit via bank",
"dealer": 1,
"deviation_pts": 1,
"expiration": "string",
"expires_at": "2026-09-25T10:15:00Z",
"fill_policy": "string",
"group": "real\\standard"
},
"reserved": "1.08452",
"result": {
"ask": "1.08460",
"bid": "1.08452",
"code": "ok",
"deals": [
{}
],
"deleted": [
1
],
"margin_check": {
"additional_margin": "1.08452",
"before": {},
"group": "real\\standard",
"projected": {},
"shortfall": "1.08452",
"symbol": "EURUSD",
"virtual_credit": "1.08452"
},
"message": "string",
"money": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"order": {
"activated_at": "2026-09-25T10:15:00Z",
"by_position_id": 1,
"comment": "Deposit via bank",
"contract_size": "1.08452",
"created_at": "2026-09-25T10:15:00Z",
"dealer": 1,
"deviation_pts": 1,
"digits": 5,
"done_at": "2026-09-25T10:15:00Z",
"expiration": "string",
"expires_at": "2026-09-25T10:15:00Z",
"external_id": "string"
},
"positions": [
{}
],
"request_id": "3f2a9c1e-8b7d-4e6f-a5c4-1b2d3e4f5a6b",
"status": "done"
},
"returned": [
1
],
"state": "waiting",
"symbol_id": 1,
"updated_by": 1
}
}System
config_changed
RECEIVE
socket.on("config_changed")A configuration record (group, symbol, gateway, …) changed. Re-read what you show.
Who receives itStaff with the matching config right
Fields
data.groupstringaccounts/staff changes carry an authorized event-time target group
data.idanyrequiredgroup path/string account login/numeric client id, or "all" for catalogue invalidation
data.kindstringrequiredaccounts, clients, groups, symbols, staff, feeds, gateways, routing, holidays, leverages or a catalogue kind
Server notes
Only to staff holding the read right for the exact kind; group/staff records also require target group scope. Unknown kinds are denied, on create/edit/delete, so matching lists can refresh instead of polling. accounts uses a positive login string and authorized event-time group; pre/post viewers receive one identity-only notification, including after deletion. clients uses a positive numeric client id; pre/post visibility includes preferred/linked trading groups, technical-account rights, creator and assignment. Neither lifecycle frame contains personal fields or other viewers' routing scopes. Catalogue kinds use id all: report_configs, report_schedules, statement_settings, mail_servers, mail_templates, automation_triggers, automation_actions, allocations, white_labels. Commission changes reuse groups, symbol-folder changes reuse symbols. This is a best-effort invalidation, not a durable event log. Admin and Manager terminals consume the same authorized data streams when staff_data_live_v1 is advertised; domain read rights and group scope apply independently of presentation.
ts
const socket = staff.socket()
socket.on("config_changed", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "config_changed",
"data": {
"group": "real\\standard",
"id": null,
"kind": "string"
}
}financial_view
RECEIVE
socket.on("financial_view")Answers and samples of a bounded positions/orders view you opened with financial_view.
Who receives itStaff (advanced)
Fields
collection_generationstringcollection_sequencestringcollectionsstring[]dataany[]execution_epochstringgenerationintegermin_projection_seqstringmodestringopstringrequiredpartintegerpartsintegerreasonstringrequest_idstringresourcesstring[]sampled_atstringsequencestringstatestringview_idstringServer notes
One shared gateway sampler reads the union through trade.live, at most200 rows/100 accounts per request,4 concurrent requests and20,000 active unique keys. Ready acknowledges registration; the first delta supplies values. String sequence is per view/generation, not durable replay. Exact ID/login/durable_seq remain strings. Data status unavailable or absent omits financial fields. For no_prices or symbol_unavailable, positions may retain last-known values, while orders carry price_current as decimal-string zero because pricing is unavailable; this is not a market price of zero. Unchanged samples may omit data; sampled_at is transport observation time, not a quote timestamp. account_money carries only id/login, optional durable_seq and money. online_equity and risk_money carry the corresponding masked observation instead of full money: Online values are login/version/currency/currency_digits/equity; Risk also includes balance/credit/profit/margin/free_margin/margin_level, never full Money, historical amounts or trade arrays; decimal amounts and login/version/revision remain exact strings. State and original calculated_at identify the last authoritative calculation; observed_at/age never promote stale, dirty, incomplete or busy values to current. Money digests exclude only observation clock/age, so state/revision/value changes still send data. Account-unavailable affects only its rows. For position/order views, a targeted structural invalidate carries the current view_id, generation, per-view string sequence and execution_epoch, without financial rows. Apply it only to the current acknowledged view generation and execution epoch. It suspends values until authorized replacement using the same view_id and a strictly higher generation; refresh SQL with min_projection_seq before replacing the visible IDs. min_projection_seq is the observed nonzero durable event sequence and does not claim collection completeness. Fixed-login Money views retain their registration after a trade: queued/pre-event RPC samples are discarded, and a later sample must reach the observed durable sequence before delivery resumes. A stale sample emits no healthy heartbeat. Busy/unavailable accounts omit Money values; they are not deletions or current zeros. Upstream gap/epoch mismatch/queue overflow closes for resync. Frames<=64KiB, complete multipart sequence<=256KiB; part/parts must assemble before application.1MiB financial queued bytes per socket and64MiB per gateway; no partial batch admission. financial_collections_v1 adds identifier-free op=collections on the same socket for authorized positions/orders/accounts/history/risk/market_usage structural refresh, including unseen accounts. It carries exact string collection_generation, collection_sequence, min_projection_seq and execution_epoch, with no account values or sampled_at. Apply only within the acknowledged generation/epoch; detect gaps and refresh with the SQL projection fence. At most one queued batch and one fixed pending mask, flushing once/second. Price/Money and unlogged position valuations do not trigger collection refresh. Legacy sockets remain unchanged.
ts
const socket = staff.socket()
socket.on("financial_view", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "financial_view",
"collection_generation": "string",
"collection_sequence": "string",
"collections": [
"positions"
],
"execution_epoch": "string",
"generation": 1,
"min_projection_seq": "string",
"mode": "string",
"op": "string",
"part": 1,
"parts": 1,
"reason": "string",
"request_id": "string",
"resources": [
"string"
],
"sampled_at": "2026-09-25T10:15:00Z",
"sequence": "string",
"state": "string",
"view_id": "string",
"data": [
{
"durable_seq": "1.00",
"id": "1.00",
"login": 1001,
"money": {}
}
]
}journal
RECEIVE
socket.on("journal")Live server journal lines.
Who receives itStaff with server.logs, after `journal on`
Fields
dataLiveEntry[]requiredServer notes
To staff sockets that sent {"type":"journal","on":true} and hold server.logs: platform lines and lines of accounts whose group is in their scope and within their history limit. Compact read projections omit params/details, retain safe search metadata and a detail_ref (line_id, at). Only INFO-and-above entries are delivered. Successful configuration field expansions, including historical INFO .field records, are classified DEBUG and excluded even when no matching action summary exists. Independent operational action notices remain; warnings, errors and failed field operations retain their severity. Full immutable records remain searchable through the authorized journal API. Frames target 64 KiB; a single oversized legacy summary remains intact. Fetch line details on explicit demand using line_id and a narrow from/to time range; no per-event detail request is needed.
ts
const socket = staff.socket()
socket.on("journal", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "journal",
"data": [
{
"action": "string",
"actor_ip": "string",
"actor_login": 1,
"at": "2026-09-25T10:15:00Z",
"category": "auth",
"channel": "string",
"correlation_id": "string",
"detail_ref": {},
"details": null,
"id": 1,
"level": "debug",
"line_id": "3f2a9c1e-8b7d-4e6f-a5c4-1b2d3e4f5a6b"
}
]
}online_equity
RECEIVE
socket.on("online_equity")Live equity of an account that is online right now.
Who receives itStaff with accounts.online
Fields
groupstringrequiredloginintegerrequiredLogin is the numeric identity of every account, trader or staff.
data.equitystringrequiredExact decimal as a string. Never parse it into a binary float for money or prices.
data.timestringrequiredUTC time, RFC 3339.
data.versionintegerrequiredServer notes
Staff accounts.online-only readers receive this masked valuation only while an active nonstaff session exists. Current group and hidden-account visibility applies before one shared per-account Redis read. Contains no balance, credit, positions or personal data. Apply only to loaded online rows ordered by version/time. accounts.view readers use existing money events without an extra lookup or duplicate frame.
ts
const socket = staff.socket()
socket.on("online_equity", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "online_equity",
"group": "real\\standard",
"login": 1001,
"data": {
"equity": "1012.40",
"time": "2026-09-25T10:15:00Z",
"version": 1
}
}risk_money
RECEIVE
socket.on("risk_money")Live money of accounts for risk screens (without full account access).
Who receives itStaff with risk.view only
Fields
groupstringrequiredloginintegerrequiredLogin is the numeric identity of every account, trader or staff.
data.moneyMoneyrequiredMoney is an account's live money in its deposit currency.
data.timestringrequiredUTC time, RFC 3339.
Server notes
Staff risk.view without accounts.view receives the same scoped money fields as risk REST, excluding staff accounts and respecting current hidden-account visibility. No positions, names or audit context. Patch loaded risk rows ordered by money.version and time; membership transitions use risk_changed/identity invalidations. Full accounts.view readers reuse the ordinary money event without a duplicate risk frame.
ts
const socket = staff.socket()
socket.on("risk_money", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "risk_money",
"group": "real\\standard",
"login": 1001,
"data": {
"money": {
"agent_commission": "1.08452",
"balance": "1000.00",
"balance_prev_day": "1.08452",
"balance_prev_month": "1.08452",
"blocked_commission": "1.08452",
"blocked_profit": "1.08452",
"credit": "0.00",
"currency": "USD",
"currency_digits": 1,
"day": "2026-09-25T10:15:00Z",
"effective_leverage": "1.08452",
"equity": "1012.40"
},
"time": "2026-09-25T10:15:00Z"
}
}snapshot
RECEIVE
socket.on("snapshot")The answer to a snapshot request: a REST read done over the socket.
Who receives itStaff (advanced)
Fields
bodyanyrequiredexisting JSON envelope, decimals and pagination unchanged
request_idstringrequiredstatusintegerrequiredoriginal response status, including denial or validation errors
ts
const socket = staff.socket()
socket.on("snapshot", (frame) => {
console.log(frame)
})
await socket.connect()json
{
"body": {
"code": "ok",
"data": {
"items": [],
"page": 1,
"total": 0
},
"message": "OK",
"success": true
},
"request_id": "accounts-1",
"status": 200,
"type": "snapshot"
}symbols_changed
RECEIVE
socket.on("symbols_changed")The symbol list changed. Re-read symbols before showing prices.
Who receives itTraders · Staff with config.symbols
Server notes
Sent after the gateway installs a changed symbol catalogue to traders and staff holding prices.raw or config.symbols. Re-read the authorized configuration, group-resolved or raw catalogue through canonical reads; staff reuse snapshots. Contains no identifiers or staff configuration data.
ts
const socket = trader.socket()
socket.on("symbols_changed", (frame) => {
console.log(frame.data)
})
await socket.connect()json
{
"type": "symbols_changed",
"data": {}
}Messages you send
dealing_heartbeat
SEND
{ "type": "dealing_heartbeat" }Dealer: keep your dealer connection alive (at least every 30 s).
Fields
request_idstringrequiredno body, path, login or target is accepted
Server notes
Requires dealing_heartbeat_v1 and an admitted staff presentation and the existing dealing right. Calls only the existing guarded heartbeat handler for the admitted session login. Never connects or revives expired desk presence. An independent worker with one pending renewal keeps ordinary page reads from delaying lease renewal; fresh session checks and the same response bounds apply.
ts
const socket = staff.socket()
await socket.connect()
socket.send({
request_id: "lease-1",
type: "dealing_heartbeat"
})json
{
"request_id": "lease-1",
"type": "dealing_heartbeat"
}financial_view
SEND
{ "type": "financial_view" }Staff (advanced): open a bounded live view of positions/orders.
Fields
generationintegeridsstring[]modestringopstringrequiredrequest_idstringrequiredresourcestringview_idstringServer notes
Requires financial_views_v1 and staff data-live policy. Positions/orders require effective trades.view and current nonstaff account scope. account_money additionally requires the execution financial_money_v1 token and accounts.view; current account scope, hidden-account visibility and staff target rights/group/history hierarchy are rechecked. Explicit mode=bounded is opt-in; acknowledgement lists only supported and authorized resources. online_equity/risk_money additionally require gateway financial_restricted_money_v1 and effective accounts.online/risk.view respectively; online equity needs current active trader presence. Both exclude staff targets and recheck scope/hidden visibility; no accounts.view grant. Replace up to200 canonical positive int64 strings per view (ticket IDs for positions/orders; login IDs for all Money projections),4 active views/800 identities per socket, with increasing generation and request_id. At most8KiB for this command; existing commands remain4KiB. Ticket IDs use the existing scoped list service; Money logins use account target authorization without requesting full account valuation. Neither is trusted as reusable access proof; queued frames recheck current authority. With financial_collections_v1, bounded mode ACK also contains collection_generation, collection_sequence=0 and min_projection_seq from a read-only TRADES stream checkpoint joined with structural events observed during negotiation. Initial SQL reads must meet that projection fence; checkpoint failure leaves mode unchanged. New mode stops this socket's broad staff financial watch; do not opt an entire workspace in until every visible consumer supports the bounded contract. Close must include the current view_id and its exact current generation to retire that registration. Reconnect negotiates afresh; discard frames from the prior execution epoch.
ts
const socket = staff.socket()
await socket.connect()
socket.send({
type: "financial_view",
generation: 1,
ids: [
"string"
],
mode: "string",
op: "string",
request_id: "string",
resource: "string",
view_id: "string"
})json
{
"type": "financial_view",
"generation": 1,
"ids": [
"string"
],
"mode": "string",
"op": "string",
"request_id": "string",
"resource": "string",
"view_id": "string"
}journal
SEND
{ "type": "journal" }Staff with server.logs: switch live journal lines on or off.
Fields
onbooleanrequiredtrue to follow the server journal, false to stop
Server notes
Staff with the server.logs right get compact INFO-and-above journal frames while on. DEBUG details require an explicit authorized journal search. Off by default.
ts
const socket = staff.socket()
await socket.connect()
socket.send({
on: true,
type: "journal"
})json
{
"on": true,
"type": "journal"
}ping
SEND
{ "type": "ping" }Check the socket is alive; answered with pong.
Server notes
Answered with pong. Optional: the gateway also sends protocol pings.
ts
const socket = trader.socket()
await socket.connect()
socket.send({
type: "ping"
})json
{
"type": "ping"
}snapshot
SEND
{ "type": "snapshot" }Staff (advanced): run an allowed REST read over the socket.
Fields
pathstringrequiredauthorized /admin/v1 JSON GET path and existing filter query
request_idstringrequiredcorrelation identifier, 1 to 64 characters
Server notes
Requires staff_snapshots_v1; Admin additionally uses staff_data_live_v1. Existing staff GET handlers/rights/group scope/field masks/filter pagination run in process, with fresh session checks before and after. No HTTP connection, second socket, or dataset mirror. Auth, mutations, downloads, exports and mail message opening are excluded. Path max3000 bytes; request id max64; query max30 single-valued keys, each value max1000 characters. One ordinary worker per socket, four queued requests, five-second timeout; file-backed /journal reads and /clients/:id/history instead use an independent worker with one pending read and the existing120-second journal budget. Journal servers/days summaries have their own worker with one pending request so summary scans do not block visible history pages. two-MiB JSON response cap and eight-MiB queued-answer budget. Full queue or the shared20-message/second inbound quota returns a correlated429 for staff snapshot/heartbeat requests; disconnect cancels active work. Failed reads return human-readable code text without raw paths or technical details.
ts
const socket = staff.socket()
await socket.connect()
socket.send({
path: "/admin/v1/accounts?page=1&limit=100",
request_id: "accounts-1",
type: "snapshot"
})json
{
"path": "/admin/v1/accounts?page=1&limit=100",
"request_id": "accounts-1",
"type": "snapshot"
}unwatch
SEND
{ "type": "unwatch" }Stop receiving prices of these symbols.
Fields
rawbooleanstaff only: true requires prices.raw; default group-resolved
symbol_idsinteger[]requiredsymbol ids from GET /trader/v1/symbols or /admin/v1/market/symbols
ts
const socket = trader.socket()
await socket.connect()
socket.unwatch([7])json
{
"symbol_ids": [
7
],
"type": "unwatch"
}unwatch_raw
SEND
{ "type": "unwatch_raw" }Stop raw prices of these symbols.
Fields
rawbooleanstaff only: true requires prices.raw; default group-resolved
symbol_idsinteger[]requiredsymbol ids from GET /trader/v1/symbols or /admin/v1/market/symbols
Server notes
Removes only the raw audience's subscriptions and pending prices; binary watch subscriptions are preserved.
ts
const socket = staff.socket()
await socket.connect()
socket.send({
symbol_ids: [
1
],
type: "unwatch_raw"
})json
{
"symbol_ids": [
1
],
"type": "unwatch_raw"
}watch
SEND
{ "type": "watch" }Start receiving prices of these symbols. The latest price of each arrives at once, then every change.
Fields
rawbooleanstaff only: true requires prices.raw; default group-resolved
symbol_idsinteger[]requiredsymbol ids from GET /trader/v1/symbols or /admin/v1/market/symbols
Server notes
Adds symbols (at most 1000 per socket; more are ignored). The latest price of each arrives at once in one binary frame, then the latest book of each symbol with depth on; afterwards every change. Traders only get symbols their group trades, with the group's markup. Staff also default to group-resolved prices; raw:true requires prices.raw.
ts
const socket = trader.socket()
await socket.connect()
socket.watch([1,2,7]) // kept across reconnectsjson
{
"symbol_ids": [
1,
2,
7
],
"type": "watch"
}watch_raw
SEND
{ "type": "watch_raw" }Staff with prices.raw: receive raw feed prices (before markup) of these symbols.
Fields
rawbooleanstaff only: true requires prices.raw; default group-resolved
symbol_idsinteger[]requiredsymbol ids from GET /trader/v1/symbols or /admin/v1/market/symbols
Server notes
Requires prices.raw and an admitted staff presentation. Advertised by raw_prices_v1. Adds active base symbols to the independent raw_prices audience; the latest quotes arrive immediately, then each update as soon as it arrives (latest per symbol while the socket is backed up). Does not change binary watch pricing or depth. Raw and legacy subscriptions together use at most 1000 entries per socket (a symbol watched in both audiences uses two). Excess or unavailable symbols are ignored. Send large watch lists in frames within the documented byte limit. Re-send subscriptions after reconnect.
ts
const socket = staff.socket()
await socket.connect()
socket.send({
symbol_ids: [
1,
2
],
type: "watch_raw"
})json
{
"symbol_ids": [
1,
2
],
"type": "watch_raw"
}