General Info
Environments and base URLs
| Purpose | URL |
|---|---|
| REST | https://trade-api.yellowboxmarkets.com — all paths are prefixed /v1, so the effective base is https://trade-api.yellowboxmarkets.com/v1 |
| WebSocket market streams | wss://trade-stream.yellowboxmarkets.com/ws (raw) and wss://trade-stream.yellowboxmarkets.com/stream?streams= (combined) |
| WebSocket user data stream | wss://trade-stream.yellowboxmarkets.com/ws/<listenKey> |
Sandbox is not a separate host. A key can be scoped to demo-group MT5 logins and is then
functionally a testnet key on the same endpoints. GET /v1/accounts reports "demo": true per
login.
Request format
GETandDELETEtake parameters in the query string.POSTandPUTtake parameters in the query string, in anapplication/x-www-form-urlencodedbody, or both.- If a parameter appears in both, the query string value wins.
- Parameters may be sent in any order.
- Responses are always JSON (
Content-Type: application/json), including error responses. - All requests must be HTTPS. Plain HTTP is refused.
Security types
Every endpoint declares a security type. It tells you what must accompany the request.
| Security type | X-YBX-APIKEY header |
timestamp + signature |
|---|---|---|
NONE |
no | no |
MARKET_DATA |
yes | no |
USER_STREAM |
yes | no |
USER_DATA |
yes | yes |
TRADE |
yes | yes |
TRADE and USER_DATA are cryptographically identical. They differ in the permission the key
must hold: a read-only key can call USER_DATA endpoints but is refused (-2015) on TRADE
endpoints. Ask for a read-only key for any component that only needs to observe.
MARKET_DATA needs the key so the request can be attributed to a rate-limit bucket. It is not
account-scoped and takes no login.
The login parameter
This is the one deliberate departure from Binance. On Binance an API key is an account. Here one key normally trades many MT5 accounts, so the account cannot be inferred from the key.
A login that IS in scope but has trading disabled is a different case, and is deliberately not
hidden: reads succeed and report tradeAllowed: false, while anything that would trade is refused
with -2024. That is what makes tradeAllowed on
GET /v1/account a value you can act on rather than an
error you have to provoke.
GET /v1/accountslists exactly the logins the key may trade.POST /v1/batchOrderscarriesloginper item, so one call can fan a signal out across many accounts.- The user-data stream is per key, not per login: one connection carries events for every login
in scope and each event carries
L(the login). - Market-data endpoints and market streams take no
login.
SIGNED endpoint security (TRADE and USER_DATA)
SIGNED endpoints require two extra parameters:
| Name | Type | Mandatory | Description |
|---|---|---|---|
timestamp |
LONG | YES | Request creation time, Unix milliseconds. Missing is -1102; not an integer is -1130. |
signature |
STRING | YES | Lowercase hex HMAC-SHA256 of totalParams, keyed with your API secret. |
recvWindow |
LONG | NO | How long the request stays valid, in milliseconds. Default 5000, maximum 60000; outside 1–60000 is -1130. |
totalParams
totalParams = <query string> + <request body>Concatenated verbatim, in that order, exactly as transmitted — no separator is inserted, and the parameters are the percent-encoded bytes you actually put on the wire. Sign last, and never re-order or re-encode a parameter after signing.
signature itself is never part of totalParams. Send it last, either in the query string or in
the body.
Because concatenation is literal, splitting parameters between the query string and the body
produces a different totalParams than sending the same parameters in one place. Send all
parameters in one place. The mixed case is supported for Binance-client compatibility, not
recommended.
Timing security
The server computes:
timestamp < serverTime + 1000 && serverTime - timestamp <= recvWindowBoth conditions must hold, otherwise -1021. The +1000 ms tolerance absorbs a small clock lead;
recvWindow bounds how stale a request may be.
Keep recvWindow small. A large window widens the replay window of a request captured in flight.
5000 is right for almost everyone. Sync your clock against GET /v1/time, not against a public
NTP pool, and resync if you see -1021 clustering.
Worked example
Use these exact values to validate your signing code.
apiKey : Tq7sVn2LpZ4wKdRj9xYc3BmHfA6eQu1oGi0tNsXrWvJbEyPzMkCdHlFgAoUvQ
secret : x9Tq2mWvJb4rZ8kLpN6yHc3sVd1fGaQeR7uI0oP5tYbXnMwKjSzCvBhDlFgAExample 1 — all parameters in the query string (GET /v1/account)
queryString : login=100123&recvWindow=5000×tamp=1789012345678
signature : 13c783f085a1ef46b6a0e1df8d59c70ab50a4f8e669ffa4a2bc49998c0a9255bReproduce it:
echo -n "login=100123&recvWindow=5000×tamp=1789012345678" \
| openssl dgst -sha256 -hmac "x9Tq2mWvJb4rZ8kLpN6yHc3sVd1fGaQeR7uI0oP5tYbXnMwKjSzCvBhDlFgA"SHA2-256(stdin)= 13c783f085a1ef46b6a0e1df8d59c70ab50a4f8e669ffa4a2bc49998c0a9255bcurl -H "X-YBX-APIKEY: Tq7sVn2LpZ4wKdRj9xYc3BmHfA6eQu1oGi0tNsXrWvJbEyPzMkCdHlFgAoUvQ" \
"https://trade-api.yellowboxmarkets.com/v1/account?login=100123&recvWindow=5000×tamp=1789012345678&signature=13c783f085a1ef46b6a0e1df8d59c70ab50a4f8e669ffa4a2bc49998c0a9255b"Example 2 — all parameters in the body (POST /v1/order)
requestBody : login=100123&symbol=XAUUSD&side=BUY&type=MARKET&volume=0.10&newClientOrderId=bot-001&recvWindow=5000×tamp=1789012345678
signature : b7f6052aa37e7d6de935cce1c099f8103a2b0a0711b77fdf48becfc8b7d2c0e7echo -n "login=100123&symbol=XAUUSD&side=BUY&type=MARKET&volume=0.10&newClientOrderId=bot-001&recvWindow=5000×tamp=1789012345678" \
| openssl dgst -sha256 -hmac "x9Tq2mWvJb4rZ8kLpN6yHc3sVd1fGaQeR7uI0oP5tYbXnMwKjSzCvBhDlFgA"SHA2-256(stdin)= b7f6052aa37e7d6de935cce1c099f8103a2b0a0711b77fdf48becfc8b7d2c0e7curl -X POST \
-H "X-YBX-APIKEY: Tq7sVn2LpZ4wKdRj9xYc3BmHfA6eQu1oGi0tNsXrWvJbEyPzMkCdHlFgAoUvQ" \
-d "login=100123&symbol=XAUUSD&side=BUY&type=MARKET&volume=0.10&newClientOrderId=bot-001&recvWindow=5000×tamp=1789012345678&signature=b7f6052aa37e7d6de935cce1c099f8103a2b0a0711b77fdf48becfc8b7d2c0e7" \
"https://trade-api.yellowboxmarkets.com/v1/order"Example 3 — mixed query string and body (the same parameters as Example 2, split)
queryString : login=100123&symbol=XAUUSD
requestBody : side=BUY&type=MARKET&volume=0.10&newClientOrderId=bot-001&recvWindow=5000×tamp=1789012345678
totalParams : login=100123&symbol=XAUUSDside=BUY&type=MARKET&volume=0.10&newClientOrderId=bot-001&recvWindow=5000×tamp=1789012345678
signature : 2cdb79a28695a12db0f60b35af89e340588d1428b97e291cb013dbff437ac392Note the missing & between XAUUSD and side — that is the literal concatenation, and it is why
the signature differs from Example 2 even though the parameters are identical. Send everything in
one place and this case never arises.
Python
import hashlib
import hmac
import time
import urllib.parse
import requests
BASE = "https://trade-api.yellowboxmarkets.com"
API_KEY = "Tq7sVn2LpZ4wKdRj9xYc3BmHfA6eQu1oGi0tNsXrWvJbEyPzMkCdHlFgAoUvQ"
API_SECRET = b"x9Tq2mWvJb4rZ8kLpN6yHc3sVd1fGaQeR7uI0oP5tYbXnMwKjSzCvBhDlFgA"
def sign(params: dict) -> str:
# Encode once, sign exactly those bytes, send exactly those bytes.
query = urllib.parse.urlencode(params)
signature = hmac.new(API_SECRET, query.encode(), hashlib.sha256).hexdigest()
return f"{query}&signature={signature}"
def signed_post(path: str, params: dict):
params = {**params, "recvWindow": 5000, "timestamp": int(time.time() * 1000)}
return requests.post(
BASE + path,
headers={
"X-YBX-APIKEY": API_KEY,
"Content-Type": "application/x-www-form-urlencoded",
},
data=sign(params),
timeout=10,
).json()
print(signed_post("/v1/order", {
"login": 100123,
"symbol": "XAUUSD",
"side": "BUY",
"type": "MARKET",
"volume": "0.10",
"newClientOrderId": "bot-001",
}))The sign() helper signs the already-encoded string and sends that exact string as the body. Do
not pass a dict to requests after signing a separately-built string — the library may re-encode
in a different order and the signature will not match.
Rate limits
Limits are enforced per API key across all IPs.
| Bucket | Limit | Counts |
|---|---|---|
REQUEST_WEIGHT |
1200 per minute | Every request, by its endpoint weight. |
ORDERS |
100 per 10 seconds | Every order-affecting request. |
ORDERS |
1200 per minute | Same. |
These numbers are tunable configuration, not contract. They may be raised or lowered per key.
GET /v1/exchangeInfo returns the live values in rateLimits[] when you send your
X-YBX-APIKEY header on it (without a key it returns the defaults) — read them at start-up rather
than hard-coding these figures.
An order-affecting request is one that places, modifies, cancels or closes something:
POST /v1/order, PUT /v1/order, DELETE /v1/order, PUT /v1/position, DELETE /v1/position,
DELETE /v1/allOpenPositions, POST /v1/batchOrders. DELETE /v1/allOpenPositions counts as
one regardless of how many positions it closes; POST /v1/batchOrders counts as one per
item.
Headers
Every response to a request made with an API key carries that key's current usage (a NONE
endpoint called without a key is not metered and carries none):
| Header | Meaning |
|---|---|
X-YBX-USED-WEIGHT-1M |
Request weight used in the current minute. |
X-YBX-ORDER-COUNT-10S |
Order-affecting requests in the current 10-second window. |
X-YBX-ORDER-COUNT-1M |
Order-affecting requests in the current minute. |
Track these. Back off before you hit the limit rather than after.
Breaching a limit
| Status | Meaning |
|---|---|
429 |
A limit was exceeded. The response carries Retry-After (seconds). Stop sending until it elapses. |
418 |
You kept sending after a 429. The key is temporarily banned. |
Bans escalate with repeated offences — from minutes to hours to a day. Retry-After always carries
the remaining ban. A 418 is not a transport error: retrying through it lengthens the ban.
Use the WebSocket streams instead of polling. Repeatedly polling GET /v1/positions or
GET /v1/ticker/price is the usual way integrators reach these limits; the tick, kline and
user-data streams carry the same information without consuming request weight.
HTTP status codes
| Status | Meaning |
|---|---|
2XX |
Success. |
4XX |
Malformed request. The problem is on your side; do not blind-retry. |
429 |
Rate limit breached. Back off. |
418 |
Key temporarily banned for ignoring 429. |
5XX |
Internal error. This is a problem on our side. It is not an indication that the operation failed. |
The two meanings of 503
503 carries a body. Read msg before deciding what to do.
{"code":-1000,"msg":"Unknown error, please check your request or try again later. The execution status is UNKNOWN and could have been a success."}The request may have reached the trade server and may have executed. Do not retry blindly.
(This message is reserved: the current implementation reports an unknown outcome as -5008 or
-5009 instead — see error-codes.md § Reading -5008 and -5009.
Handle all three the same way.)
Confirm first: watch for a DEAL event on the user data stream, or call
GET /v1/order?origClientOrderId= with the same client order id. If you always set
newClientOrderId, a retry is safe anyway — it is idempotent and returns the stored outcome.
{"code":-1000,"msg":"Service is currently unavailable, please try again later."}The service refused the request before doing anything. Retry with exponential backoff.
Error response format
Every error, at any status, has the same shape:
{"code":-1121,"msg":"Invalid symbol."}| Field | Type | Description |
|---|---|---|
code |
INT | Negative error code. Stable — safe to branch on. |
msg |
STRING | Human-readable English text. Informational; do not parse or branch on it. |
mt5RetCode |
INT | Execution errors only. The raw MT5 trade-request return code, passed through unchanged. |
An execution error looks like this:
{"code":-2018,"msg":"Insufficient margin on the trading account.","mt5RetCode":10019}mt5RetCode is present only when the trade server actually answered. Its values are listed in
enums.md § MT5 request return codes. The full error
list is in Error Codes.
Data conventions
Decimals are strings
Every decimal value — prices, volumes, money — is serialised as a JSON string, so no precision is lost by a JSON parser that stores numbers as IEEE doubles.
{"volume":"0.10","priceOpen":"2331.20","profit":"-4.5000"}Parse them into a decimal type. Never into a float. Integers (login, ticket ids, timestamps,
counts, digit counts) are JSON numbers.
Volumes are lots
volume is always in lots, matching the MT5 platform. 0.10 means one tenth of a standard lot.
Per-symbol volumeMin, volumeMax and volumeStep from GET /v1/exchangeInfo are also lots, and
a volume that is not an exact multiple of volumeStep is rejected (-4007).
The lot's notional value is volume * contractSize in currencyBase.
Prices are in symbol quote units
Prices are raw symbol prices at the symbol's digits precision — the same numbers the MT5 terminal
shows. Nothing is normalised across symbols.
Symbols are case-sensitive and exact
Use the symbol name exactly as the trade server spells it. BTCUSD.cent and BTCUSD.CENT are
two different instruments, and a symbol that differs only in case does not exist (-1121).
This includes stream names. Unlike Binance, stream names are not lowercased:
XAUUSD@tick correct
xauusd@tick WRONG — no such stream
BTCUSD.cent@tick correctEnumerate the exact names with GET /v1/exchangeInfo. Never upper-case, lower-case or otherwise
normalise a symbol anywhere in your stack.
Times are Unix milliseconds
Every timestamp on the wire is Unix epoch milliseconds, UTC.
MT5 is natively second-resolution for trade timestamps (time, timeSetup, timeExpiration,
openTime), so those fields are exact multiples of 1000 after conversion. Millisecond precision on
those fields is a unit convention, not extra resolution.
Money is in the account's deposit currency
balance, credit, equity, margin, freeMargin, profit, swap and commission are in the
account's deposit currency. GET /v1/account returns it as currency.
Identifiers
| Identifier | Type | Notes |
|---|---|---|
login |
INT64 | MT5 login. |
orderId |
INT64 | MT5 order ticket. |
positionId |
INT64 | MT5 position ticket. Under hedging, one per position — a symbol can have many. |
dealId |
INT64 | MT5 deal ticket. The natural key of an execution. |
clientOrderId |
STRING | Yours. Up to 36 characters, unique per API key. The idempotency key. |
Ticket ids can exceed 2^53, so do not parse them into a JavaScript Number. Use BigInt or
keep them as strings.
Versioning and backward compatibility
The /v1 contract is frozen in the following sense:
We may, without notice:
- Add new fields to any JSON object in a response.
- Add new elements to the end of an array row (for example a kline row).
- Add new values to any enum (
status,type, evente, errorcode). - Add new endpoints, new streams and new optional request parameters.
- Change rate-limit numbers (read them from
GET /v1/exchangeInfo).
Your client must therefore:
- Ignore unknown fields rather than fail on them.
- Tolerate unknown enum values — treat an unrecognised
status,typeor event as "something happened I do not model", log it, and do not crash or assume a default. - Index array rows from the front and tolerate extra trailing elements.
- Never rely on field ordering in a JSON object.
We will not, within /v1:
- Remove or rename a field.
- Change the type or unit of an existing field.
- Change the meaning of an existing enum value or error code.
- Remove an endpoint or a stream.
Anything that would break those guarantees ships as /v2, and /v1 keeps running alongside it for
an announced deprecation period.
Every change is recorded in Change Log.


