Crypto Exchange Request Signing in JavaScript: A Professional Engineering Guide
Master crypto exchange request signing in JavaScript. A professional guide to HMAC authentication in Node.js to build secure, production-ready API integrations.
Overview
Cryptographic signing is a solved problem. The friction is canonicalization: each exchange wants a slightly different string, timestamp, and header set. That is why DIY clients stall on signature mismatches, clock drift, and 401-style auth errors.
This guide covers how HMAC authentication actually works in Node.js, where exchange specs diverge, and how Siebly.io SDKs (binance, bybit-api, okx-api, bitget-api, kucoin-api, coinbase-api, gateio-api, bitmart-api, @siebly/kraken-api, @siebly/htx-api) take signing off your plate.
Key Takeaways
- HMAC-SHA256 is the most common REST signing method, but it is not universal. Kraken and Gate.com use HMAC-SHA512. Binance also accepts RSA and Ed25519. Coinbase Advanced Trade uses JWT.
- A valid signature is only as good as the exact string you hash. Parameter order, JSON spacing, encoding (hex vs base64), and timestamp format all matter.
- Store secrets on the server. Disable withdrawals on automation keys. Use IP allowlists.
- Siebly SDKs sign REST and WebSocket traffic for you. Time sync exists but is off by default. Rate limiting is still your job.
The Fundamentals of HMAC Authentication in Crypto APIs
Hash-based Message Authentication Code is how most private exchange endpoints prove a request came from you and was not altered in transit. See HMAC Authentication.
The exchange gives you an API key and an API secret. The key is a public identifier. The secret never leaves your server. You hash a canonical string with that secret. The exchange repeats the same calculation. If the hashes match, the request is accepted.
OKX, Bitget, and KuCoin also require a passphrase created with the key. BitMart requires a memo. Those extra values are part of the auth headers, not optional extras.
Understanding the API Secret and Key Pair
This is symmetric-key signing. You and the exchange share the secret. It must stay on the backend. Putting it in a browser or mobile app means anyone can sign as you. Use environment variables or a vault. Do not hardcode secrets.
SHA256 vs other Hashing Algorithms
HMAC-SHA256 is the default for Binance, Bybit, OKX, Bitget, KuCoin, BitMart, and HTX.
That is not the whole map:
- Binance also accepts RSA and Ed25519. Ed25519 is required if you want a one-time WebSocket API login. HMAC and RSA still sign every WS API request.
- Kraken signs with HMAC-SHA512 over a SHA-256 digest. The secret is base64-encoded.
- Gate.com signs with HMAC-SHA512. The string is method, path, query, hashed body, and timestamp, joined by newlines.
- Coinbase Advanced Trade uses JWT (ECDSA or Ed25519). The older Exchange API still uses HMAC with
CB-ACCESS-*headers. - HTX accepts HMAC-SHA256 or Ed25519.
The pieces that usually go into the signed string:
- HTTP method (GET, POST, DELETE), often uppercase.
- Endpoint path, for example
/api/v5/trade/orderon OKX (not/api/v5/order/place). - Timestamp. Milliseconds on Binance and Bybit. ISO 8601 on OKX. Seconds on Gate.com and Coinbase Exchange.
- Payload: query string or JSON body, byte for byte.
Miss one character and the hashes diverge. That is why Siebly.io SDKs keep a per-exchange signer instead of one generic function.
Implementing Request Signing with Node.js Crypto
Node's node:crypto module is enough for HMAC. createHmac('sha256', secret) is the usual starting point. After you build the string, you digest it as hex or base64 depending on the exchange.
Bybit HMAC uses hex and puts the result in X-BAPI-SIGN. OKX, Bitget, and KuCoin use base64 (OK-ACCESS-SIGN, ACCESS-SIGN, KC-API-SIGN). Binance HMAC also uses hex, but the signature is a query parameter named signature, with the key in X-MBX-APIKEY. There is no generic X-SIGN header across these APIs.
Basic HMAC Implementation Example
Credentials go in process.env.
Bybit V5 HMAC (matches bybit-api): concatenate timestamp, API key, recv window, then the raw query string or JSON body. No extra separators. HMAC-SHA256, hex digest.
Imported example
import { createHmac } from "crypto";
const apiKey = process.env.API_KEY;
const apiSecret = process.env.API_SECRET;
const recvWindow = 5000;
const timestamp = Date.now();
const body = JSON.stringify({
category: "spot",
symbol: "BTCUSDT",
side: "Buy",
orderType: "Limit",
qty: "0.001",
price: "50000",
});
const payload = `${timestamp}${apiKey}${recvWindow}${body}`;
const sign = createHmac("sha256", apiSecret).update(payload).digest("hex");
const headers = {
"X-BAPI-API-KEY": apiKey,
"X-BAPI-SIGN": sign,
"X-BAPI-TIMESTAMP": timestamp,
"X-BAPI-RECV-WINDOW": recvWindow,
"X-BAPI-SIGN-TYPE": 2,
};
OKX HMAC (matches okx-api): ISO timestamp + method + path + body. HMAC-SHA256, base64. You also send the passphrase.
Imported example
import { createHmac } from "crypto";
const apiKey = process.env.API_KEY;
const apiSecret = process.env.API_SECRET;
const apiPass = process.env.API_PASS;
const timestamp = new Date().toISOString();
const method = "POST";
const endpoint = "/api/v5/trade/order";
const body = JSON.stringify({
instId: "BTC-USDT",
tdMode: "cash",
side: "buy",
ordType: "limit",
px: "50000",
sz: "0.01",
});
const payload = `${timestamp}${method}${endpoint}${body}`;
const sign = createHmac("sha256", apiSecret).update(payload).digest("base64");
const headers = {
"OK-ACCESS-KEY": apiKey,
"OK-ACCESS-SIGN": sign,
"OK-ACCESS-TIMESTAMP": timestamp,
"OK-ACCESS-PASSPHRASE": apiPass,
};
If you already use a Siebly SDK on Node and want createHmac instead of Web Crypto, inject customSignMessageFn. Encoding must match that exchange. Bybit HMAC is hex:
Imported example
import { createHmac } from "crypto";
import { RestClientV5 } from "bybit-api";
const client = new RestClientV5({
key: process.env.API_KEY_COM,
secret: process.env.API_SECRET_COM,
customSignMessageFn: async (message, secret) => {
return createHmac("sha256", secret).update(message).digest("hex");
},
});
OKX, Bitget, and KuCoin need digest('base64') in that hook. BitMart HMAC is hex. Kraken is SHA-512 and base64 over a binary input. Do not copy one digest format across packages.
Managing Nonces and Timestamps
A timestamp (or nonce) stops replay. The exchange rejects a request whose clock is outside a short window. Binance and Bybit default that window to 5000ms (recvWindow / recv_window). Binance allows up to 60000ms. OKX does not use recvWindow. It signs an ISO timestamp instead.
Clock drift is the usual failure. A correct signature still dies if your clock is ahead or behind.
Siebly SDKs can apply a time offset, but it is off by default. In bybit-api, set enable_time_sync: true. In binance, time sync is disabled unless you set disableTimeSync: false. You can also fetch server time yourself (/api/v3/time on Binance spot) and set an offset. Syncing the OS clock is still the better fix.
The Complexity of Canonical Request Construction
HMAC is easy. Canonicalization is not. If one space, key order, or encoding differs from what the exchange hashes, you get a signature error.
This is where the APIs split.
Bybit V5 signs timestamp + apiKey + recvWindow + payload with no delimiters. GET uses the query string as the payload. POST uses JSON.stringify of the body. bybit-api does not sort query keys.
Binance signs the serialized query string, including timestamp and recvWindow, then appends signature. HMAC output is hex. RSA and Ed25519 output is base64. The binance SDK serializes keys in object order. It does not alphabetically sort them. What matters is that the sent query string matches the signed one.
OKX signs ISO timestamp + METHOD + path + body. Bitget signs timestamp + METHOD + path + queryOrBody. KuCoin signs timestamp + METHOD + /endpoint + body. BitMart signs timestamp#memo#body. Same HMAC idea, different glue.
Sorting and Formatting Query Parameters
Some DIY guides say "always sort keys alphabetically." That is not a universal rule, and it is not what these SDKs do for Bybit or Binance. Bybit GET appends the query string directly after the recv window. If you sort and the exchange does not, or the other way around, the signature breaks.
URLSearchParams is fine for encoding. It will not save you if the exchange wants a different order or a JSON body instead of a query string.
Payload Normalization for POST Requests
POST signing is usually JSON.stringify(body) with no extra spaces. If you pretty-print JSON for logs and then sign the pretty version, the exchange sees compact JSON and the hashes will not match. Sign the exact bytes you send.
That is the main reason to use bybit-api, binance, okx-api, and the other Siebly.io clients: each one already builds the string the exchange expects.
Security Best Practices for API Key Management
Least privilege first. On OKX or Binance, enable only the product you trade (spot, futures). Turn withdrawals off for every automation key. If the box is compromised, the attacker can trade, not drain the wallet.
IP allowlists are the second lock. Bybit and Bitget both support them. Use them on production keys.
Secure Secret Handling in Node.js
Do not commit secrets. Use dotenv or a secret manager. Do not log the string you sign. That string is the HMAC input. Rotate keys on a schedule.
Defining Safety Boundaries
Use testnet or demo accounts first. Bybit demo trading is demoTrading: true on RestClientV5. Binance has testnet and demo clients. Keep signing code away from strategy code so you can audit it.
For system layout, see Siebly.io research on trading system architecture.
Siebly.io SDKs sign privately by default once you pass credentials. They will not invent key permissions for you. That is still on the exchange dashboard.
Streamlining Authentication with Siebly SDKs
The SDKs build the canonical string, pick hex or base64, set the right headers, and sign WebSocket handshakes. You still own rate limits. Some clients can parse limit headers (parseAPIRateLimits on bybit-api). None of them throttle your process for you.
Request and response types are generated from the exchange specs. Missing fields fail in TypeScript before they fail on the wire.
Package names on npm:
- binance
bybit-api- okx-api
- bitget-api
- kucoin-api
- coinbase-api
- gateio-api
- bitmart-api
- @siebly/kraken-api
- @siebly/htx-api
Private REST on Bybit, from the bybit-api examples. Signing is inside submitOrder.
Imported example
import { RestClientV5 } from "bybit-api";
const client = new RestClientV5({
testnet: true,
key: process.env.API_KEY_COM,
secret: process.env.API_SECRET_COM,
});
const response = await client.submitOrder({
category: "spot",
symbol: "BTCUSDT",
side: "Buy",
orderType: "Limit",
qty: "0.001",
price: "50000",
});
OKX needs the passphrase as well (okx-api README):
Imported example
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_PASS_COM,
});
const buyResult = await client.submitOrder({
instId: "BTC-USDT",
ordType: "market",
side: "buy",
sz: "0.1",
tdMode: "cash",
tgtCcy: "base_ccy",
});
Binance HMAC or Ed25519 is the same constructor. If api_secret is a PEM private key, the SDK switches to RSA or Ed25519 on its own.
Imported example
import { MainClient } from "binance";
const client = new MainClient({
api_key: process.env.API_KEY_COM,
api_secret: process.env.API_SECRET_COM,
});
const order = await client.submitNewOrder({
symbol: "BTCUSDT",
side: "BUY",
type: "LIMIT",
timeInForce: "GTC",
quantity: 0.001,
price: 50000,
});
Coinbase Advanced Trade is JWT, not HMAC. Pass the key name and the EC or Ed25519 private key:
Imported example
import { CBAdvancedTradeClient } from "coinbase-api";
const client = new CBAdvancedTradeClient({
apiKey: process.env.API_KEY_NAME,
apiSecret: process.env.API_PRIVATE_KEY,
});
const accounts = await client.getAccounts();
The Awaitable WebSocket Pattern
You can still subscribe to streams with events. For order entry, the WebSocket API clients return a promise. The SDK signs the connection (and, where required, each request). You call submitNewOrder the same way you would call REST.
Bybit (bybit-api WS API example):
Imported example
import { WebsocketAPIClient } from "bybit-api";
const wsClient = new WebsocketAPIClient({
key: process.env.API_KEY_COM,
secret: process.env.API_SECRET_COM,
});
const response = await wsClient.submitNewOrder({
category: "linear",
symbol: "BTCUSDT",
orderType: "Limit",
qty: "0.001",
side: "Buy",
price: "50000",
});
OKX and Binance expose the same shape via WebsocketAPIClient (submitNewOrder / submitNewSpotOrder). On Binance, HMAC and RSA still sign each WS API call. Ed25519 can authenticate the session once, then skip per-request signatures. That is why Binance recommends Ed25519 if you care about WS API latency.
Kraken, Gate.com, Bitget, HTX, and KuCoin also ship WebSocket API clients in the same style. Public market streams still do not need a key.
Professional Integration Layer
The clients are TypeScript-first and stable enough for generated agent code. For V5 Bybit work, start with the Bybit JavaScript SDK. The rest of the set is on Siebly SDKs for JavaScript and TypeScript.
Standardizing Your Exchange Integration Layer
HMAC-SHA256 is the common case. Canonical strings, timestamp formats, and digest encodings are not. Least-privilege keys and server-side secrets are non-negotiable.
Siebly.io ships TypeScript clients for Binance, Bybit, OKX, Bitget, KuCoin, Coinbase, Gate.com, BitMart, Kraken, and HTX. They sign REST and awaitable WebSocket calls. You keep rate limits and risk controls. Explore production-ready Siebly SDKs for JavaScript and TypeScript.
Frequently Asked Questions
How do I handle the "Timestamp for this request is outside of the recvWindow" error?
That message is from Binance. Bybit uses the same idea. Default window is 5000ms. Binance lets you raise it to 60000ms. Raising the window hides drift. Fixing the clock is better.
Fetch /api/v3/time on Binance spot (or the matching time endpoint on futures), compute serverTime - Date.now(), and apply that offset. In bybit-api you can set enable_time_sync: true or wsClient.setTimeOffsetMs(...). In binance, set disableTimeSync: false. Time sync is not on by default.
What is the difference between an API Key and an API Secret in crypto trading?
The API key identifies the account. The secret is the HMAC (or JWT/RSA/Ed25519) key. Only the signature travels on the wire. OKX, Bitget, and KuCoin also need the passphrase you set when creating the key. That is not your login password.
Is it safe to sign crypto API requests on the client side using JavaScript?
No. The secret ends up in the browser. Sign on a Node backend. Keep keys in environment variables or a vault.
How does HMAC-SHA256 work for crypto exchange authentication?
It hashes your canonical request string with your secret. The exchange repeats that hash. Match means authentic and unmodified. Kraken and Gate.com do the same thing with SHA-512. Coinbase Advanced Trade uses a signed JWT instead.
Why do I get a "Signature for this request is not valid" error when my HMAC code looks correct?
The hash function is rarely the bug. The string is. Typical causes:
- Query key order does not match what you sent.
- Pretty-printed JSON in the signature, compact JSON on the wire.
- Hex digest where the exchange wants base64 (OKX, Bitget, KuCoin), or the reverse (Bybit HMAC, Binance HMAC, BitMart).
- Wrong path. OKX place-order is
/api/v5/trade/order. - Missing passphrase or memo.
Siebly SDKs build that string per exchange.
Do I need to sign public market data requests like order books or tickers?
No. Books, trades, and candles are public GET or public WebSocket topics. Sign private account calls: orders, balances, positions. Public endpoints often have tighter IP rate limits.
How can I synchronize my server time with the Binance or Bybit API server?
Call the public time endpoint. Binance spot is /api/v3/time (api/v3/time in the binance client). Store serverTime - localTime and add it when you stamp requests. Enable the SDK time-sync flags if you want that offset refreshed on a timer. Still fix the OS clock if drift is large.
Can I use the same API key for both REST and WebSocket integrations?
Yes on these exchanges. The handshake differs. REST signs every request. Private WebSocket topics usually send one auth message or a signed URL. Awaitable WebSocket API calls may still sign per request (Binance HMAC/RSA). Ed25519 session login on Binance is the exception.
Disclaimer
*Technical and legal disclaimer: Siebly.io provides software development tools, SDKs, documentation, and educational engineering content for crypto exchange API integrations. This content is for software engineering education only and is not financial, investment, legal, tax, accounting, compliance, or trading advice.
Nothing in this article is a recommendation, invitation, or inducement to buy, sell, hold, trade, long, short, or allocate to any cryptoasset, exchange product, strategy, bot, or automated workflow. Examples, code patterns, simulations, backtests, and architecture diagrams are illustrative only and must not be treated as trading signals, investment recommendations, or evidence of future performance.
Cryptoasset markets are high risk and volatile. If you choose to build or operate exchange-connected software, you are responsible for your own legal, regulatory, tax, security, exchange-account, API-key, and risk-management obligations. Use public data, testnet, demo, dry-run, or paper-trading workflows before any live execution. Keep API keys server-side, use least-privilege permissions, and never enable withdrawals for automation keys unless you fully understand and accept the risks.
Siebly.io is not an exchange, custodian, investment adviser, trading-signal provider, or managed trading service. Official exchange documentation remains the source of truth for exchange-specific rules, API behavior, and terms of use. Use of Siebly.io content and software is also subject to the Siebly.io terms and conditions.*
Related articles
Continue from here