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
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.
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
- Turn on the authenticator app (2FA) for your account. Creating a key needs a code from it.
- Open Account → API keys and choose Create API key.
- 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.
- 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.
- Enter the code from your authenticator app. Creating a key is paused for a while after a password, email or 2FA change.
- 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.
rjk_.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.
- ROJ1the fixed tag
- METHODthe HTTP method in upper case
- PATHthe path exactly as you send it, including
/api, up to the first?, not decoded - QUERYeverything after the first
?, exactly as you send it, not sorted or re-encoded; empty when there is none - TIMESTAMPthe value of
X-ROJ-TIMESTAMP - RECV_WINDOWthe value of
X-ROJ-RECV-WINDOW - 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/jsonin 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 includesserver_time_msso 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_INVALIDanswer, on purpose. If you get it, check the key, the secret and the exact bytes you signed.
Examples
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:
88899c6f71568fdc593230e617c3f28b84091ce9d22e71961690db1348c9bd70KEY="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"// 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.
symbol filter.Read permission
These need a key with Read, which holds the scopes spot.read, wallet.read and account.read.
balance is free to trade and locked is held by open orders.symbol filter.kind is resting, fill, rejected or pending.symbol, limit and before.limit and offset.limit, unread and before.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.
quoteOrderQty, a sell sends baseQty.maxSlippageBps (1 to 1000) or is rejected as a whole.baseQty and limitPrice.baseQty, limitPrice and stopPrice.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).
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.
{
"ok": false,
"msg": "Unauthorized — request timestamp outside the accepted window",
"reason": "api_key_ts_stale",
"server_time_ms": 1792000000123,
"code": "API_KEY_TIMESTAMP_INVALID"
}Authorization header and X-ROJ-APIKEY. Send only the key headers.server_time_ms from the answer to correct your clock.requiredScope names it. Permissions cannot be changed, so create a new key.Retry-After.Retry-After says and try again; cancelling orders still works. The website is not affected.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.