An end-to-end tutorial combining the WebSocket API and Market Data endpoints to build a live price dashboard.
Live dashboards are where REST meets push: you use Market Data endpoints once to resolve instrument metadata, then keep the UI fresh with a WebSocket subscription to quote or trade topics. This tutorial shows a small Node.js service that connects to the eToro streaming API, subscribes to a handful of instrument IDs, reconnects cleanly after network blips, and merges streaming ticks with REST-backed labels for a simple console or web UI.
The REST base URL for examples is https://public-api.etoro.com/api/v1/. Streaming uses a separate WebSocket origin at wss://ws.etoro.com/ws—refer to the API Portal for the latest connection details.
Whether you call REST or open a WebSocket, reuse the same API identity: x-api-key, and when required x-user-key. Generate a fresh x-request-id per HTTP request; for WebSockets, send an identifying header or query parameter as described in your portal’s auth section.
import { randomUUID } from "node:crypto";
import WebSocket from "ws";
const REST = "https://public-api.etoro.com/api/v1";
const WS_URL = "wss://ws.etoro.com/ws";
function restHeaders() {
return {
"x-api-key": process.env.ETORO_API_KEY,
"x-user-key": process.env.ETORO_USER_KEY ?? "",
"x-request-id": randomUUID(),
accept: "application/json",
};
}
Before displaying prices, fetch human-readable names, precision, and exchange session rules so your UI formats decimals correctly and shows bid/ask labels consistently.
async function loadInstrumentMeta(instrumentId) {
const res = await fetch(`${REST}/market-data/instruments/${instrumentId}`, {
headers: restHeaders(),
});
if (!res.ok) throw new Error(`instrument ${instrumentId}: ${res.status}`);
const { data } = await res.json();
return {
id: instrumentId,
symbol: data.symbol,
name: data.displayName,
pipSize: data.pipSize ?? 0.0001,
};
}
export async function loadWatchlist(ids) {
return Promise.all(ids.map(loadInstrumentMeta));
}
You can batch multiple IDs if your portal exposes a bulk instruments route; the pattern stays the same—one REST round-trip, then cache in memory keyed by instrument ID.
Open the socket with the same API key headers your portal specifies (some deployments use subprotocols or a short-lived ticket in the query string). After open, send a subscribe message listing topics such as quotes.{instrumentId} or trades.{instrumentId}.
function connectSocket({ instrumentIds, onTick }) {
const socket = new WebSocket(WS_URL, {
headers: {
"x-api-key": process.env.ETORO_API_KEY,
"x-user-key": process.env.ETORO_USER_KEY ?? "",
"x-request-id": randomUUID(),
},
});
socket.on("open", () => {
socket.send(
JSON.stringify({
type: "subscribe",
channels: instrumentIds.map((id) => `quotes.${id}`),
})
);
});
socket.on("message", (data) => {
const msg = JSON.parse(data.toString());
if (msg.type === "quote") onTick(msg.instrumentId, msg.bid, msg.ask, msg.ts);
});
return socket;
}
Networks drop. Wrap the client in a small reconnect loop: on close or error, wait with exponential backoff (1s, 2s, 4s, cap at 30s), then instantiate a new WebSocket. Persist the latest known quotes so the UI does not flash empty during reconnect, and resend the subscribe payload on every successful open.
export function resilientStream(instrumentIds, onTick) {
let attempt = 0;
let socket;
const connect = () => {
socket = connectSocket({ instrumentIds, onTick });
socket.on("close", scheduleReconnect);
socket.on("error", () => socket.close());
};
const scheduleReconnect = () => {
const delay = Math.min(30000, 1000 * 2 ** attempt++);
setTimeout(() => {
connect();
}, delay);
};
connect();
return () => socket?.close();
}
The dashboard model merges static metadata with the latest tick. Below, a tiny in-memory store suitable for pushing to a web client via Server-Sent Events or WebSocket fan-out from your own server.
const state = new Map();
export function createDashboard(instrumentIds) {
const metaPromise = loadWatchlist(instrumentIds);
const stop = resilientStream(instrumentIds, (id, bid, ask, ts) => {
const row = state.get(id) ?? {};
state.set(id, { ...row, bid, ask, ts });
});
return {
async snapshot() {
const meta = await metaPromise;
for (const m of meta) {
const live = state.get(m.id) ?? {};
state.set(m.id, { ...m, ...live });
}
return [...state.values()];
},
dispose: stop,
};
}
For a browser UI, expose snapshot() on an interval for a table grid, or push diffs when onTick fires. Add staleness warnings if Date.now() - ts exceeds a few seconds, and fall back to the last REST mid price if the stream is quiet during off-hours.
You now have a repeatable pattern: REST for reference data, WebSocket for ticks, reconnect logic for resilience. Extend it with order-book depth channels or authenticated user streams when you graduate from read-only market data.
Was this helpful?
A step-by-step guide to building a simple trading bot using the eToro Demo Trading API.
How to programmatically search, filter, and explore eToro's instrument catalog — asset classes, exchanges, industries, and historical data.
Be the first to know when we publish new API guides, changelog entries, and builder resources.
Newsletter coming soon. We'll only email you when it launches.