Blog
AIWebSocketsTrading systemsTypeScriptNode.js

Node.js Exchange API Request Signing: 2026 Guide

If you have ever spent a day chasing a "signature invalid" error while hashing HMAC payloads for Bybit or OKX, you already know the problem.

Siebly.io12 min readMarkdown

Overview

If you have ever spent a day chasing a "signature invalid" error while hashing HMAC payloads for Bybit or OKX, you already know the problem. Exchange request signing is unforgiving. Query-string order, timestamp format, encoding (hex vs base64), and clock drift all have to match the exchange's spec exactly. Most of that time is not trading logic. It is debugging nonce rules, recvWindow windows, and docs that disagree with each other.

Building this from scratch is usually a distraction. This guide shows how to handle request authentication with Siebly.io Node.js SDKs, which sit as the implementation layer for JavaScript and TypeScript. The samples below come from the real SDK examples: binance, bybit-api, and @siebly/kraken-api. You will see REST setup, timestamp handling, and awaitable WebSocket API calls. The SDKs sign requests and persist sockets. Rate limits and strategy logic stay yours.

Key Takeaways

  • Manual signing fails most often on payload serialization, encoding (hex vs base64), and timestamps that fall outside the exchange's validity window.
  • A specialized Exchange API request signing Node.js library from Siebly.io removes that boilerplate. HMAC-SHA256 is the common case. Binance and Coinbase also support Ed25519. Bybit V5 uses HMAC-SHA256 or RSA-SHA256, not Ed25519.
  • Use least-privilege API keys, keep secrets out of source, and disable withdrawals on automation keys.
  • Awaitable WebSocket API clients (WebsocketAPIClient) give you request/response order placement without wiring event listeners by hand.
  • Siebly SDKs do not throttle for you. You still own rate limiting. Binance tracks weight headers. Bybit can parse rate-limit headers if you turn that on.

The Challenge of Manual Request Signing in Node.js

Manual request signing is the most common failure point in custom trading clients. A missing query param, a JSON body with extra spaces, or a timestamp outside the allowed window gets you a rejected request. Teams that care about uptime treat an Exchange API request signing Node.js library as the default, not as optional polish.

Cryptographic Primitives and Exchange Standards

Node's crypto module can do the hashes. Applying them the way each exchange expects is the hard part. Most venues use HMAC-SHA256. Encoding is not universal:

  • Binance HMAC signatures are lowercase hex. RSA and Ed25519 signatures are base64.
  • Bybit HMAC signatures are lowercase hex. RSA signatures are base64.
  • OKX HMAC signatures are base64. The prehash string is timestamp + method + requestPath + body.
  • Gate.io HMAC signatures are hex.
  • Kraken Spot signs with HMAC-SHA512 over a SHA-256 digest, using a base64-encoded secret.

Ed25519 is not a Bybit V5 option. Bybit documents HMAC-SHA256 and RSA-SHA256 only, and bybit-api matches that. Binance supports HMAC, RSA, and Ed25519. Pass a PEM private key as api_secret and the SDK detects RSA vs Ed25519 from the key material. Coinbase Advanced Trade uses JWT keys (ECDSA or Ed25519), not HMAC.

Doing this by hand across Binance, Kraken, and Coinbase is where the debt piles up.

The Complexity of Payload Serialization

Signature mismatches usually come from the pre-signed string, not from the hash function. Each exchange concatenates method, path, timestamp, and body in its own order. Some require sorted query params. If you parse JSON, then JSON.stringify it again, you can change spacing or key order and invalidate the hash.

POST bodies are worse. You must sign the exact bytes you send. Bybit V5 is a good example: GET signs timestamp + apiKey + recv_window + queryString. POST signs timestamp + apiKey + recv_window + jsonBodyString. Re-serializing that JSON with different separators breaks the signature.

Maintenance and Architectural Integrity

When an exchange changes versions, signing often changes with it. Bybit's move from V3 to V5 is the usual example. A maintained Exchange API request signing Node.js library absorbs that. Clock skew still matters: Binance and Bybit reject requests when your timestamp plus recvWindow no longer covers server time. Default recvWindow is 5000ms on both. A 1ms drift is not enough to fail. A drift larger than the window is.

Siebly SDKs such as bybit-api sign requests and can apply a time offset. Automatic periodic time sync exists on some clients, but it is off by default. Bybit's enable_time_sync is documented as not recommended, because one slow time call can skew later requests. None of this is rate limiting. Throttling stays your problem.

Engineering a Robust Authentication Layer

A correct hash is not enough. Store keys in environment variables or a secret manager. Use least-privilege keys with withdrawals disabled. If you see repeated auth failures, stop. Some venues lock the key or ban the IP.

Managing API Nonces and Replay Attacks

A nonce (or equivalent timestamp) makes each signed request unique so a captured call cannot be replayed.

This is not the same on every venue:

  • Kraken Spot uses an incrementing nonce. @siebly/kraken-api generates one per private call (Date.now(), then increments if you fire faster than the clock).
  • Bitget and OKX use a timestamp plus an API passphrase (apiPass). That is a third credential, not a nonce counter.
  • Binance and Bybit use a millisecond timestamp plus recvWindow.

The SDK builds these values. You do not assemble them unless you override them.

Timestamp Synchronization Strategies

On Binance and Bybit, recvWindow is how many milliseconds the request stays valid after its timestamp. If local time is too far ahead or behind server time, you get a timestamp error even when the signature algorithm is correct. Default window is 5000ms. Tune it per client if you need more slack, but do not treat 1000ms of skew as an automatic ban.

How the SDKs actually help:

  • binance: time sync is off by default (disableTimeSync: true). Set disableTimeSync: false if you want periodic offset updates. You can also call setTimeOffsetMs() on the WebSocket API client.
  • bybit-api: enable_time_sync is false by default and not recommended. Prefer fixing the machine clock. setTimeOffsetMs() is available on the WS API client if you still see recvWindow errors.
  • okx-api and coinbase-api do not expose a recvWindow time-sync flag. OKX timestamps are ISO-8601 strings. Coinbase Advanced Trade auth is JWT-based. Those clients are not Binance-style recvWindow clients.

Do not confuse error types. An invalid or deactivated key often shows up as HTTP 401 or an exchange code such as Binance -2015. A bad signature is often -1022 on Binance. A stale timestamp is -1021. OKX wraps this in a JSON code field even on HTTP 200. okx-api also needs apiPass. Missing the passphrase looks like an auth failure, not a hash bug.

Implementing Secure Request Signing with Siebly SDKs

Install the package for the venue you are calling. Field names are not the same across SDKs. Copy from the examples, do not invent a shared config shape.

Imported example

Shell
npm install bybit-api binance @siebly/kraken-api

For OKX or Bitget you also need the passphrase package (okx-api or bitget-api). For Coinbase Advanced Trade, install coinbase-api. For Gate.io, gateio-api.

These clients sign REST and WebSocket traffic. They do not queue requests to stay under exchange limits.

REST clients: Bybit, Binance, Kraken

Bybit V5 uses RestClientV5 with key and secret. Live is the default.

Imported example

TypeScript
import { RestClientV5 } from "bybit-api";

const client = new RestClientV5({
  key: process.env.API_KEY_COM,
  secret: process.env.API_SECRET_COM,
  // testnet: true, // Bybit testnet, not the same as demo trading
  // demoTrading: true, // simulated funds, real market data
});

const order = await client.submitOrder({
  category: "linear",
  symbol: "BTCUSDT",
  orderType: "Market",
  qty: "0.001",
  side: "Buy",
});

Binance spot and margin use MainClient with api_key and api_secret. HMAC secrets are the normal string. Ed25519 and RSA keys are PEM private keys passed as api_secret. The SDK detects the type.

Imported example

TypeScript
import { MainClient } from "binance";

const client = new MainClient({
  api_key: process.env.API_KEY_COM,
  api_secret: process.env.API_SECRET_COM,
  // testnet: true, // isolated testnet, market data is not production-like
  // demoTrading: true, // simulated trading on real market data
});

const trades = await client.getAccountTradeList({ symbol: "BTCUSDT" });

Kraken Spot uses @siebly/kraken-api. The secret is the base64 private key Kraken shows in the UI. The SDK injects the nonce.

Imported example

TypeScript
import { SpotClient } from "@siebly/kraken-api";

const client = new SpotClient({
  apiKey: process.env.API_KEY_COM,
  apiSecret: process.env.API_SECRET_COM,
});

const result = await client.submitOrder({
  ordertype: "limit",
  type: "buy",
  volume: "0.0001",
  pair: "XBTUSD",
  price: "10000",
  cl_ord_id: client.generateNewOrderID(),
});

OKX is the passphrase case. Three values, not two:

Imported example

TypeScript
import { RestClient } from "okx-api";

const client = new RestClient({
  apiKey: process.env.API_KEY_COM,
  apiSecret: process.env.API_SECRET_COM,
  apiPass: process.env.API_PASSPHRASE_COM,
  demoTrading: true, // OKX has no testnet flag; this is demo trading
});

const buyResult = await client.submitOrder({
  instId: "BTC-USDT",
  ordType: "market",
  side: "buy",
  sz: "0.1",
  tdMode: "cash",
  tgtCcy: "base_ccy",
});

Bitget V3 looks the same: apiKey, apiSecret, apiPass on RestClientV3. Do not point new work at V2 unless you have not upgraded to UTA.

Testnet is not a universal flag. Bybit and Binance have both testnet and demoTrading, and they are different environments. OKX, Bitget, and BitMart use demoTrading. Kraken examples in this SDK run against production with live keys, so keep size tiny and permissions tight while you learn.

Walkthrough for Bybit credentials: Bybit JavaScript tutorial.

The Awaitable WebSocket Pattern

Raw WebSockets are event-based. That is fine for market data. It is awkward for "place this order and tell me what happened." Siebly SDKs wrap the exchange WebSocket API in WebsocketAPIClient: you await a method, the client matches the response, and you get a promise.

Auth for private sockets happens after the TCP connection is up. The SDK sends a signed login (or, on Binance HMAC/RSA WebSocket API, signs each command). Ed25519 on Binance can log in once, then send unsigned commands, which is why it is faster for WS API. HMAC and RSA still sign every WS API call.

If the socket drops, the client reconnects, resubscribes, and re-authenticates. You still should not log secrets in those event handlers.

Bybit:

Imported example

TypeScript
import { WebsocketAPIClient } from "bybit-api";

const wsClient = new WebsocketAPIClient({
  key: process.env.API_KEY_COM,
  secret: process.env.API_SECRET_COM,
  // testnet: true,
  // demoTrading does not support the Bybit WS API for sending orders
});

const response = await wsClient.submitNewOrder({
  category: "linear",
  symbol: "BTCUSDT",
  orderType: "Limit",
  qty: "0.001",
  side: "Buy",
  price: "50000",
});

Binance spot WS API:

Imported example

TypeScript
import { WebsocketAPIClient } from "binance";

const wsClient = new WebsocketAPIClient({
  api_key: process.env.API_KEY_COM,
  api_secret: process.env.API_SECRET_COM,
  beautify: true,
  // testnet: true,
});

const response = await wsClient.submitNewSpotOrder({
  symbol: "BTCUSDT",
  side: "SELL",
  type: "LIMIT",
  timeInForce: "GTC",
  price: "23416.10000000",
  quantity: "0.00847000",
});

Kraken Spot WS API:

Imported example

TypeScript
import { WebsocketAPIClient } from "@siebly/kraken-api";

const wsApiClient = new WebsocketAPIClient({
  apiKey: process.env.API_KEY_COM,
  apiSecret: process.env.API_SECRET_COM,
});

const orderResponse = await wsApiClient.submitSpotOrder({
  order_type: "limit",
  side: "buy",
  limit_price: 26500.4,
  order_qty: 1.2,
  symbol: "BTC/USD",
});

OKX WS API credentials go in accounts, not as top-level apiKey fields on that client:

Imported example

TypeScript
import { WebsocketAPIClient } from "okx-api";

const wsClient = new WebsocketAPIClient({
  accounts: [
{
apiKey: process.env.API_KEY_COM,
apiSecret: process.env.API_SECRET_COM,
apiPass: process.env.API_PASSPHRASE_COM,
},
  ],
});

const res = await wsClient.submitNewOrder({
  instId: "BTC-USDT",
  tdMode: "cash",
  side: "buy",
  ordType: "market",
  sz: "100",
});

Same pattern exists on bitget-api, gateio-api, kucoin-api, and @siebly/htx-api. Coinbase's SDK covers private user-data sockets. It does not expose a Binance-style WebsocketAPIClient for order placement.

Full client list: Siebly SDKs.

Beyond Signing: Building Resilient Trading Systems

A valid signature only means the request was accepted as authentic. Production still needs error handling, test environments, and a throttle. Use public endpoints and demo/testnet first. Check request shapes there before you touch a funded account.

Production Readiness and Rate Limiting

Siebly SDKs do not auto-throttle. If you hammer an endpoint, you get 429s or IP bans. That is on you.

What they do provide:

  • Binance REST clients track x-mbx-used-weight and related headers on the client instance. Read those and slow down. They will not pause traffic for you.
  • Bybit: set parseAPIRateLimits: true if you want parsed limit info on responses.
  • OKX has a getAccountRateLimit() REST method. That is a query, not a governor.

Watch those signals and build a queue. Details for OKX live in the official docs, which remain the source of truth for weights and windows.

Security and Key Management

Do not hardcode secrets. Use env vars locally and a secret manager in production. Restrict keys to allowlisted IPs. Disable withdrawals on bot keys. Give each key only what it needs: read-only for market/account snapshots, trade-only for execution.

Once signing is out of the way, the next problem is the rest of the stack: state, data, and failure modes. That is a separate piece: algorithmic trading system architecture. Keep using the SDK docs when you add venues.

Modernizing Your Authentication Architecture

Manual HMAC code does not age well. Serialization rules and key types keep changing. Siebly SDKs give you typed clients that already know how Binance, Bybit, OKX, Kraken, and the rest expect a request to look. HMAC is the default on most venues. Ed25519 matters on Binance (and Coinbase JWTs). Bybit is HMAC or RSA.

You still own rate limits, clock health, and secret storage. The SDKs are also shaped for AI-assisted coding (llms.txt, consistent method names), which cuts the usual class of "almost correct" signature bugs. Start from a client that already signs correctly, then build the rest of the system on top.

Next Steps: Explore Siebly SDKs for production-ready exchange integration

Frequently Asked Questions

How do I handle HMAC-SHA256 signing in Node.js for crypto APIs?

You can call crypto.createHmac('sha256', secret).update(payload).digest('hex') or .digest('base64'). That is the easy line. The payload string is the hard part: param order, JSON separators, timestamps, recvWindow, and which encoding the venue wants. bybit-api and binance build that string and pick hex vs base64 from the key type. Prefer that over concatenating query strings by hand.

Do Siebly SDKs handle rate-limiting and throttling automatically?

No. They sign and send. They do not sit in front of your traffic as a scheduler. Binance records weight headers. Bybit can parse rate-limit headers. You still need a queue, backoff, and a plan for 429s.

What is the best way to synchronize timestamps with an exchange server?

Fix the host clock first (NTP). Then, if the venue uses recvWindow, keep timestamps within that window of server time. Binance and Bybit default to 5000ms.

Do not assume every SDK has a sync flag. okx-api and coinbase-api do not offer Binance-style enable_time_sync / recvWindow sync. On Binance, time sync is off until you set disableTimeSync: false. On Bybit, enable_time_sync is off and not the first tool you should reach for. WS API clients expose setTimeOffsetMs() when you still see window errors after the clock is healthy.

Can I use these libraries with AI coding agents like GitHub Copilot?

Yes. The clients are TypeScript-first, method names stay consistent, and several packages include llms.txt. That is more useful to an agent than a pile of unsigned fetch calls. You still need to pass the right credential field names (key/secret on Bybit, api_key/api_secret on Binance, apiPass on OKX and Bitget).

How do I authenticate a WebSocket connection for private order streams?

After the socket opens, the SDK sends a signed login, or it signs each WebSocket API command. You do not hand-roll that payload if you use WebsocketAPIClient. Await the method (order, cancel, amend). For market data and account streams, subscribe on WebsocketClient and handle update events. Reconnect and re-auth are built in.

Why is my API signature invalid even though my key is correct?

Usually serialization or time, not a typo in the key. Extra JSON spaces, unsorted params, the wrong encoding, or a timestamp outside the window will all fail. A missing OKX/Bitget passphrase fails auth too. bitget-api and gateio-api follow the official prehash rules. If you still roll your own signer, compare your exact prehash string to what the SDK would have signed.

What is the difference between Ed25519 and HMAC for exchange signing?

HMAC-SHA256 is symmetric: you and the exchange share the secret. Ed25519 is asymmetric: you keep a private key, the exchange verifies with the public key. Binance supports both (plus RSA). Coinbase Advanced Trade keys are ECDSA or Ed25519 JWTs. Bybit V5 does not support Ed25519. It is HMAC or RSA. You do not flip a signAlgo: 'ed25519' flag on these clients. You pass the key material. HMAC secrets stay strings. Ed25519/RSA keys are PEM (Binance) or the format Coinbase issued.

How should I store my API secrets securely in a Node.js trading app?

Never commit them. Use a .env file locally and a secret manager in production. Least privilege: no withdraw on automation keys, IP allowlists, separate keys for read vs trade. Assume a leaked trade-only key can still wreck a position. Design for that.

Related articles

Continue from here

Related Siebly resources

All articles

Subscribe on Substack

Complete the Substack form below to join our newsletter. Substack handles all subscriber data directly.