# Kronos X Sandbox, Postman Quickstart

The fastest way to explore and demonstrate the Kronos X member API is the ready-made
Postman collection in this folder: `KronosX-Sandbox.postman_collection.json`.
Authentication is fully automatic; you only paste your credentials once.

## Setup (2 minutes)

1. Open Postman and click **Import** (top left), then drop in
   `KronosX-Sandbox.postman_collection.json`.
2. Click the imported collection **Kronos X MTF Sandbox**, open the **Variables**
   tab, and fill in:
   | Variable | Value |
   |---|---|
   | `baseUrl` | `https://sandbox.mtf.perpetuals.net` (preset; the clustered sandbox of member API revision 3.83, which replaces `sandbox.mtf.perpetuals.com` from the date venue operations announce) |
   | `apiKey` | your sandbox API key |
   | `apiSecret` | your sandbox API secret |
   Press **Save** (Ctrl+S).
3. Done. Open any request and press **Send**.

## What the collection does for you

The collection pre-request script runs before every request and does three things,
in this order:

1. **Mints a fresh `clOrdId`**, so any request can be re-sent without tripping the
   duplicate-reference check (code `2009`).
2. **Refreshes the live-market variables** the request uses. Before an order request
   is signed, the script reads `GET /v1/instruments` and `GET /v1/ticker` and fills
   `pxBuy`, `pxStopTrig`, `koBarrier`, `spotSymbol` and friends from the current mark,
   aligned to that instrument's own `tickSize` and `qtyStep`. A price hard-coded into
   a collection is only correct until the mark walks away from it, at which point every
   order example rejects `2003` (price outside band). Deriving the price from live data
   is what your own client must do, so the collection does it too rather than shipping
   numbers with a shelf life.
3. **Signs exactly the bytes that will be transmitted**:
   `hex(HMAC_SHA256(secret, timestamp + METHOD + pathWithQuery + body))`, sent as
   `X-API-KEY` / `X-TIMESTAMP` / `X-SIGNATURE`.

On point 3, one rule decides whether your own signer works: **sign the exact bytes on
the wire**. The venue verifies against the raw path and query string it received, not
a normalised form of it. The collection's query strings use unreserved characters and
commas only, which Postman transmits literally, so what the script signs and what the
socket carries are byte-identical. If you add a parameter that needs percent-encoding,
percent-encode it in the URL yourself. Signing `?a=x y` while sending `?a=x%20y` is the
classic `1001`, and it is the same bug that bites HTTP libraries which re-encode a URL
after the signature was computed (Python `aiohttp`/`yarl` re-encodes commas, for
example). Batch bodies need nothing special: `POST /v1/orders/batch`, and since
Member API revision 3.74 `POST /v1/orders/batch/amend` and
`POST /v1/orders/batch/cancel`, each take one raw JSON body and sign like any other.

## Suggested demo flow

Run the folders in order. Everything in folders 1, 2, 4, 5 and 6 is expected to answer
successfully against the live sandbox listing.

1. **Folder 1, Public market data**: instruments (note `kind: PERP | SPOT | BARRIER`
   and `mode: ORDER_BOOK | RFQ`), order books, ticker, trades. No credentials needed.
   The instruments test asserts that every listing publishes `tickSize`, `qtyStep` and
   `minNotional`, because the rest of the collection is built on those three fields.
2. **Folder 2, Trading walkthrough**: run the steps top to bottom.
   - **Steps 1 to 5** place a resting limit BUY roughly 2 percent under the mark, read
     it back by id, amend it, and cancel exactly that order. The amend re-checks the
     FULL amended quantity against the price band and the minimum notional, so it uses
     a live price too, not the one you sent an hour ago.
   - **Step 6** crosses the book with a MARKET IOC and leaves you long 0.001 BTC. If the
     sandbox book happens to be empty it answers `2007` instead of filling, and opens
     nothing.
   - **Step 7** closes that 0.001 again. The pre-request script reads `GET /v1/positions`
     and sends the **smaller** of your live net position and `flatMaxQty` (a collection
     variable, default `0.001`, the size step 6 uses), aligned to `qtyStep`. Both halves
     of that matter. Reading the position matters because a reduce-only order **larger**
     than the reducible position rejects `2008`, so a close is measured, never guessed.
     The cap matters because this is a MARKET order that trades on arrival: uncapped, it
     would close **every** BTC-USD-PERP unit on the credential, including a position that
     was open before you started, placed over FIX, from another session or by your own
     strategy. Raise `flatMaxQty` yourself when you genuinely want a full close, and read
     `GET /v1/positions` first. If you are already flat, this answers `2008`, which is
     the honest reply.
   - **Stop-limit** rests as `PENDING_TRIGGER` with its trigger 5 percent **above** the
     mark. A trigger on the wrong side of the mark fires on the next price tick and the
     resulting limit order crosses the book, which is a filled position rather than a
     demonstration of a conditional.
   - **Batch** sends `POST /v1/orders/batch`: three end clients' orders in one signed
     message, each entry with its own `clOrdId` and `clientUid`, at three passive prices,
     so all three rest and all three results come back `NEW`. The batch is non-atomic:
     the envelope is `code` 0 with `data.results` in array order, and a mixed batch
     behaves the same way. Put one `reduceOnly` entry with no position for that side into
     the array and the envelope stays `code` 0 while that single entry carries
     `status: REJECTED`, `rejectCode: 2008`, and its neighbours stand. That is also the
     mass-close pattern: one message, N reduce-only closes, one per end client. Every
     entry consumes one order-budget unit, charged up front, all or nothing. Its test
     script stores the three orders' ids (A and B by `orderId`, C by `clOrdId`) for the
     next two requests.
   - **Batch amend** (Member API revision 3.74) sends `POST /v1/orders/batch/amend`
     naming two of those orders: order A sized DOWN at its price, which keeps its queue
     priority, and order B repriced to a passive price, which is cancel and new. Each
     entry is processed exactly as `PUT /v1/orders/{orderId}`; the rows come back in
     array order, a row carrying `code` is a failure row, and every entry costs one
     order-budget unit.
   - **Batch cancel** (Member API revision 3.74) sends `POST /v1/orders/batch/cancel`
     naming the three orders, two by `orderId` and one by `clOrdId`, and every row comes
     back `CANCELED`. Cancellation batches are free and never throttled; send it again
     and every row answers `2014`, because the venue looks each entry up first and
     submits nothing for an order that is no longer open.
   - **Cancel all** clears anything still resting and the pending conditional.
   The walkthrough leaves your account flat and your book empty.
3. **Folder 3, Spot trading**: prefunded spot orders. A BUY reserves quote cash, a SELL
   delivers the base asset out of your `assets` rows, and there are no positions, no
   margin and no funding. The symbol is resolved at send time: the script takes the first
   market with `kind: SPOT` from `GET /v1/instruments` and derives the prices and quantity
   from that market's own grid and mark, so the folder runs for real the moment venue
   operations list a spot market, with no edits from you.
   **Shape only today.** The current sandbox listing carries no SPOT market, so
   `spotSymbol` falls back to the placeholder `BTC-USDC` and these requests answer `2001`
   (unknown symbol). The tests assert that documented answer instead of reporting a false
   pass. One condition the venue cannot arrange for you even on a listed market: the SELL
   needs a base-asset balance.
4. **Folder 4, Account, positions, margin**: member account and margin state, member
   positions, and the per-end-client sub-position view (the member manages margin; client
   rows are informational for your reconciliation). The margin preview is a read, not an
   order: no `clOrdId` uniqueness, no order budget, and never gated on `reg.traderId`.
5. **Folder 5, Market Segments**: read your counterparty-acceptance config, then restrict
   and re-open. Run **My config** and **Matchable members** first. `version` is optimistic
   concurrency and it **increments on every successful write**, so a hard-coded version
   works exactly once and answers `4001` for ever after; the collection therefore carries
   the version forward in `{{segVersion}}` from each response, and takes the counterparty
   code from `{{peerMember}}`, because a member code the venue does not know is rejected
   outright. Leave yourself unrestricted when you are done.
6. **Folder 6, Event stream recovery**: read your stream cursor, backfill from `fromSeq=1`,
   then page. The paging request uses `{{nextCursor}}`, which the backfill request stores
   from its own response, so you can re-send it to walk the stream. The seeded value
   (thirty zeros) is the start-of-stream cursor, so it also works standalone. Every private
   WebSocket event-class message reappears here byte-identically, with a 30-character
   fixed-width cursor (API reference section 11). The `channels=order,execution` request is
   also the worked example of the comma-in-query signing rule above.
7. **Folder 7, RFQ + barrier contracts**: the barrier flow is live on the current listing
   (`BTC-USD-KO`, `kind: BARRIER`, `mode: RFQ`). Create the barrier RFQ (it stores
   `lastRfqId`; its `barrier.expiryTs` is `{{koExpiryTs}}`, thirty days ahead and set by
   the pre-request script, because since revision 3.70 a market that publishes
   `maxContractLifeHours` on `GET /v1/instruments` refuses an omitted or later expiry with
   2027 and every sandbox KO market publishes 8784 h from the date venue operations
   announce), view the open board as the other member, quote it (`lastQuoteId`), accept,
   then read `GET /v1/barriers` to see the knock-out contract from both roles. A second
   create request adds the optional take profit (revision 3.43, `{{koTakeProfit}}`, mark plus
   5 percent for the long holder); its contract terminates AT that level too. A third
   create request asks for the STRICT take profit (revision 3.45): the same levels plus
   `barrier.expiryTs` one hour ahead (`{{koStrictExpiry}}`, set by the pre-request script)
   and `barrier.strictTakeProfit: true`; the contract pays only if the take profit is
   touched before that expiry, and the response echoes `strictTakeProfit`, the strict
   `premiumBps` (5000 on the sandbox, half the distance) and the indicative `premium` at
   the current mark. Every barrier view now carries `premium` and `premiumBps` (the
   normal share is 9000 after the sandbox's 10 percent knock-out rebate).
   Two more requests cover the EARLY CLOSE (revision 3.44): after an accept, "Close an
   open barrier contract early" runs as the HOLDER and sends `closeContractId`
   (`{{lastContractId}}`, captured by the accept) with no side, quantity or barrier; the
   venue derives the closing side and the full quantity, and only the contract's WRITER
   sees the request on its board. Quote it as the second member (`{{koQuote}}`, the mark,
   lies strictly inside the terms), accept it as the holder, and "My barrier contracts
   (closed)" (`GET /v1/barriers?status=CLOSED`) shows the row with `terminationReason`
   `EARLY_CLOSE`, `settlePrice` = the quote and the holder-signed `realizedPnl`.
   Two conditions apply, and the collection labels both:
   - **A second member is required for the quoting side.** Both legs of an RFQ trade must
     be different members, checked per member, not per API key or `clientUid`, so quoting
     your own request rejects `2026`. Import the collection a second time, or swap
     `apiKey`/`apiSecret`, to run the quote and the accept.
   - **The perp-RFQ create request is shape only** while no perpetual runs `mode: RFQ`.
     It resolves the symbol at send time and uses a real RFQ market if one is ever listed;
     on today's listing every perpetual runs `mode: ORDER_BOOK`, so it answers `2023`
     (not an RFQ market), and its test asserts that.
   Barrier terms are derived from the barrier market's own mark, which is independent of
   `BTC-USD-PERP`: a BUY is a long holder, so the knock-out level must sit strictly below
   both the mark and the eventual entry price, or the venue answers `2027`.

**No FIX equivalent for folder 7.** RFQ entry (create, quote, accept, cancel) is REST only,
with lifecycle pushes on the private WebSocket `rfq` channel. A FIX-only integration cannot
trade RFQ markets or barrier contracts at all, and an order sent to an RFQ-mode market is
rejected `2022` on every transport.

## What is shape only, and why

| Requests | Answer today | Condition that makes them live |
|---|---|---|
| Folder 1, spot order book; folder 3, all spot requests | `2001` unknown symbol | venue operations list a market with `kind: SPOT`; the folder then resolves it automatically |
| Folder 7, Create RFQ on a perp | `2023` not an RFQ market | a perpetual listed with `mode: RFQ` |
| Folder 7, Quote an RFQ / Accept quote / Withdraw quote | `2026` when run as the requester | a second member's credentials on the quoting side |

Everything else is expected to return `code` 0 with a working outcome. If it does not,
the table below is the first place to look.

## WebSocket in Postman

Postman also speaks WebSocket: **New, WebSocket** and connect to
`wss://sandbox.mtf.perpetuals.net/ws/public` (market data, no auth) or
`wss://sandbox.mtf.perpetuals.net/ws/private` (account + trading).

Public socket, paste and send:

```json
{"op":"subscribe","reqId":"s1","args":["orderbook.5.BTC-USD-PERP","trades.BTC-USD-PERP"]}
```

Private socket, first authenticate (member API revision 3.85: the login signs the
connection's challenge). As soon as the private socket connects, the venue sends
its first frame, the challenge:

```json
{"channel":"challenge","type":"challenge","nonce":"5f2b8c0e9a4d4e7c8b1f3a6d2c9e0b7a","ts":1700000000000}
```

Copy the `nonce` of YOUR connection from the messages pane (every connection gets
its own). The auth signature is
`hex(HMAC_SHA256(secret, "GET/ws/private" + expiresMs + nonce))` and the nonce
goes in as the fourth arg; compute it for example in the Postman console or any
shell, within a minute of signing (the venue refuses an `expiresMs` more than 60
seconds ahead or in the past):

```bash
NONCE=paste-the-nonce-of-this-connection
EXP=$(( $(date +%s) * 1000 + 50000 ))
SIG=$(printf 'GET/ws/private%s%s' "$EXP" "$NONCE" | openssl dgst -sha256 -hmac "YOUR_SECRET" -hex | sed 's/^.*= //')
echo "{\"op\":\"auth\",\"reqId\":\"a1\",\"args\":[\"YOUR_KEY\",$EXP,\"$SIG\",\"$NONCE\"]}"
```

(Works on Linux, macOS, and Git Bash. In the Postman console the same thing is
`CryptoJS.HmacSHA256("GET/ws/private" + exp + nonce, secret).toString(CryptoJS.enc.Hex)`.)
The message works on that connection only: after a reconnect, copy the new
connection's nonce and sign again. An `auth` without the nonce answers `1001`
`auth failed: sign the nonce of this connection's challenge frame ...`.

Send the printed message, then anything the REST API can answer works
synchronously on the socket, for example:

```json
{"op":"get.account","reqId":"q1"}
{"op":"get.clientPositions","reqId":"q2","args":[{"clientUid":"CLIENT-A"}]}
```

Order entry on the socket takes the same order body as REST, and the same rules apply.
There is no pre-request script here to compute a price for you, so read the mark first
(`GET /v1/ticker?symbol=BTC-USD-PERP`) and put the limit price inside the instrument's
band and on its tick grid. With a mark of 70000, a passive BUY is:

```json
{"op":"order.place","reqId":"o1","args":[{"symbol":"BTC-USD-PERP","side":"BUY","type":"LIMIT","tif":"GTC","price":"68600","qty":"0.005","clOrdId":"ws-1","clientUid":"CLIENT-A"}]}
```

Substitute a price derived from the current mark before you send it: a price more than
10 percent away rejects `2003`, and one off the 0.5 tick grid rejects `2002`.

See the member API reference (`KronosX-Member-API.html`), section 10.6, for the
full list of `get.*` operations. Every response carries your `reqId` and
`asOfSeq`, so replies can be correlated and ordered against streamed events.

## Troubleshooting

| Symptom | Cause |
|---|---|
| `code 1001` authentication failed | apiKey/apiSecret not saved on the collection, or a signer that hashes a different string from the one transmitted (see the encoding rule above) |
| `code 1002` timestamp out of window | your machine clock is off by more than 5 s; since member API revision 3.85 also `request already received`, the same signed request sent again unchanged (the collection signs every send afresh, so this means the request was replayed byte for byte, for example from an exported cURL: send it from Postman again) |
| `code 2001` unknown symbol | the symbol is not listed; refresh `GET /v1/instruments` and never hard-code the listing |
| `code 2002` tick/step misaligned | price off the `tickSize` grid or qty off the `qtyStep` grid; read both from `GET /v1/instruments`. The venue never rounds for you |
| `code 2003` price outside band | the limit price is more than `priceBandBps` from the current mark (10 percent on the crypto listings, 2 to 5 percent on the tradfi perpetuals by asset class; read `priceBandBps` per market from `GET /v1/instruments`). This is what a hard-coded price turns into once the mark moves; derive it from `GET /v1/ticker` |
| `code 2007` no eligible liquidity | a MARKET order met an empty book, or nothing your Market Segments configuration lets you trade with |
| `code 2008` reduce-only violation | a `reduceOnly` order bigger than your reducible position, on the wrong side, or with no position at all |
| `code 2009` duplicate clOrdId | you hard-coded a clOrdId; use the generated `{{clOrdId}}` |
| `code 2013` invalid order | malformed field content, a missing trigger on a conditional, a trigger on a plain limit order, a single message above `maxOrderNotional`, or (Member API 3.72) a `clOrdId`, a `clientUid` or a Trader ID of the `reg` block that begins or ends with whitespace, which a pasted value easily carries |
| `code 2017` unknown field | order entry is strict; the body carries a field the venue does not define, and `msg` names it. Nothing is silently ignored |
| `code 2018` assertion mismatch | an optional `leverage` or `quoteCcy` assertion that cannot match. Every current perpetual runs `leverageMode: TIERED` and therefore publishes `maxLeverage: null`, so a fixed `leverage` assertion cannot match one; read `leverageMode` from `GET /v1/instruments` and `imMultiplier` from `GET /v1/account` |
| `code 2021` trader id required | your member has registered Authorised Users, so every order must carry `reg.traderId`. Add it to the order bodies (margin previews are never gated) |
| `code 2022` RFQ market only | an order was sent to a market whose `mode` is `RFQ`; trade it through `POST /v1/rfq` instead |
| `code 2023` not an RFQ market | `POST /v1/rfq` named an `ORDER_BOOK` market; trade it on its book |
| `code 2026` RFQ self-trade | you quoted your own member's RFQ; use the second member's credentials |
| `code 2027` barrier terms | the knock-out level is not strictly beyond the knock-out side of the barrier market's current mark, or is on the wrong side of the quoted entry; likewise a `barrier.takeProfit` (revision 3.43) that is not strictly on the profit side of the mark or the quoted entry; or a strict take-profit request (revision 3.45) without a take profit, without an expiry, with an expiry more than 24 h ahead, or on a market whose `strictTakeProfitPremiumBps` is 0 |
| `code 2028` below minimum notional | `price x qty` under the market's `minNotional` (5 USD on current listings, 100 USD on the barrier markets since Member API revision 3.71, a close request included); send a larger qty |
| `code 4001` on Market Segments PUT | stale `version`; it increments on every write, so GET the config first and resend with that version |
| `code 5030` NOT_LEADER (HTTP 503) | the clustered sandbox (member API revision 3.83) is changing its leader, and your request reached a node that is not the leader; nothing took effect. The answer's `leader` is the sandbox host itself: send the request again (that node closed the connection after its answer, so the next send opens a new one, which reaches the leader). On a WebSocket that acks `5030`, disconnect and connect again |
