---
title: "Crypto Exchange Request Signing in JavaScript"
description: "Master crypto exchange request signing in JavaScript. A professional guide to HMAC authentication in Node.js to build secure, production-ready API integrations."
canonical: "https://siebly.io/blog/crypto-exchange-request-signing-in-javascript-a-professional-engineering-guide"
---

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

## Technical Disclaimer

These articles are software engineering references for exchange API integrations. They are not financial, investment, legal, tax, compliance, or trading advice. Use public data, demo, testnet, or paper workflows first. Keep API credentials out of frontend code and disable withdrawal permissions for automation keys.

## Overview {#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](/sdk/binance/javascript), [bybit-api](/sdk/bybit/javascript), [okx-api](/sdk/okx/javascript), [bitget-api](/sdk/bitget/javascript), [kucoin-api](/sdk/kucoin/javascript), [coinbase-api](/sdk/coinbase/javascript), [gateio-api](/sdk/gate/javascript), [bitmart-api](/sdk/bitmart/javascript), [@siebly/kraken-api](/sdk/kraken/javascript), [@siebly/htx-api](/sdk/htx/javascript)) take signing off your plate.

## Key Takeaways {#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 {#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](https://en.wikipedia.org/wiki/HMAC).

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 {#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 {#sha256-vs-other-hashing-algorithms}

HMAC-SHA256 is the default for [Binance](/sdk/binance/javascript), [Bybit](/sdk/bybit/javascript), [OKX](/sdk/okx/javascript), [Bitget](/sdk/bitget/javascript), [KuCoin](/sdk/kucoin/javascript), BitMart, and HTX.

That is not the whole map:

- [Binance](/sdk/binance/javascript) 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](/sdk/kraken/javascript) signs with HMAC-SHA512 over a SHA-256 digest. The secret is base64-encoded.
- [Gate.com](/sdk/gate/javascript) signs with HMAC-SHA512. The string is method, path, query, hashed body, and timestamp, joined by newlines.
- [Coinbase](/sdk/coinbase/javascript) 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/order` on 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](/sdk) SDKs keep a per-exchange signer instead of one generic function.

## Implementing Request Signing with Node.js Crypto {#implementing-request-signing-with-nodejs-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 {#basic-hmac-implementation-example}

Credentials go in `process.env`.

Bybit V5 HMAC (matches [bybit-api](/sdk/bybit/javascript)): concatenate timestamp, API key, recv window, then the raw query string or JSON body. No extra separators. HMAC-SHA256, hex digest.

```js title="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](/sdk/okx/javascript)): ISO timestamp + method + path + body. HMAC-SHA256, base64. You also send the passphrase.

```js title="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:

```js title="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 {#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](/sdk/binance/javascript), 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 {#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](/sdk/bybit/javascript) 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](/sdk/binance/javascript) 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 {#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 {#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](/sdk/okx/javascript), and the other [Siebly.io](/sdk) clients: each one already builds the string the exchange expects.



## Security Best Practices for API Key Management {#security-best-practices-for-api-key-management}

Least privilege first. On [OKX](/sdk/okx/javascript) or [Binance](/sdk/binance/javascript), 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](/sdk/bybit/javascript) and [Bitget](/sdk/bitget/javascript) both support them. Use them on production keys.

### Secure Secret Handling in Node.js {#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 {#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](/blog/algorithmic-trading-system-architecture-in-nodejs-a-2026-engineering-guide).

[Siebly.io SDKs](/sdk) 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 {#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](/sdk/bybit/javascript)). 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](/sdk/binance/javascript)
- `bybit-api`
- [okx-api](/sdk/okx/javascript)
- [bitget-api](/sdk/bitget/javascript)
- [kucoin-api](/sdk/kucoin/javascript)
- [coinbase-api](/sdk/coinbase/javascript)
- [gateio-api](/sdk/gate/javascript)
- [bitmart-api](/sdk/bitmart/javascript)
- [@siebly/kraken-api](/sdk/kraken/javascript)
- [@siebly/htx-api](/sdk/htx/javascript)

Private REST on Bybit, from the `bybit-api` examples. Signing is inside `submitOrder`.

```js title="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):

```js title="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.

```js title="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:

```js title="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 {#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):

```js title="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 {#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](/sdk/bybit/javascript). The rest of the set is on [Siebly SDKs for JavaScript and TypeScript](/sdk).

## Standardizing Your Exchange Integration Layer {#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](/sdk).

## Frequently Asked Questions {#frequently-asked-questions}

### How do I handle the "Timestamp for this request is outside of the recvWindow" error? {#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](/sdk/bybit/javascript) you can set `enable_time_sync: true` or `wsClient.setTimeOffsetMs(...)`. In [binance](/sdk/binance/javascript), 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? {#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? {#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? {#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? {#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? {#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? {#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? {#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

- [Mastering Crypto Exchange Request Signing in JavaScript: A 2026 Engineering Guide](/blog/mastering-crypto-exchange-request-signing-in-javascript-a-2026-engineering-guide)
- [Crypto Exchange Integration Patterns: Architecting Reliable Node.js Systems in 2026](/blog/crypto-exchange-integration-patterns-architecting-reliable-nodejs-systems-in-2026)
- [OKX API Wrapper JavaScript: A Production Engineering Guide for 2026](/blog/okx-api-wrapper-javascript-a-production-engineering-guide-for-2026)


## Related Siebly Resources

- [Binance JavaScript SDK](/sdk/binance/javascript)
- [Bybit JavaScript SDK](/sdk/bybit/javascript)
- [OKX JavaScript SDK](/sdk/okx/javascript)
- [Siebly SDK directory](/sdk)
- [Exchange State Management](/ai/exchange-state)
