---
title: "Crypto Exchange API Guide: REST, WebSockets, and SDKs"
description: "Master REST and WebSockets with this Developer guide for crypto exchange APIs. Build reliable Node.js integrations using typed SDKs for auth and signing."
canonical: "https://siebly.io/blog/crypto-exchange-api-guide-rest-websockets-and-sdks"
---

# Crypto Exchange API Guide: REST, WebSockets, and SDKs

Master REST and WebSockets with this Developer guide for crypto exchange APIs. Build reliable Node.js integrations using typed SDKs for auth and signing.

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

A working request is the easy part. The rest of the integration is authentication, signing, timestamps, connection state, and what your app does when a call fails. This guide walks through that work in JavaScript, TypeScript, and Node.js, using the Siebly.io SDKs as the client layer. Official exchange documentation is still where you check endpoint rules, permissions, and response fields.

REST is a request and a response. WebSockets stay open for streams. On some exchanges you can also send a command over the socket and await the reply. These SDKs sign requests and keep WebSocket connections alive. They do not pace your traffic, and they do not retry a REST call for you.

## Key Takeaways {#key-takeaways}

- Map the job first: public market data, a private read, or an order. Then open the exchange docs for that call.
- Use REST when you want one answer. Use a WebSocket when you need a stream, or an awaitable socket call where that SDK supports it.
- The SDK builds the signature, timestamp, and typed request. Your app still validates the payload and decides whether a failed call should be sent again.
- Start with public data. Move to a testnet, demo, or sandbox only when that exchange and that client actually have one.
- Keep keys out of source control. Use the smallest permission set you can, and leave withdrawal permission off on automation keys.
- Rate limits are yours to implement. A reconnect is not a retry, and a retry is not a throttle.



## What the integration actually includes {#what-the-integration-actually-includes}

An exchange API is the interface the exchange publishes. Your integration is the code that calls it and turns the result into something your app can trust.

Public market data can run with no key. Private reads and orders need credentials, and they have to follow that exchange's permission model. An [API key](https://en.wikipedia.org/wiki/Application_programming_interface_key) is only one piece of that. Store it outside the repo, scope it tightly, and do not enable withdrawals for a bot key.

Typical work inside the integration:

- Transport. Pick REST or a persistent socket.
- Request shape. Match the documented parameters. Symbol format is part of that shape. `BTCUSDT`, `BTC-USDT`, `BTC_USDT`, and `BTC/USD` are not interchangeable.
- Authentication. Attach the key, signature, and timestamp the exchange expects. The option names are not the same in every package.
- Response handling. Read the body, surface errors, and check the fields before you write them into local state.
- Operations. Reconnect streams. Pace requests yourself. Decide, per call, whether a failure should be sent again.

They do not replace the exchange's own docs.

Package setup and per-exchange notes live on the [Siebly.io SDK pages](/sdk).

## REST, streams, and awaitable socket calls {#rest-streams-and-awaitable-socket-calls}

A REST call is one round trip. You await it, handle the error, and only then update state.

A WebSocket client opens the connection when you subscribe, sends heartbeats, and if the socket drops it reconnects and resubscribes. Listen for that. On Bybit, OKX, Bitget, BitMart, and Gate the data event is `update`. On Binance, HTX, and Kraken the data event is `message`. Binance can also emit `formattedMessage` when `beautify` is on. HTX and Kraken emit `reconnecting`. Most of the others emit `reconnect`.

Some products also have a WebSocket API: you call a method, and the promise resolves with the reply on that connection. These packages ship a `WebsocketAPIClient` for that:

- [binance](/sdk/binance/javascript)
- [bybit-api](/sdk/bybit/javascript)
- [okx-api](/sdk/okx/javascript)
- [bitget-api](/sdk/bitget/javascript) (V3 / unified account)
- [gateio-api](/sdk/gate/javascript)
- [kucoin-api](/sdk/kucoin/javascript)
- [@siebly/htx-api](/sdk/htx/javascript)
- [@siebly/kraken-api](/sdk/kraken/javascript)

[coinbase-api](/sdk/coinbase/javascript) and `bitmart-api` are stream clients. They do not ship a `WebsocketAPIClient`. If you need an order acknowledgement on those two, use REST.

Public ticker over Binance's WebSocket API, with no key:

```js title="Imported example"
import { WebsocketAPIClient } from "binance";

const wsClient = new WebsocketAPIClient();

try {
  const ticker = await wsClient.getSpotTicker({
    symbol: "BTCUSDT",
  });
  console.log(ticker);
} catch (e) {
  console.error("getSpotTicker failed", e);
}
```

That pattern comes from `binance/examples/WebSockets/WS-API/ws-api-client.ts`. The same client can place and cancel orders, but those calls need a key, and Binance's WebSocket API login is fastest with an Ed25519 key. HMAC and RSA still work, and the SDK signs each command.

## From a public call to application state {#from-a-public-call-to-application-state}

Treat the lifecycle as five steps, and stop if one of them is vague:

1. Name the task and where the result will be stored.
2. Read the exchange page for that endpoint. Check parameters, auth, and error codes there.
3. Build the request with the client for that product.
4. Parse the success body and the error body separately.
5. Check the result against your own state before you write it.

Types help you notice a bad field while you are editing. They do not prove the network sent you what you expected.

### Public REST {#public-rest}

Public clients do not need a key. These three are taken from the public examples in each repo. The method names and symbol strings are different on purpose.

Binance spot ticker (`binance/examples/Rest/Spot/rest-spot-public.ts`):

```js title="Imported example"
import { MainClient } from "binance";

const client = new MainClient();

try {
  const ticker = await client.get24hrChangeStatistics({
    symbol: "BTCUSDT",
  });
  console.log(ticker);
} catch (e) {
  console.error("request failed", e);
}
```

`MainClient` covers spot, margin, wallet, and a long list of other main-site products. USD-M futures use `USDMClient`. COIN-M futures use `CoinMClient`. Portfolio margin uses `PortfolioClient`.

Bybit linear tickers (`bybit-api/examples/Rest/rest-v5-public.ts`). Category is required, because each category is a different API:

```js title="Imported example"
import { RestClientV5 } from "bybit-api";

const client = new RestClientV5();

try {
  const tickers = await client.getTickers({ category: "linear" });
  console.log(tickers);
} catch (e) {
  console.error("request failed", e);
}
```

KuCoin spot ticker (`kucoin-api/examples/Rest/rest-spot-public.ts`). The symbol uses a hyphen:

```js title="Imported example"
import { SpotClient } from "kucoin-api";

const client = new SpotClient();

try {
  const ticker = await client.getTicker({ symbol: "BTC-USDT" });
  console.log(ticker);
} catch (e) {
  console.error("request failed", e);
}
```

A few more public calls, same idea, different class and symbol:

```js title="Imported example"
import { RestClient } from "okx-api";

const okx = new RestClient();
const instruments = await okx.getInstruments({ instType: "SPOT" });
```

On OKX, a successful REST response is unwrapped. If HTTP is 200 and `code` is `"0"`, the method returns the inner `data`. If the call failed, the client throws the full body, including `code` and `msg`.

```js title="Imported example"
import { CBAdvancedTradeClient } from "coinbase-api";

const coinbase = new CBAdvancedTradeClient({});
const product = await coinbase.getPublicProduct({
  product_id: "BTC-USD",
});
```

```js title="Imported example"
import { RestClient } from "gateio-api";

const gate = new RestClient();
const gateTicker = await gate.getSpotTicker({
  currency_pair: "BTC_USDT",
});
```

Gate pairs use an underscore. BitMart spot topics use the same `BTC_USDT` form. HTX topic strings are lowercase, such as `market.btcusdt.ticker`. Kraken spot symbols look like `BTC/USD`.

### A public stream {#a-public-stream}

Bybit public V5 (`bybit-api/examples/Websocket/Public/ws-public-v5.ts`). The client opens the socket when you subscribe. If it drops, you get `reconnect`, then `reconnected`, and the topics are subscribed again.

```js title="Imported example"
import { WebsocketClient } from "bybit-api";

const wsClient = new WebsocketClient();

wsClient.on("update", (data) => {
  console.log(data);
});

wsClient.on("reconnected", (data) => {
  console.log("reconnected", data?.wsKey);
});

wsClient.subscribeV5(["kline.5.BTCUSDT"], "spot");
```

HTX spot public stream (`htx-api/examples/Spot/WebSockets/publicWs.ts`). Pass the topic list and the wsKey:

```js title="Imported example"
import { WebsocketClient, WS_KEY_MAP } from "@siebly/htx-api";

const client = new WebsocketClient();

client.on("message", (data) => {
  console.log(data);
});

client.on("reconnected", (data) => {
  console.log("reconnected", data.wsKey);
});

client.subscribe(["market.btcusdt.ticker"], WS_KEY_MAP.spotPublic);
```

Other subscribe shapes you will actually see in the examples:

- OKX: `wsClient.subscribe({ channel: 'tickers', instId: 'BTC-USDT' })`
- Bitget V3: `wsClient.subscribe({ topic: 'ticker', payload: { instType: 'spot', symbol: 'BTCUSDT' } }, WS_KEY_MAP.v3Public)` on `WebsocketClientV3`
- Gate: `ws.subscribe('spot.balances', 'spotV4')`, or a `{ topic, payload }` object plus a wsKey
- BitMart: `client.subscribe('spot/ticker:BTC_USDT', 'spot')`, then read `update`
- Coinbase Advanced Trade: `client.subscribe({ topic: 'ticker', payload: { product_ids: ['BTC-USD'] } }, 'advTradeMarketData')`
- Kraken spot: `client.subscribe({ topic: 'ticker', payload: { symbol: ['BTC/USD'] } }, WS_KEY_MAP.spotPublicV2)`

After `reconnected`, compare the stream with a REST snapshot if your app cannot tolerate a gap. The SDK resubscribes. It does not backfill trades you missed while the socket was down.



## What the SDK handles, and what stays in your app {#what-the-sdk-handles-and-what-stays-in-your-app}

The client layer is there so you do not reimplement HMAC, RSA, or Ed25519 for every call. It also sets the timestamp field the exchange expects. That is not the same as fixing a drifting clock. If you see recvWindow or timestamp errors, sync the host clock first. A few clients expose an offset or a sync option. Bybit's `enable_time_sync` is off unless you turn it on. Binance has `disableTimeSync` and `syncIntervalMs` if you choose to use them.

Credential fields differ. Read them off the constructor. These are the names the clients accept:

| Package | Key fields |
| --- | --- |
| [binance](/sdk/binance/javascript) | `api_key`, `api_secret` |
| [bybit-api](/sdk/bybit/javascript) | `key`, `secret` |
| [okx-api](/sdk/okx/javascript), [bitget-api](/sdk/bitget/javascript) | `apiKey`, `apiSecret`, `apiPass` |
| [kucoin-api](/sdk/kucoin/javascript) | `apiKey`, `apiSecret`, `apiPassphrase` |
| [coinbase-api](/sdk/coinbase/javascript) | `apiKey`, `apiSecret`, plus `apiPassphrase` on Exchange, International, and Prime. Advanced Trade can take a parsed `cdpApiKey` instead. |
| `bitmart-api` | `apiKey`, `apiSecret`, `apiMemo` |
| [gateio-api](/sdk/gate/javascript), [@siebly/htx-api](/sdk/htx/javascript), [@siebly/kraken-api](/sdk/kraken/javascript) | `apiKey`, `apiSecret` |

Load them from the environment. This is the Bybit private constructor from the SDK examples, with the secret left in the environment:

```js title="Imported example"
import { RestClientV5 } from "bybit-api";

const client = new RestClientV5({
  key: process.env.BYBIT_API_KEY,
  secret: process.env.BYBIT_API_SECRET,
});
```

Binance uses different property names:

```js title="Imported example"
import { MainClient } from "binance";

const client = new MainClient({
  api_key: process.env.BINANCE_API_KEY,
  api_secret: process.env.BINANCE_API_SECRET,
});
```

OKX also needs the passphrase you set when you created the key, and a `market` value if the account is not on global OKX. `EEA` is my.okx.com. `US` is app.okx.com. Global is the default.

```js title="Imported example"
import { RestClient } from "okx-api";

const client = new RestClient({
  apiKey: process.env.OKX_API_KEY,
  apiSecret: process.env.OKX_API_SECRET,
  apiPass: process.env.OKX_API_PASS,
});
```

WebSocket clients reconnect on their own and resubscribe to the topics you already asked for. Binance also refreshes listen keys for user-data streams. That is connection recovery. It is not a second attempt of a REST order.

If a REST call fails, the promise rejects once. Sending it again is an application decision. Repeating an order that may have reached the exchange is how you get two fills. If you do retry, use the exchange's client order id and store it before you send. Binance's `generateNewOrderId()` and Kraken's `cl_ord_id` / `cliOrdId` exist for that. They do not retry the call for you.

Rate limits work the same way. The SDK will not queue or slow your requests. When the exchange returns a limit error, stop and wait. Then send less. Test that path. A tight retry loop makes the ban worse.

## How to test without pointing at a live account {#how-to-test-without-pointing-at-a-live-account}

Public data is the first test, on every package above. You can check parsing, types, and bad-field handling before a key exists.

A practice environment is the second test, and only where the client has one. "Paper trading" in the abstract is not a flag you can pass to all ten packages.

| Package | Practice environment in the SDK |
| --- | --- |
| [binance](/sdk/binance/javascript) | `testnet: true` for the separate testnet, or `demoTrading: true` for demo trading on real market data. Demo covers spot, USD-M, and COIN-M. |
| [bybit-api](/sdk/bybit/javascript) | `testnet: true`, or `demoTrading: true` for V5 demo trading. The SDK examples note that demo trading does not support the WebSocket API. |
| [okx-api](/sdk/okx/javascript) | `demoTrading: true` |
| [bitget-api](/sdk/bitget/javascript) | `demoTrading: true` |
| `bitmart-api` | `demoTrading: true` on `FuturesClientV2` and V2 futures sockets only. Spot stays on production. |
| [gateio-api](/sdk/gate/javascript) | Futures REST via `baseUrlKey: 'futuresTestnet'`. WebSockets via `useTestnet: true`. |
| [@siebly/kraken-api](/sdk/kraken/javascript) | `testnet: true` on `DerivativesClient` only. That is Kraken's futures demo. Spot has no testnet switch in this SDK. |
| [coinbase-api](/sdk/coinbase/javascript) | `useSandbox: true` for Exchange and International only. Advanced Trade, App, Prime, and Commerce have no sandbox in this SDK. |
| [kucoin-api](/sdk/kucoin/javascript) | No testnet base URL in this SDK. |
| [@siebly/htx-api](/sdk/htx/javascript) | No testnet or demo flag in this SDK. Node.js usage wants Node 22.13.0 or newer. |

Demo and testnet fills are a way to exercise the client. They are not evidence that a strategy works.

For a private test, create a key that cannot withdraw, point the client at the practice host if there is one, and keep the secret in an env var or a secret store. Do not print keys, and do not put them in error logs. The Gate ticker example in the repo logs the account object. Skip that. Public ticker calls do not need the key at all.

Also test the boring failures: timeout, a rate-limit response, a socket drop, and a delayed reply that arrives after you have already decided to send the call again.

## Which package to install {#which-package-to-install}

These are the current npm names. HTX and Kraken are scoped. The others are not.

| Exchange | Package | REST clients |
| --- | --- | --- |
| Binance | [binance](/sdk/binance/javascript) | `MainClient`, `USDMClient`, `CoinMClient`, `PortfolioClient` |
| Bybit | [bybit-api](/sdk/bybit/javascript) | `RestClientV5` |
| OKX | [okx-api](/sdk/okx/javascript) | `RestClient` |
| Bitget | [bitget-api](/sdk/bitget/javascript) | `RestClientV3` for the unified account, `RestClientV2` if you have not moved |
| Gate | [gateio-api](/sdk/gate/javascript) | `RestClient` for spot, margin, futures, options, and the other Gate products |
| KuCoin | [kucoin-api](/sdk/kucoin/javascript) | `SpotClient`, `FuturesClient`, `BrokerClient`, `UnifiedAPIClient` |
| Coinbase | [coinbase-api](/sdk/coinbase/javascript) | `CBAdvancedTradeClient`, `CBAppClient`, `CBExchangeClient`, `CBInternationalClient`, `CBPrimeClient`, `CBCommerceClient` |
| BitMart | `bitmart-api` | `RestClient` for spot, `FuturesClientV2` for futures |
| HTX | [@siebly/htx-api](/sdk/htx/javascript) | `SpotClient`, `FuturesClient` |
| Kraken | [@siebly/kraken-api](/sdk/kraken/javascript) | `SpotClient`, `DerivativesClient`, `InstitutionalClient`, `PartnerClient` |

```bash title="Imported example"
npm install binance
npm install bybit-api
npm install okx-api
npm install bitget-api
npm install gateio-api
npm install kucoin-api
npm install coinbase-api
npm install bitmart-api
npm install @siebly/htx-api
npm install @siebly/kraken-api
```

Quickstarts for several of these are on [siebly.io/sdk](/sdk), including [Binance](/sdk/binance/javascript), [OKX](/sdk/okx/javascript), [Bitget](/sdk/bitget/javascript), [Gate](/sdk/gate/javascript), [KuCoin](/sdk/kucoin/javascript), [BitMart](/sdk/bitmart), and [HTX](/sdk/htx/javascript). Each repo also has an `examples` folder. Start with the public REST file, then the public WebSocket file, and only then a private example.

Install one package, run one public call, and read the response against the exchange docs before you add a second product.

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

### What is a crypto exchange API? {#what-is-a-crypto-exchange-api}

It is the documented interface for reading market data or sending account operations. REST is one call, one response. A WebSocket stays open and pushes events. Parameters, auth, and error bodies differ by exchange, so use that exchange's docs when you wire a call.

### How do I connect one of these APIs from Node.js? {#how-do-i-connect-one-of-these-apis-from-node-js}

Install the package for that exchange, call a public method with no key, and compare the result with the docs. Add a key only when you need a private call. Keep the secret in the environment. If that client has a testnet, demo, or sandbox flag, use it before you point at the live account.

### Should I use an SDK or call the API directly? {#should-i-use-an-sdk-or-call-the-api-directly}

The SDK is the practical choice when you want signing, timestamps, typed requests, and a WebSocket client that reconnects. Direct HTTP is reasonable when you only need one public endpoint and you are willing to own the signing code. Either way, you still read the exchange docs, and you still pace requests yourself.

### Do these SDKs handle rate limits automatically? {#do-these-sdks-handle-rate-limits-automatically}

No. Nothing in these clients queues or slows calls to stay under the exchange limit. When you receive a limit error, back off in your own code. Reconnecting a socket does not fix a REST limit, and sending the same call again immediately usually makes it worse.

### What is the difference between REST and WebSocket here? {#what-is-the-difference-between-rest-and-websocket-here}

REST answers one request. WebSocket subscriptions keep a stream open. On Binance, Bybit, OKX, Bitget V3, Gate, KuCoin, HTX, and Kraken you can also await a WebSocket API call through `WebsocketAPIClient`. Coinbase and BitMart stay on streams plus REST.

### How should I store exchange API keys in a Node.js app? {#how-should-i-store-exchange-api-keys-in-a-node-js-app}

Keep them out of source, examples, logs, and client-side bundles. Use a secret store or environment variables, and create the key with the smallest permissions that still let the app run. Leave withdrawal permission off. The field you pass into the client depends on the package: `api_key` on Binance, `key` on Bybit, `apiPass` on OKX and Bitget, `apiPassphrase` on KuCoin, `apiMemo` on BitMart.

### Can I test an integration without live orders? {#can-i-test-an-integration-without-live-orders}

Yes for public data, on every package in this guide. For private calls, use the practice host listed above when that SDK has one. KuCoin and HTX do not expose a testnet switch in these clients. Coinbase sandbox covers Exchange and International, not Advanced Trade. A demo fill only tells you the request path works.

## Related articles

- [Crypto Exchange API Timestamp Management in Node.js: A Professional Engineering Guide](/blog/crypto-exchange-api-timestamp-management-in-nodejs-a-professional-engineering-guide)
- [Mastering Crypto Exchange Request Signing in JavaScript: A 2026 Engineering Guide](/blog/mastering-crypto-exchange-request-signing-in-javascript-a-2026-engineering-guide)
- [Node.js SDK for Coinbase Advanced Trade: Integration Guide](/blog/nodejs-sdk-for-coinbase-advanced-trade-integration-guide)


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