Skip to content

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 ​

  1. Sign in over REST (staff.auth.login / trader.auth.login).
  2. Connect. The gateway answers with a welcome frame.
  3. Watch symbols for prices. Their latest prices arrive at once, then every change.
  4. Listen to the events you care about: account trade, money, CRM client_created, and so on. Which frames you get depends on your session, rights and group scope.
  5. 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 · staff
Admin and manager web. The session cookie signs it; the page must be served from an allowed origin.
wss://<gateway>/trader/v1/wsbrowser · trader
Trader web. The session cookie signs it.
wss://<gateway>/wsapps · servers
Desktop, 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 1008
Signed out, kicked or signed in elsewhere. Do not reconnect: sign in again. The SDK stops here.
session expiredclose 1008
The access token ran out. Refresh it, then reconnect. The SDK does both.
access changedclose 1008
Rights or group scope changed. Reconnect at once and reload what the user may see.
too slow to keep upclose 1008
The client read too slowly. Reconnect and reload.
too many open connections for this loginclose 1008
More than 8 sockets for one login. Close unused tabs.
live stream resync requiredclose 1008
The 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 ​

RECEIVEsocket.on("error")

Your last message was refused; code says why (e.g. unknown_message, rate_limited). The socket stays open.

Who receives itEveryone
Fields
codeenumrequired
too_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 unavailable
ts
const socket = trader.socket()

socket.on("error", (frame) => {
  console.warn("refused:", frame.code)
})

await socket.connect()
json
{
  "code": "unknown_message",
  "type": "error"
}

pong ​

RECEIVEsocket.on("pong")

The answer to your ping. Optional: browsers already answer the gateway's own pings.

Who receives itEveryone
Fields
dataany
Server 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 ​

RECEIVEsocket.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
loginintegerrequired
the signed-in login Login is the numeric identity of every account, trader or staff.
staffbooleanrequired
a 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 ​

RECEIVEsocket.on("depth")

The order book (market depth) of a watched symbol whose depth setting is on.

Who receives itTrader · Staff
Fields
asksdepthLevel[]required
bidsdepthLevel[]required
symbol_idintegerrequired
timestringrequired
UTC 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 ​

RECEIVEsocket.on("prices")binary

Bid 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.

offsetsizefield
01layout version, 1
11flags: bit 0 = the price jumped (gap), bit 1 = thrown in by staff (manual)
21digits: prices are whole points, price = points / 10^digits
31reserved
44symbol id, uint32
88time, int64 unix nanoseconds, UTC
168bid in points, int64
248ask in points, int64
328last deal price in points, int64 (0 = none)
404datafeed id, uint32 (0 = thrown in by staff)
444reserved

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 ​

RECEIVEsocket.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[]required
exact 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 ​

RECEIVEsocket.on("alert_triggered")

One of the trader's price alerts crossed its level.

Who receives itTrader (own account)
Fields
groupstring
The account's group; staff frames only.
loginintegerrequired
The account the event is about.
data.atstringrequired
UTC time, RFC 3339.
data.commentstringrequired
data.conditionstringrequired
data.idintegerrequired
data.levelstringrequired
Exact decimal as a string. Never parse it into a binary float for money or prices.
data.modestringrequired
data.pricestringrequired
Exact decimal as a string. Never parse it into a binary float for money or prices.
data.sidestringrequired
data.symbolstringrequired
Server 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 ​

RECEIVEsocket.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
groupstring
The account's group; staff frames only.
loginintegerrequired
The account the event is about.
data.closedinteger[]
position ids that no longer exist
data.command_hashstring
data.dealsDeal[]
data.deals_deletedinteger[]
data.deals_editedDeal[]
Corrections by staff: deals rewritten in place and deals removed from history.
data.groupstringrequired
data.loginintegerrequired
Login is the numeric identity of every account, trader or staff.
data.margin_checkany
rejected proposal evidence, not applied account state
data.moneyany
data.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.quietboolean
bookkeeping 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 ​

RECEIVEsocket.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
groupstring
The account's group; staff frames only.
loginintegerrequired
The account the event is about.
data.closedinteger[]
position ids that no longer exist
data.command_hashstring
data.dealsDeal[]
data.deals_deletedinteger[]
data.deals_editedDeal[]
Corrections by staff: deals rewritten in place and deals removed from history.
data.groupstringrequired
data.loginintegerrequired
Login is the numeric identity of every account, trader or staff.
data.margin_checkany
rejected proposal evidence, not applied account state
data.moneyany
data.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.quietboolean
bookkeeping 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 ​

RECEIVEsocket.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
groupstring
The account's group; staff frames only.
loginintegerrequired
The account the event is about.
data.closedinteger[]
position ids that no longer exist
data.command_hashstring
data.dealsDeal[]
data.deals_deletedinteger[]
data.deals_editedDeal[]
Corrections by staff: deals rewritten in place and deals removed from history.
data.groupstringrequired
data.loginintegerrequired
Login is the numeric identity of every account, trader or staff.
data.margin_checkany
rejected proposal evidence, not applied account state
data.moneyany
data.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.quietboolean
bookkeeping 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 ​

RECEIVEsocket.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
groupstring
The account's group; staff frames only.
loginintegerrequired
The account the event is about.
data.closedinteger[]
position ids that no longer exist
data.command_hashstring
data.dealsDeal[]
data.deals_deletedinteger[]
data.deals_editedDeal[]
Corrections by staff: deals rewritten in place and deals removed from history.
data.groupstringrequired
data.loginintegerrequired
Login is the numeric identity of every account, trader or staff.
data.margin_checkany
rejected proposal evidence, not applied account state
data.moneyany
data.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.quietboolean
bookkeeping 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 ​

RECEIVEsocket.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
groupstring
The account's group; staff frames only.
loginintegerrequired
The account the event is about.
data.closedinteger[]
position ids that no longer exist
data.command_hashstring
data.dealsDeal[]
data.deals_deletedinteger[]
data.deals_editedDeal[]
Corrections by staff: deals rewritten in place and deals removed from history.
data.groupstringrequired
data.loginintegerrequired
Login is the numeric identity of every account, trader or staff.
data.margin_checkany
rejected proposal evidence, not applied account state
data.moneyany
data.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.quietboolean
bookkeeping 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 ​

RECEIVEsocket.on("notification")

A message to show in the terminal. A stored copy is at trader.notifications.list().

Who receives itTrader (own account)
Fields
groupstring
The account's group; staff frames only.
loginintegerrequired
The account the event is about.
data.companystringrequired
the signature: the account group's company
data.created_atstringrequired
UTC time, RFC 3339.
data.idintegerrequired
data.loginintegerrequired
Login is the numeric identity of every account, trader or staff.
data.messagestringrequired
data.senderintegerrequired
Login 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 ​

RECEIVEsocket.on("order")

A pending order was placed, changed, cancelled or expired.

Who receives itTrader (own account) · Staff (in scope)
Fields
groupstring
The account's group; staff frames only.
loginintegerrequired
The account the event is about.
data.closedinteger[]
position ids that no longer exist
data.command_hashstring
data.dealsDeal[]
data.deals_deletedinteger[]
data.deals_editedDeal[]
Corrections by staff: deals rewritten in place and deals removed from history.
data.groupstringrequired
data.loginintegerrequired
Login is the numeric identity of every account, trader or staff.
data.margin_checkany
rejected proposal evidence, not applied account state
data.moneyany
data.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.quietboolean
bookkeeping 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 ​

RECEIVEsocket.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
groupstring
The account's group; staff frames only.
loginintegerrequired
The account the event is about.
data.closedinteger[]
position ids that no longer exist
data.command_hashstring
data.dealsDeal[]
data.deals_deletedinteger[]
data.deals_editedDeal[]
Corrections by staff: deals rewritten in place and deals removed from history.
data.groupstringrequired
data.loginintegerrequired
Login is the numeric identity of every account, trader or staff.
data.margin_checkany
rejected proposal evidence, not applied account state
data.moneyany
data.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.quietboolean
bookkeeping 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 ​

RECEIVEsocket.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
groupstring
The account's group; staff frames only.
loginintegerrequired
The account the event is about.
data.timestringrequired
UTC 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 ​

RECEIVEsocket.on("session_connected")

A terminal of this account signed in.

Who receives itTrader (own account)
Fields
groupstring
The account's group; staff frames only.
loginintegerrequired
The account the event is about.
data.ipstringrequired
data.sessionstringrequired
data.staffbooleanrequired
data.terminalTerminalrequired
Terminal 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 ​

RECEIVEsocket.on("session_disconnected")

A terminal of this account disconnected (sign-out, kick, expiry or network drop).

Who receives itTrader (own account)
Fields
groupstring
The account's group; staff frames only.
loginintegerrequired
The account the event is about.
data.ipstringrequired
data.sessionstringrequired
data.staffbooleanrequired
data.terminalTerminalrequired
Terminal 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 ​

RECEIVEsocket.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
groupstring
The account's group; staff frames only.
loginintegerrequired
The account the event is about.
data.closedinteger[]
position ids that no longer exist
data.command_hashstring
data.dealsDeal[]
data.deals_deletedinteger[]
data.deals_editedDeal[]
Corrections by staff: deals rewritten in place and deals removed from history.
data.groupstringrequired
data.loginintegerrequired
Login is the numeric identity of every account, trader or staff.
data.margin_checkany
rejected proposal evidence, not applied account state
data.moneyany
data.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.quietboolean
bookkeeping 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 ​

RECEIVEsocket.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_atstringrequired
UTC time, RFC 3339.
data.approved_byintegerrequired
Login is the numeric identity of every account, trader or staff.
data.assigned_managerintegerrequired
0 = none Login is the numeric identity of every account, trader or staff.
data.citystringrequired
data.commentstringrequired
data.company_namestringrequired
data.countrystringrequired
data.created_atstringrequired
UTC time, RFC 3339.
data.created_byintegerrequired
Login is the numeric identity of every account, trader or staff.
data.detailsDetailsrequired
Details is the rest of the profile. Dates are YYYY-MM-DD, amounts are decimals.
data.emailstringrequired
data.first_namestringrequired
data.idintegerrequired
data.kyc_statusstringrequired
Server 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 ​

RECEIVEsocket.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_atstringrequired
UTC time, RFC 3339.
data.approved_byintegerrequired
Login is the numeric identity of every account, trader or staff.
data.assigned_managerintegerrequired
0 = none Login is the numeric identity of every account, trader or staff.
data.citystringrequired
data.commentstringrequired
data.company_namestringrequired
data.countrystringrequired
data.created_atstringrequired
UTC time, RFC 3339.
data.created_byintegerrequired
Login is the numeric identity of every account, trader or staff.
data.detailsDetailsrequired
Details is the rest of the profile. Dates are YYYY-MM-DD, amounts are decimals.
data.emailstringrequired
data.first_namestringrequired
data.idintegerrequired
data.kyc_statusstringrequired
Server 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 ​

RECEIVEsocket.on("mail")

A new internal mail message arrived.

Who receives itMailbox owner
Fields
data.attachmentsMailAttachment[]required
data.bodystringrequired
data.created_atstringrequired
UTC time, RFC 3339.
data.draftany
drafts: the addressing as saved
data.folderstringrequired
data.fromintegerrequired
0 = the platform Login is the numeric identity of every account, trader or staff.
data.from_namestringrequired
data.idintegerrequired
data.message_idstringrequired
data.readbooleanrequired
data.recipientsstringrequired
sent copies: who it went to
data.subjectstringrequired
data.thread_countinteger
folder lists: messages of the thread in the folder
data.thread_idstringrequired
shared 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 ​

RECEIVEsocket.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 ​

RECEIVEsocket.on("news")

A news item was published.

Who receives itStaff with news · Traders as their group allows
Fields
data.bodystringrequired
data.categorystringrequired
data.datafeed_idinteger
data.external_idstring
the datafeed's id of the item
data.idintegerrequired
data.published_atstringrequired
UTC time, RFC 3339.
data.sourcestringrequired
data.subjectstringrequired
data.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 ​

RECEIVEsocket.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 ​

RECEIVEsocket.on("report_job")

Progress of a report you started: queued, running, done or failed.

Who receives itStaff who ran the report
Fields
data.attemptsintegerrequired
runs tried
data.errorstringrequired
why it failed, when it did
data.idintegerrequired
the report job
data.rowsintegerrequired
rows written so far
data.statestringrequired
queued, 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 ​

RECEIVEsocket.on("dealing_heartbeat")

The answer to your dealing heartbeat: your dealer connection is still alive.

Who receives itSigned-in dealer
Fields
bodyanyrequired
existing JSON envelope, decimals and pagination unchanged
request_idstringrequired
statusintegerrequired
original 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 ​

RECEIVEsocket.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 ​

RECEIVEsocket.on("desk")

A request in the dealer queue changed: waiting, taken, requoted or done.

Who receives itStaff with dealing (in scope)
Fields
groupstring
The client's group.
logininteger
Always 0 on desk frames: the client's login is in data.request.login.
data.askstringrequired
Exact decimal as a string. Never parse it into a binary float for money or prices.
data.bidstringrequired
requote Exact decimal as a string. Never parse it into a binary float for money or prices.
data.deadlinestringrequired
UTC time, RFC 3339.
data.dealersinteger[]
offer order; empty = any online dealer
data.holderintegerrequired
the dealer it is offered to now Login is the numeric identity of every account, trader or staff.
data.last_actionany
LastAction 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_actorany
authenticated actor of LastAction, absent for server actions
data.queued_atstringrequired
UTC time, RFC 3339.
data.requestTradeRequestrequired
TradeRequest travels gateway -> validation -> routing -> execution. The gateway fills Login, Group and Leverage from the session, never from the client.
data.reservedstringrequired
margin held while it waits Exact decimal as a string. Never parse it into a binary float for money or prices.
data.resultany
when done
data.returnedinteger[]
data.stateDeskStaterequired
DeskState 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 how
data.symbol_idintegerrequired
Server 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 ​

RECEIVEsocket.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.groupstring
accounts/staff changes carry an authorized event-time target group
data.idanyrequired
group path/string account login/numeric client id, or "all" for catalogue invalidation
data.kindstringrequired
accounts, 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 ​

RECEIVEsocket.on("financial_view")

Answers and samples of a bounded positions/orders view you opened with financial_view.

Who receives itStaff (advanced)
Fields
collection_generationstring
collection_sequencestring
collectionsstring[]
dataany[]
execution_epochstring
generationinteger
min_projection_seqstring
modestring
opstringrequired
partinteger
partsinteger
reasonstring
request_idstring
resourcesstring[]
sampled_atstring
sequencestring
statestring
view_idstring
Server 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 ​

RECEIVEsocket.on("journal")

Live server journal lines.

Who receives itStaff with server.logs, after `journal on`
Fields
dataLiveEntry[]required
Server 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 ​

RECEIVEsocket.on("online_equity")

Live equity of an account that is online right now.

Who receives itStaff with accounts.online
Fields
groupstringrequired
loginintegerrequired
Login is the numeric identity of every account, trader or staff.
data.equitystringrequired
Exact decimal as a string. Never parse it into a binary float for money or prices.
data.timestringrequired
UTC time, RFC 3339.
data.versionintegerrequired
Server 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 ​

RECEIVEsocket.on("risk_money")

Live money of accounts for risk screens (without full account access).

Who receives itStaff with risk.view only
Fields
groupstringrequired
loginintegerrequired
Login is the numeric identity of every account, trader or staff.
data.moneyMoneyrequired
Money is an account's live money in its deposit currency.
data.timestringrequired
UTC 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 ​

RECEIVEsocket.on("snapshot")

The answer to a snapshot request: a REST read done over the socket.

Who receives itStaff (advanced)
Fields
bodyanyrequired
existing JSON envelope, decimals and pagination unchanged
request_idstringrequired
statusintegerrequired
original 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 ​

RECEIVEsocket.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_idstringrequired
no 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
generationinteger
idsstring[]
modestring
opstringrequired
request_idstringrequired
resourcestring
view_idstring
Server 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
onbooleanrequired
true 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
pathstringrequired
authorized /admin/v1 JSON GET path and existing filter query
request_idstringrequired
correlation 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
rawboolean
staff only: true requires prices.raw; default group-resolved
symbol_idsinteger[]required
symbol 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
rawboolean
staff only: true requires prices.raw; default group-resolved
symbol_idsinteger[]required
symbol 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
rawboolean
staff only: true requires prices.raw; default group-resolved
symbol_idsinteger[]required
symbol 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 reconnects
json
{
  "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
rawboolean
staff only: true requires prices.raw; default group-resolved
symbol_idsinteger[]required
symbol 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"
}