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, WebApiKey and a Secret) - provided by the operator.
  • The endpoint URL: wss://feed.finaquotes.com/feed (secure WebSocket only).
  • Any standard WebSocket client in your language of choice.
Token Credentials are issued by the operator. Keep the 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:

  1. Open a WebSocket connection to wss://feed.finaquotes.com/feed.
  2. Send a Login request with an HMAC signature.
  3. Receive Login → Authenticated: true, then a SessionInfo message arrives automatically.
  4. Send FeedSubscribe with the symbols you want. You get an immediate snapshot of the last prices.
  5. 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.

Skip ahead to the Full client example for a copy-paste runnable client in Node.js and Python that already does all of this, including heartbeat and reconnect.

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

json
{
  "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:

  1. 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.
  2. Build the message string by concatenating, in this exact order, with no separators:
    message = Timestamp + Id + WebApiKey
    where Timestamp is the millisecond value as a string, Id is your request id, and WebApiKey is your key.
  3. Sign the message with HMAC-SHA256 using your Secret as the key.
  4. Base64-encode the raw HMAC digest. That string is the Signature.
Formula Signature = Base64( HMAC_SHA256( Timestamp + Id + WebApiKey, key = Secret ) )

Example calculation

For these inputs:

Timestamp1700000000000
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:

signature
dwVxvaQwOI831BmW7OVKxcta3Q4WtYAIYOp+lTTV8u8=

Use this vector to verify your own implementation before going live.

Reference implementations

javascript
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=
python
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:

json
{ "Id": "1", "Response": "Login", "Result": { "Authenticated": true } }

On failure the server returns an error and closes the connection:

json
{ "Id": "1", "Response": "Error", "Error": { "Code": "login_failed", "Message": "Authentication failed" } }
Rate limiting Failed login attempts are rate-limited to prevent brute force. Do not loop Login on a signature error - fix the parameters first (see Troubleshooting).

Connect

Connect to the endpoint over a secure WebSocket. Plain ws:// is not accepted.

EndpointTransport
wss://feed.finaquotes.com/feedWebSocket (wss only)

The connection lifecycle is:

  1. open - the WebSocket connection is established.
  2. send Login - your first frame should be the Login request.
  3. SessionInfo arrives - right after a successful Login, the server pushes a SessionInfo message on its own. You can also request it any time with {"Request":"SessionInfo"}.

SessionInfo

json
{
  "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

json
{ "Id": "2", "Request": "Symbols" }

Request - a single symbol

json
{ "Request": "Symbols", "Params": { "Symbol": "AAPL" } }

Response

json
{
  "Id": "2",
  "Response": "Symbols",
  "Result": {
    "Symbols": [
      {
        "Symbol": "AAPL",
        "Precision": 2,
        "Description": "Apple Inc",
        "ContractSize": 1,
        "MarginCurrency": "USD",
        "ProfitCurrency": "USD",
        "TradeAmountStep": 1,
        "MinTradeAmount": 1
      }
    ]
  }
}

Fields

FieldTypeMeaning
SymbolstringThe ticker, e.g. AAPL.
PrecisionintNumber of decimal places in the price (cents = 2).
DescriptionstringHuman-readable instrument name.
ContractSizenumberNominal default.
MarginCurrencystringNominal default.
ProfitCurrencystringNominal default.
TradeAmountStepnumberNominal default.
MinTradeAmountnumberNominal default.
The 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

json
{
  "Id": "3",
  "Request": "FeedSubscribe",
  "Params": {
    "Subscribe": [ { "Symbol": "AAPL" }, { "Symbol": "MSFT" } ]
  }
}

Response - snapshot + fails

json
{
  "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 its Timestamp may be old; no FeedTick follows until the market opens.
  • Fails - symbols from your request that were not recognized.
You may include a 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:

json
{
  "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:

  • FeedTick for 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 FeedSubscribe returns the latest quote including the extended session, and SessionOpen is true during 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.

json
{ "Id": "4", "Request": "FeedUnsubscribe", "Params": { "Unsubscribe": [ "AAPL" ] } }
json
{ "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.Price and BestAsk.Price carry 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, Volume carries the actual size of the level in units of the instrument (shares). Where sizes are not available (forex, indices and others), Volume is 0.
  • Single-price instruments. Where the market provides no quote data (crypto, indices, commodities, bonds), both sides carry the same last price - bid = ask = last - and Volume is 0. 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 BookDepth in a subscription is ignored.
A mid price can always be derived as the average of 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 Symbols request 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 example BRK.B becomes BRKB.

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:

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.

request
curl -H "x-history-token: YOUR_HISTORY_TOKEN" \
  "https://feed.finaquotes.com/api/v1/instruments?category=forex"
json
{
  "count": 70,
  "instruments": [
    {
      "symbol": "AUDCAD",
      "name": "Australian Dollar / Canadian Dollar",
      "category": "forex",
      "currency": "CAD",
      "digits": 5,
      "session": "forex_24_5"
    }
  ]
}

Fields

FieldTypeMeaning
symbolstringThe ticker as used by the feed, e.g. AAPL, EURUSD, BTCUSDT.
namestringHuman-readable instrument name.
categorystringAsset class: crypto, stocks, forex, metals, commodities, indices or bonds.
currencystringQuote currency of the price.
digitsintNumber of decimal places in the price - matches Precision in the feed.
sessionstringTrading session type, e.g. 24_7, nasdaq, forex_24_5.
Without 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:

json
{
  "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 }
  ]
}
The counts grow as the instrument list expands - treat the numbers above as a point-in-time example, not a contract.

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

request
curl -H "x-history-token: YOUR_HISTORY_TOKEN" \
  "https://feed.finaquotes.com/api/v1/candles?symbol=AAPL&tf=1H&limit=2"
json
{
  "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.

DirectionShape
request{ "Id", "Request", "Params" }
response{ "Id", "Response", "Result" }
error{ "Id", "Response": "Error", "Error": { "Code", "Message" } }
notification{ "Response", "Result" }  (no Id)

Login

request
{ "Id": "1", "Request": "Login", "Params": { "AuthType": "HMAC", "WebApiId": "YOUR_WEB_API_ID", "WebApiKey": "YOUR_WEB_API_KEY", "Timestamp": 1700000000000, "Signature": "<base64>" } }
response
{ "Id": "1", "Response": "Login", "Result": { "Authenticated": true } }

SessionInfo

request (optional)
{ "Request": "SessionInfo" }
response
{ "Response": "SessionInfo", "Result": { "PlatformName": "FinaQuotes Feed", "PlatformCompany": "FinaQuotes", "PlatformTimezoneOffset": 0, "SessionId": "<uuid>", "SessionStatus": "Opened", "SessionStartTime": 1700000000000 } }

Symbols

request
{ "Id": "2", "Request": "Symbols" }
response
{ "Id": "2", "Response": "Symbols", "Result": { "Symbols": [ { "Symbol": "AAPL", "Precision": 2, "Description": "Apple Inc", "ContractSize": 1, "MarginCurrency": "USD", "ProfitCurrency": "USD", "TradeAmountStep": 1, "MinTradeAmount": 1 } ] } }

FeedSubscribe

request
{ "Id": "3", "Request": "FeedSubscribe", "Params": { "Subscribe": [ { "Symbol": "AAPL" }, { "Symbol": "MSFT" } ] } }
response
{ "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)

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

request
{ "Id": "4", "Request": "FeedUnsubscribe", "Params": { "Unsubscribe": [ "AAPL" ] } }
response
{ "Id": "4", "Response": "FeedUnsubscribe", "Result": { "Symbols": [ "MSFT" ] } }

Ping / Pong

request
{ "Request": "Ping" }
response
{ "Response": "Pong" }

Error

response
{ "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.

Requires the ws package: npm install ws. The crypto module is built in.
client.js
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();
Requires websocket-client: pip install websocket-client. The hmac, hashlib and base64 modules are built in.
client.py
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 + WebApiKey in exactly that order, concatenated with no separators, Timestamp as a string.
  • The HMAC key is your Secret - not the WebApiKey.
  • 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.