For developers

API documentation

Read your account and trade spot markets from your own programs. This page explains API keys, request signing, the endpoints a key can call and how your account's data is addressed.

On this page
API keys are for programs that run on your own servers. The secret signs every request, so never put it in a web page, a mobile app or a public repository. Funds leave your account only through the website, with 2FA: an API key reads balances and trades, but does not take funds out.

Overview

The Rojbit API is a REST API that uses JSON. Your program signs every request with an API key that you create on the website. There is no login step and no session to refresh.

Base URLhttps://api.rojbit.io

The full address is the base URL followed by the endpoint path, and every path starts with /api/. For example, the list of trading pairs is GET https://api.rojbit.io/api/spot/exchange-info.

The API is for server-to-server use. It does not send CORS headers for other websites, so a browser page cannot call it. Call it from your own back end.

API keys work on the REST API only. The private WebSocket stream does not accept them, so follow your orders with the order endpoints.

Requests and responses are JSON (UTF-8). Send Content-Type: application/json with a body.

When you place an order, quantities and prices are sent as JSON numbers. Responses mix types: fills and open orders return exact strings, while a spot balance returns balance as a number and locked as a string. Parse strings with a decimal type, never as a float.

Create and manage API keys

You create keys on the website, in your account. Each key has a label, permissions and an optional IP whitelist.

Create a key

  1. Turn on the authenticator app (2FA) for your account. Creating a key needs a code from it.
  2. Open Account → API keys and choose Create API key.
  3. Give the key a label and choose its permissions. Read covers balances, orders and history. Trade adds placing and cancelling spot orders and always includes Read. Withdraw is shown as not available.
  4. Add an IP whitelist (recommended): the public addresses your servers call from, separated by commas (up to 20). A key with Trade and no whitelist expires 90 days after it is created. Read-only keys and keys with a whitelist do not expire.
  5. Enter the code from your authenticator app. Creating a key is paused for a while after a password, email or 2FA change.
  6. Copy the key and the secret. The secret is shown once and never again. If you lose it, revoke the key and create a new one.

What you get

The API key is the public id of the key. It starts with rjk_live_ followed by 22 characters. You send it in the X-ROJ-APIKEY header.

The secret starts with rjs_live_ followed by 49 characters. It is never sent: you only use it to compute signatures. Keep it as private as a password.

Managing keys

  • You can rename a key. Its permissions, IP whitelist and expiry cannot be changed: create a new key and revoke the old one.
  • You can revoke a key at any time, without a code. A revoked key stops working on its very next request.
  • A password, email or 2FA change revokes all your existing keys shortly afterwards. Create new keys after the change.
  • An account can hold a limited number of active keys. The API keys page shows the limit.

Sign your requests

Every request to an endpoint that needs a key carries four headers. Rojbit recomputes the signature with your secret and refuses the request if it does not match.

Header
Description
X-ROJ-APIKEY
Your API key, the public id that starts with rjk_.
X-ROJ-TIMESTAMP
When you signed the request, in milliseconds since the Unix epoch. It may be at most 1 second ahead of our clock and at most the receive window old.
X-ROJ-RECV-WINDOW
How long the request stays valid after its timestamp, in milliseconds: 1000 to 60000 on GET, 1000 to 5000 on every other method. A value outside the range is rejected, not adjusted. 5000 is a good default.
X-ROJ-SIGNATURE
The HMAC-SHA256 signature, as 64 hexadecimal characters (lowercase or uppercase).

Do not send an Authorization header with them: a request with both is refused with 400 API_KEY_MALFORMED. X-Device-Id is not needed.

The canonical string

The signature is the HMAC-SHA256 of the canonical string, keyed with the whole secret string (UTF-8, including its rjs_ prefix and checksum). The canonical string has seven fields joined by a single line feed (LF), with no line feed at the end.

  1. ROJ1the fixed tag
  2. METHODthe HTTP method in upper case
  3. PATHthe path exactly as you send it, including /api, up to the first ?, not decoded
  4. QUERYeverything after the first ?, exactly as you send it, not sorted or re-encoded; empty when there is none
  5. TIMESTAMPthe value of X-ROJ-TIMESTAMP
  6. RECV_WINDOWthe value of X-ROJ-RECV-WINDOW
  7. BODY_SHA256the SHA-256 of the raw request body, as lowercase hexadecimal; for no body, the hash of the empty string

Things to get right

  • Serialize the JSON body once, hash those bytes and send those same bytes. Only application/json in UTF-8 is accepted; any other body is refused.
  • Keep your clock accurate. A timestamp outside the window gets 401 API_KEY_TIMESTAMP_INVALID, and the answer includes server_time_ms so you can correct the difference.
  • Sign again to retry. A signed request that is not a GET, sent again with the same signature, is refused with 401 API_KEY_REPLAYED. That includes your own retry after a timeout, because the first request may have been applied. Read your open orders and fills first, then sign a new request with a fresh timestamp. GET requests are never checked for replay.
  • A wrong key, secret or signature, a revoked or expired key, and a body that cannot be signed all get the same 401 API_KEY_INVALID answer, on purpose. If you get it, check the key, the secret and the exact bytes you signed.

Examples

Worked example with a fake key and secret, to check your own signing code against
Key:        rjk_live_xxxxxxxxxxxxxxxxxxxxxx
Secret:     rjs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Request:    GET /api/spot/open-orders?symbol=BTCUSDT (no body)
Timestamp:  1792000000000
Window:     5000

Canonical string (seven lines, no line feed after the last):
ROJ1
GET
/api/spot/open-orders
symbol=BTCUSDT
1792000000000
5000
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

X-ROJ-SIGNATURE:
88899c6f71568fdc593230e617c3f28b84091ce9d22e71961690db1348c9bd70
A signed GET request with curl and openssl
KEY="rjk_live_…"
SECRET="rjs_live_…"

TARGET="/api/spot/open-orders?symbol=BTCUSDT"   # path and query, exactly as you send them
TS=$(( $(date +%s) * 1000 ))                      # milliseconds since the epoch
RW=5000

REQ_PATH=$(printf '%s' "$TARGET" | cut -d'?' -f1)    # up to the first ?
QUERY=$(printf '%s' "$TARGET" | cut -s -d'?' -f2-)   # after the first ?, empty if none
BODY_SHA=$(printf '' | openssl dgst -sha256 | awk '{print $NF}')   # no body

CANON=$(printf 'ROJ1\nGET\n%s\n%s\n%s\n%s\n%s' "$REQ_PATH" "$QUERY" "$TS" "$RW" "$BODY_SHA")
SIG=$(printf '%s' "$CANON" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $NF}')

curl -sS "https://api.rojbit.io$TARGET" \
  -H "X-ROJ-APIKEY: $KEY" \
  -H "X-ROJ-TIMESTAMP: $TS" \
  -H "X-ROJ-RECV-WINDOW: $RW" \
  -H "X-ROJ-SIGNATURE: $SIG"
Node.js: a signed GET and POST
// roj.mjs · Node.js 18 or later
// run: ROJ_API_KEY=… ROJ_API_SECRET=… node roj.mjs
import { createHash, createHmac } from "node:crypto";

const BASE = "https://api.rojbit.io";
const API_KEY = process.env.ROJ_API_KEY; // rjk_live_…
const API_SECRET = process.env.ROJ_API_SECRET; // rjs_live_…

async function call(method, target, body) {
  const m = method.toUpperCase();
  const raw = body === undefined ? "" : JSON.stringify(body); // serialize once
  const q = target.indexOf("?");
  const path = q === -1 ? target : target.slice(0, q);
  const query = q === -1 ? "" : target.slice(q + 1);
  const timestamp = String(Date.now());
  const recvWindow = "5000";
  const bodyHash = createHash("sha256").update(raw, "utf8").digest("hex");
  const canonical = ["ROJ1", m, path, query, timestamp, recvWindow, bodyHash].join("\n");
  const signature = createHmac("sha256", API_SECRET).update(canonical, "utf8").digest("hex");

  const res = await fetch(BASE + target, {
    method: m,
    headers: {
      "X-ROJ-APIKEY": API_KEY,
      "X-ROJ-TIMESTAMP": timestamp,
      "X-ROJ-RECV-WINDOW": recvWindow,
      "X-ROJ-SIGNATURE": signature,
      ...(raw && { "Content-Type": "application/json" }),
    },
    body: raw || undefined, // the same bytes that were hashed
  });
  console.log("weight", res.headers.get("x-roj-used-weight-1m"), "of", res.headers.get("x-roj-weight-limit-1m"));
  return res.json();
}

console.log(await call("GET", "/api/wallets/spot"));
console.log(
  await call("POST", "/api/spot/limit", { symbol: "BTCUSDT", side: "buy", baseQty: 0.002, limitPrice: 50000 }),
);

Endpoints

These are the endpoints an API key can call, and the public ones that need no key. Every path starts with /api, after the base URL https://api.rojbit.io.

Public: no key needed

These need no key and no signature.

Method
Path
What it does
GET
/api/spot/exchange-info
The trading pairs and their rules (minimum order size, price and quantity filters), and whether spot trading is on. Optional symbol filter.
GET
/api/fees/schedule
The public fee schedule: maker and taker rates for every fee level.
GET
/api/status
Platform status: the components, incidents and maintenance shown on the status page. Poll at most about once a minute.

Read permission

These need a key with Read, which holds the scopes spot.read, wallet.read and account.read.

Method
Path
What it does
GET
/api/wallets/spot
Your spot balances, one row per asset: balance is free to trade and locked is held by open orders.
GET
/api/wallets/funding
Your funding wallet balances, one row per asset.
GET
/api/spot/open-orders
Your open (resting) spot orders. Optional symbol filter.
GET
/api/spot/orders/{clientOrderId}
The status of one order. In the answer, kind is resting, fill, rejected or pending.
GET
/api/spot/fills
Your fills, newest first, in pages. Optional symbol, limit and before.
GET
/api/history/transactions
Your transaction history, newest first. Paged with limit and offset.
GET
/api/fees/tier
Your current fee level and its maker and taker rates.
GET
/api/notifications
Your notifications. Optional limit, unread and before.
GET
/api/legal
Which legal documents you have accepted. Accepting them is done on the website.

Trade permission

These need a key with Trade, which holds spot.trade and everything in Read. An accepted order is answered with HTTP 202 and settles afterwards.

Method
Path
What it does
POST
/api/spot/order
Market order. A buy sends quoteOrderQty, a sell sends baseQty.
POST
/api/spot/instant
Market order that fills within maxSlippageBps (1 to 1000) or is rejected as a whole.
POST
/api/spot/limit
Limit order with baseQty and limitPrice.
POST
/api/spot/stop-limit
Stop-limit order with baseQty, limitPrice and stopPrice.
POST
/api/spot/orders/{clientOrderId}/cancel
Cancel one of your open orders.

Not available to API keys

Every other endpoint answers 403 API_KEY_NOT_PERMITTED: withdrawals, transfers, saved addresses, deposit addresses, deposit and withdrawal history, security settings, identity verification, profile, price alerts, support tickets, referrals and key management. Do these on the website.

Account references

An API key belongs to one account, and the account is implied by the key. No endpoint takes an account number, a user id or a UID, and a userId in a body or query string is ignored.

Your UID

Your UID is the number shown in Account → Profile and in the account menu. Quote it when you contact support. It is for people, not a key or a secret: you never send it to the API, and API keys cannot call the profile endpoint that returns it. Show it exactly as it is and do not read a meaning into its digits.

Balances

GET /api/wallets/spot and GET /api/wallets/funding return one row per asset, identified by its ticker, for example USDT. In the spot wallet, balance is free to trade and locked is held by your open orders. Added together they are your total for that asset.

Orders

Every order has an id named clientOrderId. Rojbit creates it and returns it when it accepts your order (HTTP 202, status accepted). Use it to read that order and to cancel it with the order endpoints above. GET /api/spot/open-orders lists your open orders.

Trades

GET /api/spot/fills lists your fills, newest first. Each fill carries clientOrderId (the id of its order, which can be null), symbol, side and liquidity (maker or taker). For the next, older page, send the nextCursor value of the answer as before; when it is null there are no more.

Trading pairs

A pair is written as one word in the API, for example BTCUSDT. Use it as the symbol.

Limits and response headers

Each key has three budgets: request weight per minute, and order placements per 10 seconds and per day. Every request costs weight: a single read costs little and a history page costs more. The limits can change, and yours are always in the response headers.

Every response to a key-signed request carries these headers (all integers).

Header
Description
X-ROJ-USED-WEIGHT-1M
Request weight used in the last 60 seconds.
X-ROJ-WEIGHT-LIMIT-1M
This key's weight limit per 60 seconds.
X-ROJ-ORDER-COUNT-10S
Orders placed in the last 10 seconds.
X-ROJ-ORDER-LIMIT-10S
This key's order limit per 10 seconds.
X-ROJ-ORDER-COUNT-1D
Orders placed in the last 24 hours.
X-ROJ-ORDER-LIMIT-1D
This key's order limit per 24 hours.

When a budget is used up, the answer is 429 with a Retry-After header (in seconds) and a body whose code is RATE_LIMITED and whose bucket names the limit that was exceeded. A refused request is not charged. Cancelling an order is counted but is never refused for being over a limit.

Failed authentication is limited too. After repeated failures from one IP address or for one key, further key requests get 429 with Retry-After for a few minutes. Fix your signer and wait: retrying a failing signature only extends the wait.

Errors and reject codes

A failed request returns a JSON body: ok is false and code is a stable code. Branch on ok and code, never on the message text, which can change. The message is in msg or error. The 429 body has no ok field, so check the status code for it.

Example: a request signed too late
{
  "ok": false,
  "msg": "Unauthorized — request timestamp outside the accepted window",
  "reason": "api_key_ts_stale",
  "server_time_ms": 1792000000123,
  "code": "API_KEY_TIMESTAMP_INVALID"
}
Code
HTTP
What it means and what to do
API_KEY_MALFORMED
400
You sent both an Authorization header and X-ROJ-APIKEY. Send only the key headers.
API_KEY_INVALID
401
The key could not be authenticated: unknown, revoked or expired key, wrong signature, malformed headers or a body that cannot be signed. All of these give the same answer on purpose. Check the key, the secret and the bytes you signed.
API_KEY_TIMESTAMP_INVALID
401
The timestamp is too far ahead or older than the receive window, or the window is out of range. Use server_time_ms from the answer to correct your clock.
API_KEY_REPLAYED
401
This exact signed request was already received. Read the current state, then sign a new request with a fresh timestamp.
API_KEY_NOT_PERMITTED
403
This endpoint cannot be called with an API key.
API_KEY_SCOPE_DENIED
403
The key does not have the permission the endpoint needs; requiredScope names it. Permissions cannot be changed, so create a new key.
API_KEY_IP_NOT_ALLOWED
403
The key has an IP whitelist and the request came from another address, or its address could not be determined.
RATE_LIMITED
429
A limit was exceeded. Wait for the number of seconds in Retry-After.
API_KEYS_PAUSED
503
Rojbit has paused API key access for now. Wait as Retry-After says and try again; cancelling orders still works. The website is not affected.
API_KEY_CHECK_UNAVAILABLE
503
The key could not be checked right now. It is safe to try again.
SPOT_TRADING_DISABLED
503
Spot trading is switched off for now. Orders are refused until it is turned on again.

Orders are accepted first and settled afterwards. If an order cannot be filled, the order status endpoint answers with kind set to rejected and a code such as SPOT_INSUFFICIENT_FUNDS, SPOT_SLIPPAGE_EXCEEDED or SPOT_HALTED.

Order endpoints can also answer 403 with an account status code, for example ACCOUNT_FROZEN, GEO_RESTRICTED_IP or LEGAL_ACCEPTANCE_REQUIRED. These are resolved on the website, not through the API.

Treat a code you do not know by its HTTP status: an unknown 4xx means fix the request, a 5xx means try again later.

Questions about the API?Contact us