Code Examples
Signed request helpers, REST examples, WebSocket subscriptions, and integration patterns.
Disclaimer
The following examples are simplified onboarding examples intended for API exploration and integration learning.
They are NOT production-ready trading bots.
Production systems should additionally implement:
-
reconnect handling
-
rate limit tracking
-
persistence
-
retry policies
-
risk management
-
monitoring
-
security hardening
TypeScript helper example for signed requests
import { loadExampleConfig } from './lib/config.js';
import { loadSignedAuthFromEnv, requestJson } from './lib/http.js';
import { buildSignedHeaders } from './lib/signing.js';
type AccountDetailsResponse = Record<string, unknown>;
const config = loadExampleConfig();
const auth = await loadSignedAuthFromEnv();
const path = `/v1/configuration/accounts/${config.accountId}/account`;
const headers = await buildSignedHeaders({
apiKey: auth.apiKey,
privateKey: auth.privateKey,
method: 'GET',
pathWithQuery: path,
body: '',
idempotencyKey: '',
});
console.log('Signed headers example:');
console.log(JSON.stringify(headers, null, 2));
const account = await requestJson<AccountDetailsResponse>({
baseUrl: config.discoveryBaseUrl,
method: 'GET',
path,
auth,
});
console.log('Account details:');
console.log(JSON.stringify(account, null, 2));
TypeScript: open position and wait for transaction event
import { loadExampleConfig } from './lib/config.js';
import { createIdempotencyKey, loadSignedAuthFromEnv, requestJson, resolveTradingAccessPoint } from './lib/http.js';
import { closeOnSigint, connectSignedWebSocket, subscribeTransactions, waitForJsonMessage } from './lib/ws.js';
type AckResponse = {
status: string;
operation_id?: string;
client_request_id?: string;
};
type TransactionEvent = {
event_type?: string;
client_request_id?: string;
operation_id?: string;
};
const config = loadExampleConfig();
const auth = await loadSignedAuthFromEnv();
const accessPoint = await resolveTradingAccessPoint({
accountId: config.accountId,
discoveryBaseUrl: config.discoveryBaseUrl,
auth,
});
const ws = await connectSignedWebSocket({
baseUrl: config.wsBaseUrl,
path: `/v1/server-events/accounts/${config.accountId}/ws/events`,
auth,
});
closeOnSigint(ws);
subscribeTransactions(ws, 'open-position-transactions');
const idempotencyKey = createIdempotencyKey('open-position');
const ack = await requestJson<AckResponse>({
baseUrl: accessPoint.access_point,
method: 'POST',
path: `/v1/trading/accounts/${config.accountId}/positions`,
auth,
idempotencyKey,
body: {
instrument: config.instrument,
side: config.side,
volume: config.volume,
},
});
console.log('ACK:');
console.log(JSON.stringify(ack, null, 2));
const transactionEvent = await waitForJsonMessage<TransactionEvent>(
ws,
(message) =>
message.event_type === 'transaction_event' &&
(message.client_request_id === idempotencyKey || message.operation_id === ack.operation_id),
config.timeoutMs,
);
console.log('Matched transaction_event:');
console.log(JSON.stringify(transactionEvent, null, 2));
ws.close();
TypeScript: place pending order
import { loadExampleConfig, requireEnv } from './lib/config.js';
import { createIdempotencyKey, loadSignedAuthFromEnv, requestJson, resolveTradingAccessPoint } from './lib/http.js';
type AckResponse = Record<string, unknown>;
const config = loadExampleConfig();
const auth = await loadSignedAuthFromEnv();
const price = requireEnv('EXNESS_LIMIT_PRICE', config.limitPrice);
const accessPoint = await resolveTradingAccessPoint({
accountId: config.accountId,
discoveryBaseUrl: config.discoveryBaseUrl,
auth,
});
const ack = await requestJson<AckResponse>({
baseUrl: accessPoint.access_point,
method: 'POST',
path: `/v1/trading/accounts/${config.accountId}/orders`,
auth,
idempotencyKey: createIdempotencyKey('pending-order'),
body: {
type: 'limit',
instrument: config.instrument,
side: config.side,
volume: config.volume,
price,
},
});
console.log('Pending order ACK:');
console.log(JSON.stringify(ack, null, 2));
TypeScript: helper for signed WebSocket requests
import { randomUUID } from 'node:crypto';
import WebSocket from 'ws';
import { buildSignedHeaders } from './signing.js';
import type { SignedAuth } from './http.js';
export type WsJson = Record<string, unknown>;
export function toWsBaseUrl(baseUrl: string): string {
return baseUrl.replace(/^http:/, 'ws:').replace(/^https:/, 'wss:');
}
export async function connectSignedWebSocket({
baseUrl,
path,
auth,
}: {
baseUrl: string;
path: string;
auth: SignedAuth;
}): Promise<WebSocket> {
const headers = await buildSignedHeaders({
apiKey: auth.apiKey,
privateKey: auth.privateKey,
method: 'GET',
pathWithQuery: path,
body: '',
idempotencyKey: '',
});
const ws = new WebSocket(`${toWsBaseUrl(baseUrl)}${path}`, 'exness-ws-protocol', {
headers,
handshakeTimeout: 10000,
perMessageDeflate: false,
rejectUnauthorized: false,
});
await new Promise<void>((resolve, reject) => {
ws.once('open', () => resolve());
ws.once('error', (error: Error) => reject(error));
});
return ws;
}
export function sendJson(ws: WebSocket, payload: object): void {
ws.send(JSON.stringify(payload));
}
export function closeOnSigint(ws: WebSocket): void {
process.on('SIGINT', () => {
ws.close();
process.exit(0);
});
}
export function logParsedMessages(ws: WebSocket, label = 'ws'): void {
ws.on('message', (raw: WebSocket.RawData) => {
const text = raw.toString();
try {
console.log(`[${label}]`, JSON.stringify(JSON.parse(text)));
} catch {
console.log(`[${label}]`, text);
}
});
}
export function subscribeTransactions(ws: WebSocket, requestId: string = randomUUID()): string {
sendJson(ws, { id: requestId, subscribe: { event: 'transactions' } });
return requestId;
}
export function subscribeAccountState(ws: WebSocket, requestId: string = randomUUID()): string {
sendJson(ws, { id: requestId, subscribe: { event: 'account_state' } });
return requestId;
}
export function subscribeInstruments(ws: WebSocket, instruments: string[] = ['all'], requestId: string = randomUUID()): string {
sendJson(ws, { id: requestId, subscribe: { event: 'instruments', instruments } });
return requestId;
}
export function subscribeHmr(ws: WebSocket, instruments: string[], requestId: string = randomUUID()): string {
sendJson(ws, { id: requestId, subscribe: { event: 'hmr', instruments } });
return requestId;
}
export function subscribeTicks(ws: WebSocket, instruments: string[], requestId: string = randomUUID()): string {
sendJson(ws, { id: requestId, subscribe: { event: 'ticks', instruments } });
return requestId;
}
export function waitForJsonMessage<T extends WsJson>(
ws: WebSocket,
predicate: (message: T) => boolean,
timeoutMs = 30000,
): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => {
cleanup();
reject(new Error(`Timed out after ${timeoutMs} ms`));
}, timeoutMs);
const onMessage = (raw: WebSocket.RawData) => {
const text = raw.toString();
let parsed: T;
try {
parsed = JSON.parse(text) as T;
} catch {
return;
}
if (!predicate(parsed)) {
return;
}
cleanup();
resolve(parsed);
};
const onError = (error: Error) => {
cleanup();
reject(error);
};
function cleanup() {
clearTimeout(timer);
ws.off('message', onMessage);
ws.off('error', onError);
}
ws.on('message', onMessage);
ws.on('error', onError);
});
}
TypeScript: subscribe to live XAUUSD ticks
import type WebSocket from 'ws';
import { loadExampleConfig } from './lib/config.js';
import { loadSignedAuthFromEnv } from './lib/http.js';
import { closeOnSigint, connectSignedWebSocket, subscribeTicks } from './lib/ws.js';
const config = loadExampleConfig();
const auth = await loadSignedAuthFromEnv();
const ws = await connectSignedWebSocket({
baseUrl: config.wsBaseUrl,
path: `/v1/server-events/accounts/${config.accountId}/ws/ticks`,
auth,
});
closeOnSigint(ws);
subscribeTicks(ws, [config.instrument], 'ticks-live');
console.log(`Subscribed to live ticks for ${config.instrument}`);
ws.on('message', (raw: WebSocket.RawData) => {
const text = raw.toString();
try {
const message = JSON.parse(text);
console.log(JSON.stringify(message, null, 2));
} catch {
console.log(text);
}
});
TypeScript: ticks bot
import type WebSocket from 'ws';
import { loadExampleConfig } from './lib/config.js';
import { loadSignedAuthFromEnv } from './lib/http.js';
import { closeOnSigint, connectSignedWebSocket, subscribeTicks } from './lib/ws.js';
type TickMessage = {
instrument?: string;
bid?: number | string;
ask?: number | string;
timestamp?: string;
};
const config = loadExampleConfig();
const auth = await loadSignedAuthFromEnv();
const ws = await connectSignedWebSocket({
baseUrl: config.wsBaseUrl,
path: `/v1/server-events/accounts/${config.accountId}/ws/ticks`,
auth,
});
closeOnSigint(ws);
subscribeTicks(ws, [config.instrument], 'ticks-bot');
let previousMid: number | null = null;
console.log(`Ticks bot started for ${config.instrument}`);
ws.on('message', (raw: WebSocket.RawData) => {
const text = raw.toString();
let message: TickMessage;
try {
message = JSON.parse(text) as TickMessage;
} catch {
return;
}
if (!message.instrument || message.bid === undefined || message.ask === undefined) {
return;
}
const bid = Number(message.bid);
const ask = Number(message.ask);
const mid = (bid + ask) / 2;
const spread = ask - bid;
const direction =
previousMid === null ? 'flat' :
mid > previousMid ? 'up' :
mid < previousMid ? 'down' :
'flat';
previousMid = mid;
console.log(JSON.stringify({
instrument: message.instrument,
timestamp: message.timestamp,
bid,
ask,
mid,
spread,
direction,
}));
});
TypeScript: account equity monitor
import type WebSocket from 'ws';
import { loadExampleConfig } from './lib/config.js';
import { loadSignedAuthFromEnv } from './lib/http.js';
import { closeOnSigint, connectSignedWebSocket, subscribeAccountState } from './lib/ws.js';
type AccountStateEvent = {
event_type?: string;
event_time?: string;
payload?: {
account_state?: {
balance?: string;
equity?: string;
used_margin?: string;
};
};
};
const config = loadExampleConfig();
const auth = await loadSignedAuthFromEnv();
const ws = await connectSignedWebSocket({
baseUrl: config.wsBaseUrl,
path: `/v1/server-events/accounts/${config.accountId}/ws/events`,
auth,
});
closeOnSigint(ws);
subscribeAccountState(ws, 'equity-monitor');
console.log('Account equity monitor started');
ws.on('message', (raw: WebSocket.RawData) => {
const text = raw.toString();
let message: AccountStateEvent;
try {
message = JSON.parse(text) as AccountStateEvent;
} catch {
return;
}
if (message.event_type !== 'account_state_event') {
return;
}
const state = message.payload?.account_state;
console.log(JSON.stringify({
event_time: message.event_time,
balance: state?.balance,
equity: state?.equity,
used_margin: state?.used_margin,
}));
});
TypeScript: get available instruments and subscribe to updates
import type WebSocket from 'ws';
import { loadExampleConfig } from './lib/config.js';
import { loadSignedAuthFromEnv, requestJson } from './lib/http.js';
import { closeOnSigint, connectSignedWebSocket, subscribeInstruments } from './lib/ws.js';
type InstrumentListResponse = {
instruments?: string[];
};
type InstrumentEvent = {
event_type?: string;
};
const config = loadExampleConfig();
const auth = await loadSignedAuthFromEnv();
const instrumentList = await requestJson<InstrumentListResponse>({
baseUrl: config.discoveryBaseUrl,
method: 'GET',
path: `/v1/configuration/accounts/${config.accountId}/instruments`,
auth,
});
console.log('Available instruments:');
console.log(JSON.stringify({
count: instrumentList.instruments?.length ?? 0,
preview: instrumentList.instruments?.slice(0, 20) ?? [],
}, null, 2));
const ws = await connectSignedWebSocket({
baseUrl: config.wsBaseUrl,
path: `/v1/server-events/accounts/${config.accountId}/ws/events`,
auth,
});
closeOnSigint(ws);
subscribeInstruments(ws, ['all'], 'instrument-updates');
console.log('Subscribed to instrument updates');
ws.on('message', (raw: WebSocket.RawData) => {
const text = raw.toString();
let message: InstrumentEvent;
try {
message = JSON.parse(text) as InstrumentEvent;
} catch {
return;
}
if (message.event_type === 'instrument_event') {
console.log(JSON.stringify(message, null, 2));
}
});
TypeScript: Subscribe to HMR
import type WebSocket from 'ws';
import { loadExampleConfig } from './lib/config.js';
import { loadSignedAuthFromEnv } from './lib/http.js';
import { closeOnSigint, connectSignedWebSocket, subscribeHmr } from './lib/ws.js';
type HmrEvent = {
event_type?: string;
};
const config = loadExampleConfig();
const auth = await loadSignedAuthFromEnv();
const instruments = (process.env.EXNESS_HMR_INSTRUMENTS ?? config.instrument)
.split(',')
.map((item: string) => item.trim())
.filter(Boolean);
const ws = await connectSignedWebSocket({
baseUrl: config.wsBaseUrl,
path: `/v1/server-events/accounts/${config.accountId}/ws/events`,
auth,
});
closeOnSigint(ws);
subscribeHmr(ws, instruments, 'hmr-subscribe');
console.log(`Subscribed to HMR for: ${instruments.join(', ')}`);
ws.on('message', (raw: WebSocket.RawData) => {
const text = raw.toString();
let message: HmrEvent;
try {
message = JSON.parse(text) as HmrEvent;
} catch {
return;
}
if (message.event_type === 'hmr_snapshot' || message.event_type === 'hmr_update') {
console.log(JSON.stringify(message, null, 2));
}
});
TypeScript: Subscribe to Transactions
import type WebSocket from 'ws';
import { loadExampleConfig } from './lib/config.js';
import { loadSignedAuthFromEnv } from './lib/http.js';
import { closeOnSigint, connectSignedWebSocket, subscribeTransactions } from './lib/ws.js';
type TradingEvent = {
event_type?: string;
};
const config = loadExampleConfig();
const auth = await loadSignedAuthFromEnv();
const ws = await connectSignedWebSocket({
baseUrl: config.wsBaseUrl,
path: `/v1/server-events/accounts/${config.accountId}/ws/events`,
auth,
});
closeOnSigint(ws);
subscribeTransactions(ws, 'transactions-subscribe');
console.log('Subscribed to transactions');
ws.on('message', (raw: WebSocket.RawData) => {
const text = raw.toString();
let message: TradingEvent;
try {
message = JSON.parse(text) as TradingEvent;
} catch {
return;
}
if (message.event_type === 'trading_state_snapshot' || message.event_type === 'transaction_event') {
console.log(JSON.stringify(message, null, 2));
}
});
TypeScript: Correlate ACK and transaction_event
import { loadExampleConfig, requireEnv } from './lib/config.js';
import { createIdempotencyKey, loadSignedAuthFromEnv, requestJson, resolveTradingAccessPoint } from './lib/http.js';
import { closeOnSigint, connectSignedWebSocket, subscribeTransactions, waitForJsonMessage } from './lib/ws.js';
type AckResponse = {
operation_id?: string;
client_request_id?: string;
};
type TransactionEvent = {
event_type?: string;
client_request_id?: string;
operation_id?: string;
status?: string;
error_code?: number;
error_message?: string;
};
const config = loadExampleConfig();
const auth = await loadSignedAuthFromEnv();
const price = requireEnv('EXNESS_LIMIT_PRICE', config.limitPrice);
const accessPoint = await resolveTradingAccessPoint({
accountId: config.accountId,
discoveryBaseUrl: config.discoveryBaseUrl,
auth,
});
const ws = await connectSignedWebSocket({
baseUrl: config.wsBaseUrl,
path: `/v1/server-events/accounts/${config.accountId}/ws/events`,
auth,
});
closeOnSigint(ws);
subscribeTransactions(ws, 'ack-correlation');
const idempotencyKey = createIdempotencyKey('ack-correlation');
const ack = await requestJson<AckResponse>({
baseUrl: accessPoint.access_point,
method: 'POST',
path: `/v1/trading/accounts/${config.accountId}/orders`,
auth,
idempotencyKey,
body: {
type: 'limit',
instrument: config.instrument,
side: config.side,
volume: config.volume,
price,
},
});
console.log('ACK:');
console.log(JSON.stringify(ack, null, 2));
const transactionEvent = await waitForJsonMessage<TransactionEvent>(
ws,
(message) =>
message.event_type === 'transaction_event' &&
(message.client_request_id === idempotencyKey || message.operation_id === ack.operation_id),
config.timeoutMs,
);
console.log('Correlation result:');
console.log(JSON.stringify({
ack_operation_id: ack.operation_id,
ack_client_request_id: ack.client_request_id,
matched_operation_id: transactionEvent.operation_id,
matched_client_request_id: transactionEvent.client_request_id,
matched_status: transactionEvent.status,
matched_error_code: transactionEvent.error_code,
matched_error_message: transactionEvent.error_message,
}, null, 2));
ws.close();
TypeScript: Poll Operation Status
import { loadExampleConfig, requireEnv } from './lib/config.js';
import { createIdempotencyKey, loadSignedAuthFromEnv, requestJson, resolveTradingAccessPoint } from './lib/http.js';
type AckResponse = {
operation_id?: string;
};
type OperationStatusResponse = {
status: string;
};
function isTerminalStatus(status: string): boolean {
return status === 'confirmed' || status === 'rejected' || status === 'failed';
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const config = loadExampleConfig();
const auth = await loadSignedAuthFromEnv();
const accessPoint = await resolveTradingAccessPoint({
accountId: config.accountId,
discoveryBaseUrl: config.discoveryBaseUrl,
auth,
});
let operationId = process.env.EXNESS_OPERATION_ID;
if (!operationId) {
const price = requireEnv('EXNESS_LIMIT_PRICE', config.limitPrice);
const ack = await requestJson<AckResponse>({
baseUrl: accessPoint.access_point,
method: 'POST',
path: `/v1/trading/accounts/${config.accountId}/orders`,
auth,
idempotencyKey: createIdempotencyKey('poll-operation'),
body: {
type: 'limit',
instrument: config.instrument,
side: config.side,
volume: config.volume,
price,
},
});
operationId = ack.operation_id;
console.log('Created operation from new pending order ACK:');
console.log(JSON.stringify(ack, null, 2));
}
if (!operationId) {
throw new Error('Operation ID is missing');
}
for (;;) {
const status = await requestJson<OperationStatusResponse & Record<string, unknown>>({
baseUrl: accessPoint.access_point,
method: 'GET',
path: `/v1/trading/accounts/${config.accountId}/operations/${operationId}`,
auth,
});
console.log(JSON.stringify(status, null, 2));
if (isTerminalStatus(status.status)) {
break;
}
await sleep(1000);
}
TypeScript: Get Rate Limits
import { loadExampleConfig } from './lib/config.js';
import { loadSignedAuthFromEnv, requestJson } from './lib/http.js';
type LimitsResponse = Record<string, unknown>;
const config = loadExampleConfig();
const auth = await loadSignedAuthFromEnv();
const limits = await requestJson<LimitsResponse>({
baseUrl: config.discoveryBaseUrl,
method: 'GET',
path: `/v1/configuration/accounts/${config.accountId}/limits`,
auth,
});
console.log('API limits:');
console.log(JSON.stringify(limits, null, 2));