Skip to main content

Basic Information

Status: Implemented.

Operational facts that apply to every Spot API endpoint: base URLs, request conventions, both signature schemes with copy-paste examples, rate limits, error codes, and the consolidated list of differences from Binance Spot.

Base URLs​

EnvironmentRESTWebSocket
Productionhttps://api.rocky.exchangewss://api.rocky.exchange
Sandboxprovided in your Integration Packprovided in your Integration Pack

Request conventions​

  • Send every parameter in the query string, for GET, POST, and DELETE alike. The signature verifier accepts a form-encoded body (see Canonical payload), but the Spot handlers read parameters only from the query string. A POST /api/v3/order whose parameters are in the body will verify and then be rejected by the framework with a plain-text HTTP 400 (not a {code,msg} body). This is stricter than Binance, which reads either.
  • Unknown parameters are ignored. Binance-only parameters such as timeInForce, quoteOrderQty, startTime, endTime, fromId, and orderId filters on list endpoints have no effect unless a page says otherwise.
  • Responses are application/json. All decimals (prices, quantities, balances) are strings; timestamps are Unix milliseconds as JSON numbers.
  • orderId and trade id values are UUID strings, not integers.
  • Symbols are uppercase and hyphenated (CBTC-USDCB) and are matched case-sensitively.

Endpoint security types​

TypeMeaning
NONEPublic. No API key. Rate-limited per client IP.
USER_DATARequires X-MBX-APIKEY and a valid signature. Rate-limited per key.
TRADEAs USER_DATA, and additionally charged to the order-count buckets.

Authentication​

Signed endpoints require three things: the X-MBX-APIKEY header, a timestamp parameter, and a signature parameter that is the last parameter in the canonical payload. The key's type — HMAC or ED25519 — is fixed when the key is issued and selects which verifier runs; the request format is otherwise identical.

API key header​

X-MBX-APIKEY: <key>

The key is 1–256 characters from [A-Za-z0-9_-]. Anything else (or a missing header) is rejected with -2014.

Canonical payload​

The string that is signed is built from the request as received:

  1. Take the raw query string (everything after ?) and the raw request body.
  2. If only one of them is non-empty, that one is the payload. If both are non-empty, the payload is <query>&<body>.
  3. The payload must end with &signature=<value> (or be exactly signature=<value> when nothing else is sent). The verifier splits on the last &signature=; what precedes it is the string you must have signed, byte for byte.

Consequences:

  • signature must be the final parameter. Anything after it becomes part of the signature value and fails verification.
  • Sign the exact bytes you send. Do not re-encode, reorder, or add parameters after signing. Percent-encoding is part of the payload; an HTTP library that encodes differently from your signer produces -1022.
  • Request bodies are limited to 64 KB (-1100 beyond that). In practice, leave the body empty and put everything in the query string (see Request conventions).

timestamp and recvWindow​

ParameterRequiredRule
timestampyesUnix milliseconds. Missing or non-numeric → -1102.
recvWindownoMilliseconds. Default 5000, maximum 60000. Negative, non-numeric, or above the maximum → -1100.

The request is accepted only if |serverTime − timestamp| ≤ recvWindow; otherwise -1021. The check runs before the key lookup, so a clock problem is reported as -1021 even if the key is also wrong. Use GET /api/v3/time to measure your offset (see Time synchronization).

Verification order​

Checks run in this order, and the first failure is what you receive:

  1. Pre-auth per-IP flood brake (24000 weight/min) → 429 -1003
  2. X-MBX-APIKEY present and well-formed → -2014
  3. Body ≤ 64 KB and valid UTF-8 → -1100
  4. signature present and made only of [A-Za-z0-9+/=_%-] → -1102 (absent) / -1022 (bad characters)
  5. timestamp / recvWindow parse and window check → -1102 / -1100 / -1021
  6. Key exists and is not revoked → -2015
  7. Signature verifies under the key's type → -1022
  8. Per-key request-weight and order-count buckets → 429 -1003

HMAC-SHA256 keys​

For a key of type HMAC you hold a shared secret. signature is the lowercase hex HMAC-SHA256 of the canonical payload keyed with the secret. The comparison is byte-exact, so uppercase hex is rejected.

Golden vector (the Binance documentation vector; the gateway's own test suite pins it):

secret = NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j
payload = symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000&timestamp=1499827319559
HMAC-SHA256 hex = c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71

Python:

import hashlib, hmac, time, urllib.parse, requests

BASE = "https://api.rocky.exchange"
API_KEY = "your_key"
API_SECRET = "your_secret"

def signed_query(params: dict) -> str:
params = {**params, "timestamp": int(time.time() * 1000)}
payload = urllib.parse.urlencode(params)
sig = hmac.new(API_SECRET.encode(), payload.encode(), hashlib.sha256).hexdigest()
return f"{payload}&signature={sig}"

headers = {"X-MBX-APIKEY": API_KEY}

# USER_DATA: GET /api/v3/account
r = requests.get(f"{BASE}/api/v3/account?{signed_query({})}", headers=headers)
print(r.status_code, r.headers.get("x-mbx-used-weight-1m"), r.json())

# TRADE: POST /api/v3/order — parameters go in the query string, body stays empty
q = signed_query({"symbol": "CBTC-USDCB", "side": "BUY", "type": "LIMIT",
"quantity": "0.001", "price": "50000"})
r = requests.post(f"{BASE}/api/v3/order?{q}", headers=headers)
print(r.status_code, r.json())

Ed25519 keys​

For a key of type ED25519 you generate the keypair yourself and register only the public key (32 bytes, base64). Rocky never sees the private key. This is the recommended scheme for institutional integrations.

  • signature is the 64-byte Ed25519 signature of the canonical payload (the same string an HMAC key would sign), encoded in base64. Standard (+/=) and URL-safe (-_) alphabets are both accepted, and the value may be percent-encoded — a query-string + / = arriving as %2B %2F %3D is decoded before verification.
  • The signed payload is the raw bytes of the canonical string; there is no hashing step on your side.
  • Verification uses strict Ed25519 (RFC 8032). Any decode failure or mismatch is -1022.

Reference vector, identical to the gateway's unit test (fapi/sign.rs). Ed25519 is deterministic, so a correct implementation reproduces the signature exactly:

seed (32 bytes) = 0x07 repeated 32 times
public key base64 = 6kpsY+KcUgq+9VB7Ey7F+ZVHdq6+vnuSQh7qaRRG0iw=
payload = symbol=CBTC-USDCB&side=BUY&type=LIMIT&quantity=0.001&price=50000&timestamp=1756600000000
signature base64 = chwbFjh1nJSjnj+i0fFMMmtTUigvPTjwk5maFhIFWrf24/xNQohyGlGAhXQS6ciKveL7pQQ7Gy4VfodWpB9fDQ==

Python with PyNaCl:

import base64, time, urllib.parse, requests
from nacl.signing import SigningKey

BASE = "https://api.rocky.exchange"
API_KEY = "your_key"
seed = bytes([7] * 32) # replace with your own 32-byte seed, kept secret
sk = SigningKey(seed)
print("register this public key:", base64.b64encode(bytes(sk.verify_key)).decode())

def sign(payload: str) -> str:
return base64.b64encode(sk.sign(payload.encode()).signature).decode()

# Self-test against the reference vector
vector = "symbol=CBTC-USDCB&side=BUY&type=LIMIT&quantity=0.001&price=50000&timestamp=1756600000000"
assert sign(vector) == "chwbFjh1nJSjnj+i0fFMMmtTUigvPTjwk5maFhIFWrf24/xNQohyGlGAhXQS6ciKveL7pQQ7Gy4VfodWpB9fDQ=="

params = {"symbol": "CBTC-USDCB", "side": "BUY", "type": "LIMIT",
"quantity": "0.001", "price": "50000", "timestamp": int(time.time() * 1000)}
payload = urllib.parse.urlencode(params)
# GET/DELETE and POST alike: query string, signature last, percent-encode the base64
url = f"{BASE}/api/v3/order?{payload}&signature={urllib.parse.quote(sign(payload), safe='')}"
r = requests.post(url, headers={"X-MBX-APIKEY": API_KEY})
print(r.status_code, r.json())

Python with cryptography:

import base64, time, urllib.parse, requests
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

BASE = "https://api.rocky.exchange"
API_KEY = "your_key"
sk = Ed25519PrivateKey.from_private_bytes(bytes([7] * 32)) # your secret seed
pk = sk.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
print("register this public key:", base64.b64encode(pk).decode())

def sign(payload: str) -> str:
return base64.b64encode(sk.sign(payload.encode())).decode()

vector = "symbol=CBTC-USDCB&side=BUY&type=LIMIT&quantity=0.001&price=50000&timestamp=1756600000000"
assert sign(vector) == "chwbFjh1nJSjnj+i0fFMMmtTUigvPTjwk5maFhIFWrf24/xNQohyGlGAhXQS6ciKveL7pQQ7Gy4VfodWpB9fDQ=="

payload = urllib.parse.urlencode({"timestamp": int(time.time() * 1000)})
url = f"{BASE}/api/v3/account?{payload}&signature={urllib.parse.quote(sign(payload), safe='')}"
r = requests.get(url, headers={"X-MBX-APIKEY": API_KEY})
print(r.status_code, r.json())

Generating a key to register (either library above prints it; with OpenSSL):

openssl genpkey -algorithm ed25519 -out spot-ed25519.pem
# 32-byte raw public key, base64 — this is the value you hand to Rocky
openssl pkey -in spot-ed25519.pem -pubout -outform DER | tail -c 32 | base64

How the public key is registered, and how its fingerprint is confirmed out of band, is in Institutional Onboarding.

Rate limits​

Rate limiting applies to every /api/v3/* request. It is Binance-shaped: fixed, wall-clock-aligned windows; a request-weight bucket and two order-count buckets; usage reported in response headers; HTTP 429 with -1003 when a bucket is exhausted.

Per-key tiers (signed endpoints)​

Each API key carries a tier. The ceilings below are the defaults; the values in force are advertised by GET /api/v3/exchangeInfo (rateLimits, standard tier).

TierWhoRequest weight / 1 minOrders / 10 sOrders / 1 min
standarddefault for every key120050200
ext-mmexternal market makers (assigned during onboarding)2400100600
houseRocky's own market-making fleet60002001200

Per-IP ceilings​

BucketApplies toCeiling
Public weightunauthenticated /api/v3/* (market data)6000 weight / 1 min
Pre-auth flood brakesigned /api/v3/*, charged before signature verification24000 weight / 1 min
WebSocket connectionsconcurrent sockets on /ws/* and /stream16 per IP

The client IP is the X-Real-IP set by Rocky's edge proxy. If your firm quotes from several processes behind one NAT, budget the shared per-IP brake accordingly. Exemption from the IP buckets is reserved for Rocky's own infrastructure hosts and is not granted to external participants; per-key tiers always apply.

Request weights​

WeightEndpoints
10GET /api/v3/exchangeInfo, GET /api/v3/allOrders, GET /api/v3/myTrades
5GET /api/v3/depth, GET /api/v3/klines, GET /api/v3/trades, GET /api/v3/ticker/24hr
1everything else, including POST / GET / DELETE /api/v3/order, openOrders, account, ping, time

Weights are priced by Rocky's actual serving cost and do not follow Binance's table (for example depth is 5 regardless of limit).

Only POST /api/v3/order is counted against the order buckets. Cancels are not counted, so a market maker can always pull quotes. A placement rejected by the order bucket has still spent its request weight.

Response headers​

HeaderPresent onValue
X-MBX-USED-WEIGHT-1Mevery public and signed responseweight used in the current minute, for the IP (public) or the key (signed)
X-MBX-ORDER-COUNT-10SPOST /api/v3/order responsesorders placed in the current 10 s window
X-MBX-ORDER-COUNT-1MPOST /api/v3/order responsesorders placed in the current minute

Being throttled​

HTTP/1.1 429 Too Many Requests
X-MBX-USED-WEIGHT-1M: 1203

{"code":-1003,"msg":"Too many requests."}
  • Windows are fixed, not sliding. Usage resets at the top of each aligned window, and a burst across a window boundary can briefly reach 2× the ceiling — the same behaviour as Binance.
  • No 418 escalation and no Retry-After header. Binance escalates persistent violators to 418 I'm a teapot with an IP ban; Rocky does not. A client that keeps hitting a bucket simply keeps receiving 429. Back off until the next window; do not wait for a 418 that never comes.
  • The WebSocket connection cap is enforced at the HTTP upgrade: the 17th concurrent socket from one IP gets 429 -1003 instead of 101.

Error codes​

All Spot errors use the FAPI envelope:

{ "code": -1013, "msg": "Filter failure: NOTIONAL" }
HTTPcodemsgWhen
500-1000An unknown error occurred while processing the request.Database or downstream failure.
429-1003Too many requests.A rate-limit bucket is exhausted, or the per-IP WebSocket connection cap is reached.
400-1013Filter failure: NOTIONALprice × quantity below the symbol's minNotional (wallet-funded accounts).
401-1021Timestamp for this request is outside of the recvWindow.Clock drift or stale request.
401-1022Signature for this request is not valid.Wrong secret/key, wrong canonical payload, malformed signature.
400-1100Illegal characters found in a parameter.Also: invalid side, unsupported type, price on a MARKET order, non-UUID orderId, recvWindow out of range, invalid parameter rejected by the trading service.
400-1102Mandatory parameter was not sent, was empty/null, or malformed.Missing symbol, quantity, price (LIMIT), orderId, timestamp, or signature.
422-2010Account has insufficient balance for requested action.Insufficient free balance to lock. Also returned with this msg when the market is inactive or order entry is paused by the risk gate (both surface as the same downstream precondition failure).
422-2010Market order rejected: no counterparty liquidity.MARKET order with an empty opposite side.
422-2010Account is frozen pending settlement review.Wallet-funded account frozen by the mirror reconciler; see On-chain Settlement.
422-2011Order does not exist.Unknown orderId, or the order is no longer open.
422-2012Complete wallet enrollment (MemberAuth) before trading.Party-bound account that has not completed MemberAuth; see Institutional Onboarding.
400-2014API-key format invalid.Missing or malformed X-MBX-APIKEY.
401-2015Invalid API-key, IP, or permissions for action.Unknown or revoked key.

Note that -2010 is reused for three distinct rejections, exactly as Binance reuses it for order-rejected reasons; distinguish them by msg.

Time synchronization​

  • All timestamp values are Unix milliseconds.
  • GET /api/v3/time returns {"serverTime": <ms>} at weight 1. Compute offset = serverTime − localNow once at startup and after any -1021, and add it to your timestamps.
  • Keep recvWindow small (the default 5000 is right for a co-located client). Enlarging it hides clock drift and widens the replay window of a captured request.

Differences from Binance​

Consolidated list; each item is also noted on the affected page.

AreaRocky behaviour
SymbolsUppercase, hyphenated, case-sensitive (CBTC-USDCB). Stream names are lowercase and keep the hyphen (cbtc-usdcb@trade).
IdentifiersorderId and trade id are UUID strings. Do not sort numerically or use them as fromId cursors (not supported).
ParametersRead from the query string only; form bodies are not read by handlers. Unknown parameters are ignored.
exchangeInfoNo baseAssetPrecision / quotePrecision / quoteAssetPrecision, no permissions, no PERCENT_PRICE or MAX_NUM_ORDERS filters. Precision is implied by tickSize / stepSize.
FeesFills that settle on-chain pay the standard maker/taker fee in the received asset, withheld from the on-chain leg; see Fees.
myTradesNo orderId field. Two extra fields: settlementStatus and cantonUpdateId. internal means the fill did not settle on-chain (house-bot segment, or executed before on-chain settlement was enabled).
tradesNewest first (Binance returns oldest first).
Rate limitingNo 418 ban escalation, no Retry-After. Weights follow Rocky's cost table. Cancels are not counted as orders.
Order typesLIMIT (GTC) and MARKET (IOC, 5 % protective band) only. No timeInForce, quoteOrderQty, icebergQty, stopPrice, OCO.
Self-trade preventionAlways on: your own resting orders are skipped by the matcher, never filled against you. Not configurable.
Query endpointsGET /api/v3/order returns open orders only; completed orders come from allOrders. No startTime / endTime / fromId / orderId filters on list endpoints.
WebSocketNo SUBSCRIBE / UNSUBSCRIBE control messages (the stream list is fixed at the URL), no listenKey user-data streams, no diff-depth, no @aggTrade / @bookTicker / !ticker@arr, klines only at 1m.