Yellow BoxTrading APIv1 · pre-release

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

  • GET and DELETE take parameters in the query string.
  • POST and PUT take parameters in the query string, in an application/x-www-form-urlencoded body, 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/accounts lists exactly the logins the key may trade.
  • POST /v1/batchOrders carries login per 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

Text
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:

Text
timestamp < serverTime + 1000  &&  serverTime - timestamp <= recvWindow

Both 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.

Text
apiKey  : Tq7sVn2LpZ4wKdRj9xYc3BmHfA6eQu1oGi0tNsXrWvJbEyPzMkCdHlFgAoUvQ
secret  : x9Tq2mWvJb4rZ8kLpN6yHc3sVd1fGaQeR7uI0oP5tYbXnMwKjSzCvBhDlFgA

Example 1 — all parameters in the query string (GET /v1/account)

Text
queryString : login=100123&recvWindow=5000&timestamp=1789012345678
signature   : 13c783f085a1ef46b6a0e1df8d59c70ab50a4f8e669ffa4a2bc49998c0a9255b

Reproduce it:

cURL / shell
echo -n "login=100123&recvWindow=5000&timestamp=1789012345678" \
  | openssl dgst -sha256 -hmac "x9Tq2mWvJb4rZ8kLpN6yHc3sVd1fGaQeR7uI0oP5tYbXnMwKjSzCvBhDlFgA"
Text
SHA2-256(stdin)= 13c783f085a1ef46b6a0e1df8d59c70ab50a4f8e669ffa4a2bc49998c0a9255b
cURL / shell
curl -H "X-YBX-APIKEY: Tq7sVn2LpZ4wKdRj9xYc3BmHfA6eQu1oGi0tNsXrWvJbEyPzMkCdHlFgAoUvQ" \
  "https://trade-api.yellowboxmarkets.com/v1/account?login=100123&recvWindow=5000&timestamp=1789012345678&signature=13c783f085a1ef46b6a0e1df8d59c70ab50a4f8e669ffa4a2bc49998c0a9255b"

Example 2 — all parameters in the body (POST /v1/order)

Text
requestBody : login=100123&symbol=XAUUSD&side=BUY&type=MARKET&volume=0.10&newClientOrderId=bot-001&recvWindow=5000&timestamp=1789012345678
signature   : b7f6052aa37e7d6de935cce1c099f8103a2b0a0711b77fdf48becfc8b7d2c0e7
cURL / shell
echo -n "login=100123&symbol=XAUUSD&side=BUY&type=MARKET&volume=0.10&newClientOrderId=bot-001&recvWindow=5000&timestamp=1789012345678" \
  | openssl dgst -sha256 -hmac "x9Tq2mWvJb4rZ8kLpN6yHc3sVd1fGaQeR7uI0oP5tYbXnMwKjSzCvBhDlFgA"
Text
SHA2-256(stdin)= b7f6052aa37e7d6de935cce1c099f8103a2b0a0711b77fdf48becfc8b7d2c0e7
cURL / shell
curl -X POST \
  -H "X-YBX-APIKEY: Tq7sVn2LpZ4wKdRj9xYc3BmHfA6eQu1oGi0tNsXrWvJbEyPzMkCdHlFgAoUvQ" \
  -d "login=100123&symbol=XAUUSD&side=BUY&type=MARKET&volume=0.10&newClientOrderId=bot-001&recvWindow=5000&timestamp=1789012345678&signature=b7f6052aa37e7d6de935cce1c099f8103a2b0a0711b77fdf48becfc8b7d2c0e7" \
  "https://trade-api.yellowboxmarkets.com/v1/order"

Example 3 — mixed query string and body (the same parameters as Example 2, split)

Text
queryString : login=100123&symbol=XAUUSD
requestBody : side=BUY&type=MARKET&volume=0.10&newClientOrderId=bot-001&recvWindow=5000&timestamp=1789012345678
totalParams : login=100123&symbol=XAUUSDside=BUY&type=MARKET&volume=0.10&newClientOrderId=bot-001&recvWindow=5000&timestamp=1789012345678
signature   : 2cdb79a28695a12db0f60b35af89e340588d1428b97e291cb013dbff437ac392

Note 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

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.

JSON
{"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.

JSON
{"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:

JSON
{"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:

JSON
{"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.

JSON
{"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:

Text
XAUUSD@tick        correct
xauusd@tick        WRONG — no such stream
BTCUSD.cent@tick   correct

Enumerate 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, event e, error code).
  • 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, type or 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.