FinaQuotes Feed: WebSocket API documentation
A real-time market quotes feed delivered over a single WebSocket connection. Authenticate once, subscribe to the instruments you need, and receive a live stream of price ticks.
What it is
The feed is a streaming API: prices arrive as notifications the moment they change, so there is no polling. Quotes are aggregated from multiple institutional and exchange data sources, normalized by FinaQuotes and delivered to you under a single, simple JSON protocol.
What you need to connect
- An access token (
WebApiId,WebApiKeyand aSecret) - provided by the operator. - The endpoint URL:
wss://feed.finaquotes.com/feed(secure WebSocket only). - Any standard WebSocket client in your language of choice.
Secret private - it never leaves your side and is only used to compute request signatures.
Quick start
The minimal path from zero to a live price stream is five steps:
- Open a WebSocket connection to
wss://feed.finaquotes.com/feed. - Send a Login request with an HMAC signature.
- Receive
Login → Authenticated: true, then a SessionInfo message arrives automatically. - Send FeedSubscribe with the symbols you want. You get an immediate snapshot of the last prices.
- Receive FeedTick notifications as prices update.
Every message is JSON. Requests carry an Id you choose; the matching response echoes the same Id. Notifications (like FeedTick) have no Id.
Authentication
Authentication uses an HMAC-SHA256 signature. You never send the Secret over the wire - instead you sign a short message with it, and the server verifies the signature using its copy of your secret.
The Login request
{
"Id": "1",
"Request": "Login",
"Params": {
"AuthType": "HMAC",
"WebApiId": "YOUR_WEB_API_ID",
"WebApiKey": "YOUR_WEB_API_KEY",
"Timestamp": 1700000000000,
"Signature": "<base64 HMAC-SHA256>"
}
}
How to compute the signature
The signature is computed step by step from four values you already have:
- Timestamp - current time in milliseconds since the Unix epoch (ms). It must be within ±60 seconds of the server clock, so keep your machine time synchronized.
- Build the message string by concatenating, in this exact order, with no separators:
message = Timestamp + Id + WebApiKey
whereTimestampis the millisecond value as a string,Idis your request id, andWebApiKeyis your key. - Sign the message with HMAC-SHA256 using your
Secretas the key. - Base64-encode the raw HMAC digest. That string is the
Signature.
Signature = Base64( HMAC_SHA256( Timestamp + Id + WebApiKey, key = Secret ) )Example calculation
For these inputs:
| Timestamp | 1700000000000 |
|---|---|
| Id | "1" |
| WebApiKey | "YOUR_WEB_API_ID_KEY" |
| Secret | "YOUR_SECRET" |
The message string is 17000000000001YOUR_WEB_API_ID_KEY and the resulting signature is:
dwVxvaQwOI831BmW7OVKxcta3Q4WtYAIYOp+lTTV8u8=
Use this vector to verify your own implementation before going live.
Reference implementations
const crypto = require("crypto");
function sign(timestamp, id, webApiKey, secret) {
const message = String(timestamp) + id + webApiKey;
return crypto.createHmac("sha256", secret)
.update(message)
.digest("base64");
}
// Example
const sig = sign(1700000000000, "1", "YOUR_WEB_API_ID_KEY", "YOUR_SECRET");
console.log(sig); // dwVxvaQwOI831BmW7OVKxcta3Q4WtYAIYOp+lTTV8u8=
import hmac, hashlib, base64
def sign(timestamp, id, web_api_key, secret):
message = str(timestamp) + id + web_api_key
digest = hmac.new(secret.encode(), message.encode(), hashlib.sha256).digest()
return base64.b64encode(digest).decode()
# Example
sig = sign(1700000000000, "1", "YOUR_WEB_API_ID_KEY", "YOUR_SECRET")
print(sig) # dwVxvaQwOI831BmW7OVKxcta3Q4WtYAIYOp+lTTV8u8=
Success and failure
On success the server replies with Authenticated: true and then sends SessionInfo automatically:
{ "Id": "1", "Response": "Login", "Result": { "Authenticated": true } }
On failure the server returns an error and closes the connection:
{ "Id": "1", "Response": "Error", "Error": { "Code": "login_failed", "Message": "Authentication failed" } }
Connect
Connect to the endpoint over a secure WebSocket. Plain ws:// is not accepted.
| Endpoint | Transport |
|---|---|
| wss://feed.finaquotes.com/feed | WebSocket (wss only) |
The connection lifecycle is:
- open - the WebSocket connection is established.
- send Login - your first frame should be the Login request.
- SessionInfo arrives - right after a successful Login, the server pushes a
SessionInfomessage on its own. You can also request it any time with{"Request":"SessionInfo"}.
SessionInfo
{
"Response": "SessionInfo",
"Result": {
"PlatformName": "FinaQuotes Feed",
"PlatformCompany": "FinaQuotes",
"PlatformTimezoneOffset": 0,
"SessionId": "550e8400-e29b-41d4-a716-446655440000",
"SessionStatus": "Opened",
"SessionStartTime": 1700000000000
}
}
Get symbols
Use the Symbols request to retrieve the list of available instruments - either all of them, or a single one.
Request - all symbols
{ "Id": "2", "Request": "Symbols" }
Request - a single symbol
{ "Request": "Symbols", "Params": { "Symbol": "AAPL" } }
Response
{
"Id": "2",
"Response": "Symbols",
"Result": {
"Symbols": [
{
"Symbol": "AAPL",
"Precision": 2,
"Description": "Apple Inc",
"ContractSize": 1,
"MarginCurrency": "USD",
"ProfitCurrency": "USD",
"TradeAmountStep": 1,
"MinTradeAmount": 1
}
]
}
}
Fields
| Field | Type | Meaning |
|---|---|---|
| Symbol | string | The ticker, e.g. AAPL. |
| Precision | int | Number of decimal places in the price (cents = 2). |
| Description | string | Human-readable instrument name. |
| ContractSize | number | Nominal default. |
| MarginCurrency | string | Nominal default. |
| ProfitCurrency | string | Nominal default. |
| TradeAmountStep | number | Nominal default. |
| MinTradeAmount | number | Nominal default. |
ContractSize, MarginCurrency, ProfitCurrency, TradeAmountStep and MinTradeAmount fields carry nominal default values. For a quotes-only integration the fields that matter are Symbol, Precision and Description.Subscribe
Subscribe to one or more symbols with FeedSubscribe. The response delivers an immediate snapshot of the current quote for each recognized symbol, plus a list of any that could not be matched. After that, FeedTick notifications stream in as prices change.
Request
{
"Id": "3",
"Request": "FeedSubscribe",
"Params": {
"Subscribe": [ { "Symbol": "AAPL" }, { "Symbol": "MSFT" } ]
}
}
Response - snapshot + fails
{
"Id": "3",
"Response": "FeedSubscribe",
"Result": {
"Snapshot": [
{
"Symbol": "AAPL",
"Timestamp": 1700000000000,
"BestBid": { "Type": "Bid", "Price": 307.36, "Volume": 100 },
"BestAsk": { "Type": "Ask", "Price": 307.42, "Volume": 200 },
"SessionOpen": true
}
],
"Fails": [ "UNKNOWN1" ]
}
}
- Snapshot - the current quote (best bid/ask) for each recognized symbol, returned immediately.
- SessionOpen - whether the instrument's market is trading at the moment of the snapshot. If it is
false, the quote is the last one before the close and itsTimestampmay be old; noFeedTickfollows until the market opens. - Fails - symbols from your request that were not recognized.
BookDepth field in the subscription, but it is ignored - the feed always delivers top-of-book (a single level).FeedTick notifications
For every subscribed symbol, a FeedTick notification is pushed whenever its price changes. These have no Id:
{
"Response": "FeedTick",
"Result": {
"Symbol": "AAPL",
"Timestamp": 1700000000500,
"BestBid": { "Type": "Bid", "Price": 307.47, "Volume": 300 },
"BestAsk": { "Type": "Ask", "Price": 307.53, "Volume": 100 }
}
}
Extended hours (US stocks)
US stocks also trade before and after the regular session. If extended hours are enabled for your account, the feed covers them too:
FeedTickfor US stocks keeps streaming in the pre-market, 04:00-09:30, and in the post-market, 16:00-20:00 New York time (daylight saving time is taken into account). The message format is the same as in the regular session.- The snapshot on
FeedSubscribereturns the latest quote including the extended session, andSessionOpenistrueduring these hours. - Price history for the extended session is available over REST with
session=extended- see GET /api/v1/candles.
If extended hours are disabled for your account, US stocks stream only in the regular session, 09:30-16:00 New York time, and SessionOpen is false outside it. Extended hours apply to US stocks only; other asset classes follow their usual trading hours. To enable or disable extended hours for your account, contact support through your account manager.
Unsubscribe
FeedUnsubscribe removes symbols. The response returns Symbols - the subscriptions that remain active.
{ "Id": "4", "Request": "FeedUnsubscribe", "Params": { "Unsubscribe": [ "AAPL" ] } }
{ "Id": "4", "Response": "FeedUnsubscribe", "Result": { "Symbols": [ "MSFT" ] } }
Price format
Each tick carries the best bid and the best ask - one price level per side. What the fields contain depends on the instrument:
- Real spread where the market quotes one. For stocks, forex and spot metals,
BestBid.PriceandBestAsk.Pricecarry the actual best bid and ask, so the spread between them is real market data. - Real level sizes for equities. For US stocks and a number of international listings,
Volumecarries the actual size of the level in units of the instrument (shares). Where sizes are not available (forex, indices and others),Volumeis0. - Single-price instruments. Where the market provides no quote data (crypto, indices, commodities, bonds), both sides carry the same last price -
bid = ask = last- andVolumeis0. If your application needs a spread for these, apply it yourself on top of the last price. - Top-of-book only. One price level is provided - there is no order book depth. Any
BookDepthin a subscription is ignored.
BestBid.Price and BestAsk.Price; for single-price instruments both sides are equal. Volume: 0 means the size is not available - not an empty level. Timestamp is the price time in milliseconds since the Unix epoch.Symbols list
- Categories. The current universe spans stocks, forex, crypto, metals, commodities, bonds and indices.
- Getting the current list. Always pull the live list with the
Symbolsrequest rather than hardcoding it - see Get symbols. - Automatic expansion. The instrument list grows automatically; new symbols appear on their own without any action on your side.
- Ticker normalization. Tickers are clean (e.g.
AAPL). Tickers that contain a dot are passed without it - for exampleBRK.BbecomesBRKB.
Full instruments list - what we deliver now and the broader catalogue available on request.
REST API
Alongside the WebSocket feed, reference data and price history are available over plain HTTPS. All endpoints live under https://feed.finaquotes.com/api/v1/ and are authenticated with a single header:
x-history-token: YOUR_HISTORY_TOKEN
The token is issued by the operator together with your feed credentials. Requests without a valid token return 404.
Full parameter lists, error codes, rate limits and curl / Python examples are in the REST API reference.
GET /api/v1/instruments
The catalogue of instruments served by the feed - the same set the Symbols request returns over WebSocket. Optional category query parameter filters by asset class; an unknown category returns 400.
curl -H "x-history-token: YOUR_HISTORY_TOKEN" \
"https://feed.finaquotes.com/api/v1/instruments?category=forex"
{
"count": 70,
"instruments": [
{
"symbol": "AUDCAD",
"name": "Australian Dollar / Canadian Dollar",
"category": "forex",
"currency": "CAD",
"digits": 5,
"session": "forex_24_5"
}
]
}
Fields
| Field | Type | Meaning |
|---|---|---|
| symbol | string | The ticker as used by the feed, e.g. AAPL, EURUSD, BTCUSDT. |
| name | string | Human-readable instrument name. |
| category | string | Asset class: crypto, stocks, forex, metals, commodities, indices or bonds. |
| currency | string | Quote currency of the price. |
| digits | int | Number of decimal places in the price - matches Precision in the feed. |
| session | string | Trading session type, e.g. 24_7, nasdaq, forex_24_5. |
category the full list is returned, sorted by category and symbol. There are no prices in the response - live prices come from the feed, history from /api/v1/candles.GET /api/v1/categories
Asset classes with the number of active instruments in each:
{
"count": 7,
"categories": [
{ "category": "crypto", "count": 292 },
{ "category": "stocks", "count": 819 },
{ "category": "forex", "count": 70 },
{ "category": "metals", "count": 19 },
{ "category": "commodities", "count": 38 },
{ "category": "indices", "count": 38 },
{ "category": "bonds", "count": 6 }
]
}
GET /api/v1/candles
OHLCV price history. Parameters: symbol (required), tf - one of 1m, 5m, 15m, 30m, 1H, 4H, 1D, 1W, 1MN, from/to - milliseconds since the Unix epoch (optional), limit - number of bars (default 1000, maximum 5000), session - regular (default) or extended to include the US stock pre-market and post-market (only if extended hours are enabled for your account).
curl -H "x-history-token: YOUR_HISTORY_TOKEN" \
"https://feed.finaquotes.com/api/v1/candles?symbol=AAPL&tf=1H&limit=2"
{
"symbol": "AAPL",
"tf": "1H",
"candles": [
{ "ts": 1782928800000, "open": 293.725, "high": 295.53, "low": 292.7, "close": 294.72, "volume": 4190956, "trades": 43640 },
{ "ts": 1782932400000, "open": 294.73, "high": 295.55, "low": 293.7501, "close": 294.25, "volume": 5363332, "trades": 59130 }
]
}
ts is the bar open time (UTC, ms). A wrong tf or a missing symbol returns 400; an unknown symbol returns 404.
Message reference
All messages share a consistent envelope.
| Direction | Shape |
|---|---|
| request | { "Id", "Request", "Params" } |
| response | { "Id", "Response", "Result" } |
| error | { "Id", "Response": "Error", "Error": { "Code", "Message" } } |
| notification | { "Response", "Result" } (no Id) |
Login
{ "Id": "1", "Request": "Login", "Params": { "AuthType": "HMAC", "WebApiId": "YOUR_WEB_API_ID", "WebApiKey": "YOUR_WEB_API_KEY", "Timestamp": 1700000000000, "Signature": "<base64>" } }{ "Id": "1", "Response": "Login", "Result": { "Authenticated": true } }SessionInfo
{ "Request": "SessionInfo" }{ "Response": "SessionInfo", "Result": { "PlatformName": "FinaQuotes Feed", "PlatformCompany": "FinaQuotes", "PlatformTimezoneOffset": 0, "SessionId": "<uuid>", "SessionStatus": "Opened", "SessionStartTime": 1700000000000 } }Symbols
{ "Id": "2", "Request": "Symbols" }{ "Id": "2", "Response": "Symbols", "Result": { "Symbols": [ { "Symbol": "AAPL", "Precision": 2, "Description": "Apple Inc", "ContractSize": 1, "MarginCurrency": "USD", "ProfitCurrency": "USD", "TradeAmountStep": 1, "MinTradeAmount": 1 } ] } }FeedSubscribe
{ "Id": "3", "Request": "FeedSubscribe", "Params": { "Subscribe": [ { "Symbol": "AAPL" }, { "Symbol": "MSFT" } ] } }{ "Id": "3", "Response": "FeedSubscribe", "Result": { "Snapshot": [ { "Symbol": "AAPL", "Timestamp": 1700000000000, "BestBid": { "Type": "Bid", "Price": 307.36, "Volume": 100 }, "BestAsk": { "Type": "Ask", "Price": 307.42, "Volume": 200 }, "SessionOpen": true } ], "Fails": [ "UNKNOWN1" ] } }FeedTick (notification)
{ "Response": "FeedTick", "Result": { "Symbol": "AAPL", "Timestamp": 1700000000500, "BestBid": { "Type": "Bid", "Price": 307.47, "Volume": 300 }, "BestAsk": { "Type": "Ask", "Price": 307.53, "Volume": 100 } } }FeedUnsubscribe
{ "Id": "4", "Request": "FeedUnsubscribe", "Params": { "Unsubscribe": [ "AAPL" ] } }{ "Id": "4", "Response": "FeedUnsubscribe", "Result": { "Symbols": [ "MSFT" ] } }Ping / Pong
{ "Request": "Ping" }{ "Response": "Pong" }Error
{ "Id": "1", "Response": "Error", "Error": { "Code": "login_failed", "Message": "Authentication failed" } }Rules
One connection per token
A token may hold a single active connection. Opening a new connection with the same token disconnects the previous one.
Heartbeat
- If the server sees no activity from the client for more than 60 seconds, it drops the connection.
- Send a
Ping(or a WebSocket ping frame) periodically - every 20-30 seconds is recommended. - The server also sends a WebSocket ping every 30 seconds.
Reconnect
On disconnect, reconnect with exponential backoff (for example 1s, 2s, 4s … capped at 30s) and perform Login again.
Outside market hours
When the market is closed, subscribing still returns a snapshot (the last known quote), but no FeedTick stream arrives until the market opens. This is expected. For US stocks with extended hours enabled, the pre-market and post-market count as open - see Extended hours.
Login attempts
Failed login attempts are rate-limited (brute-force protection). Do not loop Login on a signature error - verify your parameters first.
Full client example
A complete, copy-paste runnable client. It connects, logs in with a correct HMAC signature, subscribes, prints incoming ticks, sends a Ping every 25 seconds as a heartbeat, and reconnects with exponential backoff.
ws package: npm install ws. The crypto module is built in.const WebSocket = require("ws");
const crypto = require("crypto");
const URL = "wss://feed.finaquotes.com/feed";
const WEB_API_ID = "YOUR_WEB_API_ID";
const WEB_API_KEY = "YOUR_WEB_API_KEY";
const SECRET = "YOUR_SECRET";
const SYMBOLS = ["AAPL", "MSFT"];
let ws;
let pingTimer = null;
let backoff = 1000; // start at 1s, cap at 30s
function sign(timestamp, id, webApiKey, secret) {
const message = String(timestamp) + id + webApiKey;
return crypto.createHmac("sha256", secret).update(message).digest("base64");
}
function send(obj) {
ws.send(JSON.stringify(obj));
}
function login() {
const id = "1";
const ts = Date.now();
const signature = sign(ts, id, WEB_API_KEY, SECRET);
send({
Id: id,
Request: "Login",
Params: {
AuthType: "HMAC",
WebApiId: WEB_API_ID,
WebApiKey: WEB_API_KEY,
Timestamp: ts,
Signature: signature
}
});
}
function subscribe() {
send({
Id: "3",
Request: "FeedSubscribe",
Params: { Subscribe: SYMBOLS.map((s) => ({ Symbol: s })) }
});
}
function startHeartbeat() {
stopHeartbeat();
pingTimer = setInterval(() => send({ Request: "Ping" }), 25000);
}
function stopHeartbeat() {
if (pingTimer) clearInterval(pingTimer);
pingTimer = null;
}
function connect() {
ws = new WebSocket(URL);
ws.on("open", () => {
console.log("connected, logging in");
login();
});
ws.on("message", (data) => {
let msg;
try { msg = JSON.parse(data.toString()); } catch (e) { return; }
if (msg.Response === "Login" && msg.Result && msg.Result.Authenticated) {
console.log("authenticated");
backoff = 1000; // reset backoff on success
startHeartbeat();
subscribe();
} else if (msg.Response === "SessionInfo") {
console.log("session:", msg.Result.SessionId);
} else if (msg.Response === "FeedSubscribe") {
console.log("snapshot:", msg.Result.Snapshot);
if (msg.Result.Fails && msg.Result.Fails.length) {
console.log("unrecognized:", msg.Result.Fails);
}
} else if (msg.Response === "FeedTick") {
const r = msg.Result;
console.log(r.Symbol, r.BestBid.Price, "@", r.Timestamp);
} else if (msg.Response === "Pong") {
// heartbeat acknowledged
} else if (msg.Response === "Error") {
console.error("error:", msg.Error.Code, msg.Error.Message);
}
});
ws.on("close", () => {
console.log("disconnected, reconnecting in", backoff, "ms");
stopHeartbeat();
setTimeout(connect, backoff);
backoff = Math.min(backoff * 2, 30000);
});
ws.on("error", (err) => {
console.error("socket error:", err.message);
// close will fire next and trigger reconnect
});
}
connect();
websocket-client: pip install websocket-client. The hmac, hashlib and base64 modules are built in.import json
import time
import hmac
import hashlib
import base64
import threading
import websocket # pip install websocket-client
URL = "wss://feed.finaquotes.com/feed"
WEB_API_ID = "YOUR_WEB_API_ID"
WEB_API_KEY = "YOUR_WEB_API_KEY"
SECRET = "YOUR_SECRET"
SYMBOLS = ["AAPL", "MSFT"]
backoff = 1.0 # seconds, cap at 30
_ping_stop = None
def sign(timestamp, id, web_api_key, secret):
message = str(timestamp) + id + web_api_key
digest = hmac.new(secret.encode(), message.encode(), hashlib.sha256).digest()
return base64.b64encode(digest).decode()
def send(ws, obj):
ws.send(json.dumps(obj))
def login(ws):
id = "1"
ts = int(time.time() * 1000)
signature = sign(ts, id, WEB_API_KEY, SECRET)
send(ws, {
"Id": id,
"Request": "Login",
"Params": {
"AuthType": "HMAC",
"WebApiId": WEB_API_ID,
"WebApiKey": WEB_API_KEY,
"Timestamp": ts,
"Signature": signature,
},
})
def subscribe(ws):
send(ws, {
"Id": "3",
"Request": "FeedSubscribe",
"Params": {"Subscribe": [{"Symbol": s} for s in SYMBOLS]},
})
def start_heartbeat(ws):
global _ping_stop
_ping_stop = threading.Event()
def loop():
while not _ping_stop.wait(25):
try:
send(ws, {"Request": "Ping"})
except Exception:
break
threading.Thread(target=loop, daemon=True).start()
def stop_heartbeat():
global _ping_stop
if _ping_stop:
_ping_stop.set()
_ping_stop = None
def on_open(ws):
print("connected, logging in")
login(ws)
def on_message(ws, message):
global backoff
try:
msg = json.loads(message)
except ValueError:
return
resp = msg.get("Response")
if resp == "Login" and msg.get("Result", {}).get("Authenticated"):
print("authenticated")
backoff = 1.0 # reset backoff on success
start_heartbeat(ws)
subscribe(ws)
elif resp == "SessionInfo":
print("session:", msg["Result"]["SessionId"])
elif resp == "FeedSubscribe":
print("snapshot:", msg["Result"]["Snapshot"])
fails = msg["Result"].get("Fails")
if fails:
print("unrecognized:", fails)
elif resp == "FeedTick":
r = msg["Result"]
print(r["Symbol"], r["BestBid"]["Price"], "@", r["Timestamp"])
elif resp == "Pong":
pass # heartbeat acknowledged
elif resp == "Error":
print("error:", msg["Error"]["Code"], msg["Error"]["Message"])
def on_close(ws, status, reason):
print("disconnected:", status, reason)
stop_heartbeat()
def run():
global backoff
while True:
ws = websocket.WebSocketApp(
URL,
on_open=on_open,
on_message=on_message,
on_close=on_close,
)
# ping_interval keeps a WS-level heartbeat as well
ws.run_forever(ping_interval=25)
print("reconnecting in", backoff, "s")
time.sleep(backoff)
backoff = min(backoff * 2, 30.0)
if __name__ == "__main__":
run()
FAQ / Troubleshooting
Login fails with "Authentication failed"
Almost always a signature mismatch. Check, in order:
- The message is built as
Timestamp + Id + WebApiKeyin exactly that order, concatenated with no separators,Timestampas a string. - The HMAC key is your
Secret- not theWebApiKey. - The digest is Base64-encoded (not hex).
- Compare against the published test vector - it should produce
dwVxvaQwOI831BmW7OVKxcta3Q4WtYAIYOp+lTTV8u8=.
Login rejected even though the signature looks right
Your Timestamp is probably stale. It must be within ±60 seconds of server time. Synchronize your machine clock (NTP) and generate the timestamp fresh at send time.
The connection drops after ~60 seconds of silence
You are not sending heartbeats. Send a Ping every 20-30 seconds (the examples use 25s). Without activity the server closes idle connections.
I subscribed but no ticks arrive
The market is likely closed. You will still receive the snapshot (the last known quote) on subscription, but FeedTick notifications only flow while the market is open. They resume automatically when it opens.
The connection drops right after a second Login
This is the one-connection-per-token rule. A new connection with the same token disconnects the older one. Make sure you are not running two clients with the same credentials.
A symbol shows up in "Fails"
It was not recognized. Check the spelling and remember ticker normalization: dotted tickers are passed without the dot (e.g. BRK.B → BRKB). Pull the live list with Symbols to confirm the exact name.