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
| Environment | REST | WebSocket |
|---|---|---|
| Production | https://api.rocky.exchange | wss://api.rocky.exchange |
| Sandbox | provided in your Integration Pack | provided in your Integration Pack |
Request conventions
- Send every parameter in the query string, for
GET,POST, andDELETEalike. The signature verifier accepts a form-encoded body (see Canonical payload), but the Spot handlers read parameters only from the query string. APOST /api/v3/orderwhose 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, andorderIdfilters 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. orderIdand tradeidvalues are UUID strings, not integers.- Symbols are uppercase and hyphenated (
CBTC-USDCB) and are matched case-sensitively.
Endpoint security types
| Type | Meaning |
|---|---|
NONE | Public. No API key. Rate-limited per client IP. |
USER_DATA | Requires X-MBX-APIKEY and a valid signature. Rate-limited per key. |
TRADE | As 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:
- Take the raw query string (everything after
?) and the raw request body. - If only one of them is non-empty, that one is the payload. If both are non-empty, the payload is
<query>&<body>. - The payload must end with
&signature=<value>(or be exactlysignature=<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:
signaturemust 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 (
-1100beyond that). In practice, leave the body empty and put everything in the query string (see Request conventions).
timestamp and recvWindow
| Parameter | Required | Rule |
|---|---|---|
timestamp | yes | Unix milliseconds. Missing or non-numeric → -1102. |
recvWindow | no | Milliseconds. 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:
- Pre-auth per-IP flood brake (24000 weight/min) →
429 -1003 X-MBX-APIKEYpresent and well-formed →-2014- Body ≤ 64 KB and valid UTF-8 →
-1100 signaturepresent and made only of[A-Za-z0-9+/=_%-]→-1102(absent) /-1022(bad characters)timestamp/recvWindowparse and window check →-1102/-1100/-1021- Key exists and is not revoked →
-2015 - Signature verifies under the key's type →
-1022 - 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×tamp=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.
signatureis 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%3Dis 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×tamp=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×tamp=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×tamp=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).
| Tier | Who | Request weight / 1 min | Orders / 10 s | Orders / 1 min |
|---|---|---|---|---|
standard | default for every key | 1200 | 50 | 200 |
ext-mm | external market makers (assigned during onboarding) | 2400 | 100 | 600 |
house | Rocky's own market-making fleet | 6000 | 200 | 1200 |
Per-IP ceilings
| Bucket | Applies to | Ceiling |
|---|---|---|
| Public weight | unauthenticated /api/v3/* (market data) | 6000 weight / 1 min |
| Pre-auth flood brake | signed /api/v3/*, charged before signature verification | 24000 weight / 1 min |
| WebSocket connections | concurrent sockets on /ws/* and /stream | 16 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
| Weight | Endpoints |
|---|---|
| 10 | GET /api/v3/exchangeInfo, GET /api/v3/allOrders, GET /api/v3/myTrades |
| 5 | GET /api/v3/depth, GET /api/v3/klines, GET /api/v3/trades, GET /api/v3/ticker/24hr |
| 1 | everything 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
| Header | Present on | Value |
|---|---|---|
X-MBX-USED-WEIGHT-1M | every public and signed response | weight used in the current minute, for the IP (public) or the key (signed) |
X-MBX-ORDER-COUNT-10S | POST /api/v3/order responses | orders placed in the current 10 s window |
X-MBX-ORDER-COUNT-1M | POST /api/v3/order responses | orders 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
418escalation and noRetry-Afterheader. Binance escalates persistent violators to418 I'm a teapotwith an IP ban; Rocky does not. A client that keeps hitting a bucket simply keeps receiving429. Back off until the next window; do not wait for a418that never comes. - The WebSocket connection cap is enforced at the HTTP upgrade: the 17th concurrent socket from one IP gets
429 -1003instead of101.
Error codes
All Spot errors use the FAPI envelope:
{ "code": -1013, "msg": "Filter failure: NOTIONAL" }
| HTTP | code | msg | When |
|---|---|---|---|
500 | -1000 | An unknown error occurred while processing the request. | Database or downstream failure. |
429 | -1003 | Too many requests. | A rate-limit bucket is exhausted, or the per-IP WebSocket connection cap is reached. |
400 | -1013 | Filter failure: NOTIONAL | price × quantity below the symbol's minNotional (wallet-funded accounts). |
401 | -1021 | Timestamp for this request is outside of the recvWindow. | Clock drift or stale request. |
401 | -1022 | Signature for this request is not valid. | Wrong secret/key, wrong canonical payload, malformed signature. |
400 | -1100 | Illegal 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 | -1102 | Mandatory parameter was not sent, was empty/null, or malformed. | Missing symbol, quantity, price (LIMIT), orderId, timestamp, or signature. |
422 | -2010 | Account 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 | -2010 | Market order rejected: no counterparty liquidity. | MARKET order with an empty opposite side. |
422 | -2010 | Account is frozen pending settlement review. | Wallet-funded account frozen by the mirror reconciler; see On-chain Settlement. |
422 | -2011 | Order does not exist. | Unknown orderId, or the order is no longer open. |
422 | -2012 | Complete wallet enrollment (MemberAuth) before trading. | Party-bound account that has not completed MemberAuth; see Institutional Onboarding. |
400 | -2014 | API-key format invalid. | Missing or malformed X-MBX-APIKEY. |
401 | -2015 | Invalid 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
timestampvalues are Unix milliseconds. GET /api/v3/timereturns{"serverTime": <ms>}at weight 1. Computeoffset = serverTime − localNowonce at startup and after any-1021, and add it to your timestamps.- Keep
recvWindowsmall (the default5000is 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.
| Area | Rocky behaviour |
|---|---|
| Symbols | Uppercase, hyphenated, case-sensitive (CBTC-USDCB). Stream names are lowercase and keep the hyphen (cbtc-usdcb@trade). |
| Identifiers | orderId and trade id are UUID strings. Do not sort numerically or use them as fromId cursors (not supported). |
| Parameters | Read from the query string only; form bodies are not read by handlers. Unknown parameters are ignored. |
exchangeInfo | No baseAssetPrecision / quotePrecision / quoteAssetPrecision, no permissions, no PERCENT_PRICE or MAX_NUM_ORDERS filters. Precision is implied by tickSize / stepSize. |
| Fees | Fills that settle on-chain pay the standard maker/taker fee in the received asset, withheld from the on-chain leg; see Fees. |
myTrades | No 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). |
trades | Newest first (Binance returns oldest first). |
| Rate limiting | No 418 ban escalation, no Retry-After. Weights follow Rocky's cost table. Cancels are not counted as orders. |
| Order types | LIMIT (GTC) and MARKET (IOC, 5 % protective band) only. No timeInForce, quoteOrderQty, icebergQty, stopPrice, OCO. |
| Self-trade prevention | Always on: your own resting orders are skipped by the matcher, never filled against you. Not configurable. |
| Query endpoints | GET /api/v3/order returns open orders only; completed orders come from allOrders. No startTime / endTime / fromId / orderId filters on list endpoints. |
| WebSocket | No 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. |