Perpetuals Kronos X v2.0 “Pacifica” Member API

REST API

The complete request/response reference for the Kronos X MTF. Every endpoint below is served on both environments over TLS, returns the same envelope, and is browsable and executable at /docs. Read this page alongside WebSocket: REST is for requests, reference data and history; the socket is for the live event stream.

Base URLs and transport

EnvironmentREST base URLWebSocketFIX 4.4
Sandboxhttps://sandbox.mtf.perpetuals.netwss://sandbox.mtf.perpetuals.netsandbox.mtf.perpetuals.net:9879
Productionhttps://production.mtf.perpetuals.comwss://production.mtf.perpetuals.comissued with your production credentials

TLS is mandatory on both environments. Certificates are publicly trusted (Let's Encrypt), so standard system trust stores validate the chain and no certificate pinning is required or supported. Plain HTTP is not exposed at all, so a misconfigured client fails to connect rather than leaking a signed request in clear text.

One entry host per environment (revision 3.83). The sandbox is sandbox.mtf.perpetuals.net, three nodes behind that one entry host; it replaces the single-node sandbox.mtf.perpetuals.com from the date venue operations announce. The entry host sends new connections to the node that accepts writes, the leader. Node addresses are not published and are not member endpoints, so never pin a node. A leader change therefore happens on the sandbox as it can on production, with the 5030 redirect described under Errors and the retry matrix; for Conformance Test Plan case I-05 venue operations stage a leader hand-over at an agreed time.
Sandbox operations policy. Full sandbox data resets are announced at least 5 business days in advance via the members Slack channel and a member portal notice, scheduled together with breaking manual revisions where possible; the announcement names the post-reset baseline (manual revision, instrument set, standard test balances). API credentials survive a data reset unless the announcement says otherwise, and portal logins persist. Margin top-ups for test members are available on request via the members Slack channel, normally booked the same business day; per-member state resets (flatten positions, restore standard balances) execute in the next maintenance window. Resting orders and other standing test state survive ordinary releases; only announced resets clear them. Sandbox risk tiers match production on every listed perpetual, and the production listing as planned today uses the sandbox's grid and minNotional on every market both list; the two deliberate parity differences are the sandbox's fixed funding rate on the GRID perpetuals and its 60 second stale-mark timeout (300 seconds in production). Production lists the perpetual and the knock-out markets; its instrument list differs from the sandbox's and is confirmed with the go-live listing. The sandbox is available during market hours on a best-efforts basis and observes the production maintenance window (Saturday 02:00 to 06:00 UTC); no service credit applies. Full policy: Member API reference, section 1a.
Interactive reference
/docs on either host. Paste a key and secret into the bar at the top and the page signs each request in your browser, so "Try it out" works before you have written any signing code.
Machine-readable spec
/openapi.json (OpenAPI 3.1). Generate your client from this rather than transcribing tables by hand.
Content type
application/json on every request with a body. The single exception is GET /v1/reconciliation/end-client-positions?format=csv, which answers text/csv.

Response envelope

Every JSON response, success or failure, uses one shape:

{ "code": 0, "msg": "ok", "data": { }, "asOfSeq": 123456789, "ts": 1785000000000 }
FieldTypeMeaning
codeint0 on success, otherwise a registry code. The HTTP status mirrors the class of error (401, 403, 404, 409, 429, 400, 503).
msgstring"ok" or a human-readable reason. Log it, never branch on it: wording is not part of the contract, code is.
dataobject, array or absentThe payload. Note the shape per endpoint: several return an ARRAY at the top level.
asOfSeqint64The engine sequence number the response was read at.
tsint64Server time, Unix milliseconds.

What asOfSeq is for. The engine assigns one strictly monotone sequence number per applied command, venue-wide. Every REST response, every WebSocket frame and every recovery cursor carries a number from that same counter, so you can totally order data taken from different transports. If a REST read of /v1/positions returns asOfSeq: 104870 and a WebSocket position frame carries asOfSeq: 104882, the frame is newer and must win, no matter which arrived at your process first. This is what lets you resync a stale cache without a race.

Null fields are omitted. The REST serializer drops keys whose value is null, so an absent key means null, not an error. On error envelopes data is normally absent and asOfSeq is 0. Build tolerant parsers: the venue adds response fields and enum values inside /v1 without notice, and a strict parser of venue responses will break on a routine additive release.

The two-level success check

code: 0 means the gateway accepted and sequenced your request. It does not mean the order is working. A fully validated order can be rejected by the engine inside an HTTP 200 with code: 0, and the outcome lives in data.status and data.rejectCode:

{
  "code": 0,
  "msg": "ok",
  "data": {
    "orderId": 0, "clOrdId": "a3f1c9d24b7e4f108c6d2e5b", "symbol": "BTC-USD-PERP",
    "side": "BUY", "type": "LIMIT", "tif": "POST_ONLY",
    "price": "65000", "qty": "1", "remainingQty": "0", "reduceOnly": false,
    "clientUid": "CLIENT-A", "status": "REJECTED", "rejectCode": 2005,
    "acceptedTs": 1785000000000
  },
  "asOfSeq": 104873, "ts": 1785000000001
}

Treat the check as two gates in sequence: first envelope.code == 0, then data.status != "REJECTED". A pre-book rejection legitimately carries "orderId": 0 because the rejection happened before an order id was assigned; correlate it by clOrdId, which is always echoed. The other transports put the same verdict where their protocol puts verdicts: the WebSocket order.place ack IS the verdict (success: false, the code, the same payload in data) and FIX answers an ExecutionReport with 150=8, 39=8 and the code in 9010. The difference is deliberate and stable; map all three onto one internal outcome, the code and the order state.

Ack after durable. No acknowledgement and no event is emitted before the journal write is durable. When you hold a code: 0 response in your hand, the venue will not forget that order across a restart or a failover. That is why a timeout is genuinely ambiguous and must be resolved by reconciliation, never by a blind resend with a new reference.

Authentication

Public market data needs no credentials. Every private endpoint requires three headers, plus one optional fourth.

HeaderRequiredValue
X-API-KEYyesYour API key, verbatim.
X-TIMESTAMPyesCurrent Unix time in milliseconds, as a decimal string.
X-SIGNATUREyesLowercase hex HMAC-SHA256 (see below). The gateway lowercases what you send and compares in fixed time.
X-RECV-WINDOWnoThe receive window in milliseconds, default 5000, an integer from 1 to 60000: any other value is refused with code 1002, never reduced (revision 3.85). X-TIMESTAMP may lag the venue clock by up to the window and lead it by at most the smaller of the window and 5000 ms; outside that the request fails with code 1002, checked when the headers arrive and again once the whole body has arrived. Not part of the signed string.

The signing string

signature = hex_lowercase( HMAC_SHA256( secret, timestamp + METHOD + pathWithQuery + body ) )

The four parts are concatenated with no separator, no newline and no host:

Request bodies are capped at 1 MiB. A body larger than 1,048,576 bytes answers HTTP 413 before authentication. No legitimate request approaches this; it exists so an oversized payload is refused at the edge.

Verified signature vector

Reproduce this exactly before you send a single live request. It is the fastest way to isolate a signer bug from a credential problem.

secret     = test-secret
timestamp  = 1700000000000
method     = GET
path       = /v1/account
body       = (empty)

payload    = 1700000000000GET/v1/account
signature  = 741b61b6d3740ede7864551612095d2fb73b75a4f70534f45e9e207bdd1f321c
Sign the bytes your HTTP stack actually transmits. The venue verifies the signature against the path and query string exactly as they arrive on the wire. Some HTTP libraries re-normalize percent-encoding AFTER you build the URL, so the string you signed is not the string that is sent and every such request fails with code 1001. The known case is Python aiohttp/yarl, which re-encodes characters such as the commas in channels=execution,margin,order,wallet (raw comma vs %2C); construct the URL with yarl.URL(url, encoded=True) or sign the exact final string your client transmits. If your stack sends %2C, sign %2C; if it sends a raw comma, sign the raw comma. The two spellings produce different, individually valid signatures; the failure mode is signing one and transmitting the other. This bites first on the gap-frame backfill query, whose ready-made channels list contains raw commas.

Second vector: a comma-bearing query string

A GET whose query carries raw commas (not %2C), no body. Reproducing the hex proves your signer; after it matches, also verify in your library's request tracing that the transmitted query is byte-identical to the form you signed.

secret     = test-secret
timestamp  = 1700000000000
method     = GET
path       = /v1/stream/events?fromSeq=904118&channels=execution,margin,order,wallet&limit=500
body       = (empty)

signature  = 821b87a9ee51c26503856f18c0faa712b093aef4ec387e0ef1ecb1ea550651ac

Signer implementations

import hashlib, hmac, json, time
import requests

BASE   = "https://sandbox.mtf.perpetuals.net"
KEY    = "your-api-key"
SECRET = "your-api-secret"

def signed(method: str, path: str, body: dict | None = None, recv_window: int = 5000):
    # Serialize ONCE. The signed string and the transmitted bytes must be identical.
    payload = json.dumps(body, separators=(",", ":")) if body is not None else ""
    ts = str(int(time.time() * 1000))
    sig = hmac.new(SECRET.encode(), (ts + method + path + payload).encode(),
                   hashlib.sha256).hexdigest()
    headers = {
        "X-API-KEY": KEY,
        "X-TIMESTAMP": ts,
        "X-RECV-WINDOW": str(recv_window),
        "X-SIGNATURE": sig,
        "Content-Type": "application/json",
    }
    r = requests.request(method, BASE + path, headers=headers,
                         data=payload.encode() if payload else None)
    return r.json()

# Self-test against the published vector at startup.
assert hmac.new(b"test-secret", b"1700000000000GET/v1/account",
                hashlib.sha256).hexdigest() == \
    "741b61b6d3740ede7864551612095d2fb73b75a4f70534f45e9e207bdd1f321c"

print(signed("GET", "/v1/account"))
import crypto from "node:crypto";

const BASE   = "https://sandbox.mtf.perpetuals.net";
const KEY    = process.env.KRONOSX_API_KEY;
const SECRET = process.env.KRONOSX_API_SECRET;

export async function signed(method, path, body, recvWindow = 5000) {
  // Serialize ONCE; `payload` is both what we sign and what we send.
  const payload = body === undefined ? "" : JSON.stringify(body);
  const ts = Date.now().toString();
  const sig = crypto.createHmac("sha256", SECRET)
                    .update(ts + method + path + payload)
                    .digest("hex");
  const res = await fetch(BASE + path, {
    method,
    headers: {
      "X-API-KEY": KEY,
      "X-TIMESTAMP": ts,
      "X-RECV-WINDOW": String(recvWindow),
      "X-SIGNATURE": sig,
      "Content-Type": "application/json",
    },
    body: payload === "" ? undefined : payload,
  });
  return res.json();
}

// Self-test against the published vector at startup.
const vector = crypto.createHmac("sha256", "test-secret")
                     .update("1700000000000GET/v1/account").digest("hex");
if (vector !== "741b61b6d3740ede7864551612095d2fb73b75a4f70534f45e9e207bdd1f321c")
  throw new Error("signer does not match the published vector");
using System.Security.Cryptography;
using System.Text;

public sealed class KronosXClient(HttpClient http, string apiKey, string secret)
{
    public static string Sign(string secret, string ts, string method,
                              string pathWithQuery, string body)
    {
        using var mac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
        byte[] h = mac.ComputeHash(Encoding.UTF8.GetBytes(ts + method + pathWithQuery + body));
        return Convert.ToHexString(h).ToLowerInvariant();
    }

    /// <summary>`body` is the exact JSON string that will be transmitted.</summary>
    public async Task<string> SendAsync(HttpMethod method, string pathWithQuery,
                                        string body = "", int recvWindow = 5000)
    {
        string ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
        var req = new HttpRequestMessage(method, pathWithQuery);
        if (body.Length > 0)
            req.Content = new StringContent(body, Encoding.UTF8, "application/json");
        req.Headers.TryAddWithoutValidation("X-API-KEY", apiKey);
        req.Headers.TryAddWithoutValidation("X-TIMESTAMP", ts);
        req.Headers.TryAddWithoutValidation("X-RECV-WINDOW", recvWindow.ToString());
        req.Headers.TryAddWithoutValidation("X-SIGNATURE",
            Sign(secret, ts, method.Method, pathWithQuery, body));
        var res = await http.SendAsync(req);
        return await res.Content.ReadAsStringAsync();
    }
}

// Startup assertion:
// Sign("test-secret", "1700000000000", "GET", "/v1/account", "")
//   == "741b61b6d3740ede7864551612095d2fb73b75a4f70534f45e9e207bdd1f321c"
Sign the exact bytes you transmit. The single most common integration failure is signing a re-serialized body. If you sign an object and then let the HTTP library serialize it again, a difference in key order, whitespace, unicode escaping or number formatting changes the bytes on the wire and the signature no longer matches. The symptom is maddening: intermittent 1001, usually correlated with a particular field being present. Serialize to a string once, hash that string, transmit that same string. The same rule applies to the path: sign the query string in the exact order and encoding your HTTP client will actually send.

A signed write is accepted once (revision 3.85)

The venue records the X-API-KEY and X-SIGNATURE of every signed request whose method is not GET, once the signature verifies and before the request runs, and keeps the pair for 60 seconds past its X-TIMESTAMP, the longest window a request can ask for; when a pair leaves the record the node refuses every signed write of the same member stamped at or before it with 1002 request signed before the oldest one this node still records: sign it again with a fresh X-TIMESTAMP, so no pair is admitted twice. The same pair again answers HTTP 401, code 1002, msg request already received: a signed write is accepted once, sign every request (a retry included) with a fresh X-TIMESTAMP, and nothing of it runs or is charged. A captured request therefore cannot be replayed: not an amend that would set a resting order back to an old price, not a quote that would come back with a fresh validity, not a batch whose entries carry no clOrdId. A GET is never recorded, and neither is POST /v1/margin/preview, which only reads.

Credential scope

Every credential carries a scope, fixed at issue time by venue operations.

ScopeReadsOrder entry, cancel, amend, config writes
TRADEallowedallowed
READ_ONLY (drop copy)allowedrefused with code 1005, HTTP 403

The gate is enforced on every write on every transport, REST, WebSocket and FIX alike, so a drop-copy key cannot become a trading key by changing protocol. Issue READ_ONLY keys to back office, risk, reporting and reconciliation components: it removes an entire class of production accident from processes that have no business sending orders. On POST /v1/orders the scope check runs after the body parse, so a 1005 rejection still echoes your clOrdId and is correlatable from the response body alone.

Multi-key credentials. A member may hold up to 5 active keys per scope. Issuing a key ADDS it and every existing key keeps working; nothing is replaced implicitly. Revocation is explicit and per key, from the member portal keys card or via venue operations, and is immediate on every transport: requests signed with a revoked key fail with 1001, and live WebSocket and FIX sessions authenticated with it are terminated, while the member's other keys are untouched. Rotate as a failover, not a cutover: issue the new key, migrate traffic, then revoke the old one. The secret is shown exactly once, at issuance. Rate-limit buckets are per member and shared across all of a member's keys (below), so extra keys never change your budget.

Rate limits

Two independent token buckets per MEMBER, shared across every API key the member holds (previously documented as per API key; with multi-key credentials, per-key buckets would multiply the budget by the number of keys issued). Order placements, amendments and every entry of a placement or amendment batch consume the order budget; cancels, the entries of a cancellation batch included, are never throttled. Order-budget traffic does not consume a read token, so heavy polling can never throttle your order flow and heavy order flow can never exhaust your read allowance.

BucketDefault capacityCharged byNot charged by
Reads6000 per minuteEvery signed GET, POST /v1/margin/preview, WebSocket get.* requestsOrder entry, amends, cancels, public endpoints
Order budget40000 per minute, and the per-second ceiling your class publishes (STANDARD 1,000 orders/s on production, DMA 3,000, MM 5,000; 0 on the sandbox)POST /v1/orders, every entry of POST /v1/orders/batch and of POST /v1/orders/batch/amend (N units, charged up front, all or nothing), PUT /v1/orders/{orderId}, and the WebSocket and FIX equivalentsCancels (POST /v1/orders/batch/cancel included), all reads

A denied batch consumes nothing: if fewer than N units are available, the whole batch answers 1003 with a retryAfterMs covering the full N-unit deficit, and every token you had stays in the bucket. The amendment batch (revision 3.74) is charged after the venue's engine-busy check, so its engine-busy 5001 consumes nothing either; the units of a batch whose entries are refused after the charge (an order not found, a blocked market, a grid refusal) stay spent, and a placement batch refused at the envelope with 5001 or 1004, which come after its charge, has spent its units too.

Both are full-capacity token buckets that refill continuously, so after a quiet period the entire minute's allowance is available at once. That is deliberate: a DMA member must be able to push a burst of risk-reducing orders when many end clients liquidate in the same few seconds, and a smoothed limiter would be exactly wrong at that moment. A breach returns HTTP 429 with code 1003.

Backing off correctly

The buckets are not fixed or sliding windows. Tokens accrue every millisecond, so a drained bucket admits again after one token's worth of time rather than at some window boundary. Every 1003 tells you exactly how long that is:

HTTP/1.1 429 Too Many Requests
Retry-After: 1
X-RateLimit-Retry-After-Ms: 2

{ "code": 1003, "msg": "rate limit exceeded",
  "data": { "clOrdId": "your-ref-9001", "retryAfterMs": 2 },
  "asOfSeq": 0, "ts": 1785000000123 }
Sleep retryAfterMs, nothing else. At the default order capacity one message is 1.5 ms of refill, so a throttled sender resumes almost immediately and converges smoothly on the cap. A client that instead assumes a one-minute window stalls for up to 60 seconds and then fires a burst, producing a sawtooth that looks like a venue outage and is purely self-inflicted. The Retry-After header carries the same value for off-the-shelf HTTP clients, but the specification makes it whole seconds, so it is always the coarser of the two: prefer data.retryAfterMs.

The same number rides the WebSocket ack (data.retryAfterMs) and the FIX reject (in Text(58)), so the contract is identical on all three transports.

Cancels consume neither bucket. A member must always be able to withdraw liquidity, so cancellation is never rate limited and never blocked. Cancels do still count as order messages under the RTS 9 order-to-trade regime. Every entry of a placement batch counts, whatever its per-entry outcome; an entry of an amendment or cancellation batch (revision 3.74) counts only when the venue submits it, so a row the venue answers before submitting anything for it (2014, 1004, 2049, a grid refusal, a cancellation naming no open order of yours) counts nothing. The regime measures order messages against trades over a rolling window and blocks new orders, batches and amendments only with code 1004 when a member-specific threshold is exceeded; a placement batch is gated per market as a whole, while an amendment batch (revision 3.74) answers 1004 as the row of each entry on a blocked market and runs its other entries, and counts an entry when the venue hands it to the matching engine. A lower warning threshold flags the member to venue operations without rejecting anything. Limits and thresholds are per environment and can be adjusted per member.

Repeated engine faults (revision 3.79). A command the matching engine aborts on answers 5005 (section 9), and the venue verifies its books after every such abort, which costs every venue node time. When ten of your commands answer 5005 within 60 seconds, every request of yours that consumes the order budget, on every key and channel, answers HTTP 429 with code 1003 and retryAfterMs until the oldest of those ten is 60 seconds old, so the wait can be up to 60,000 ms here. A request refused this way does nothing and consumes no order-budget unit (a barrier early close spends one read unit, see below). A request that consumes no order-budget unit, every cancellation among them, is never refused by it, and no other member is affected. A request that can only reduce your risk passes it too: a reduce-only order (reduceOnly true on POST /v1/orders and the WebSocket order.place, FIX 9001=Y on 35=D), a placement batch (POST /v1/orders/batch, WebSocket orders.batch) whose every entry is reduce-only, a lot close (POST /v1/lots/{lotId}/close) sent with reduceOnly true, and a barrier early close: the holder's close request (POST /v1/rfq with closeContractId), the writer's quote on that close request and the holder's accept of that quote. Such a request still consumes its order-budget unit, and the engine still rejects a reduce-only order that would increase a position. At the budget the venue reads whether such an early close is your own (a close request naming an open contract you hold, a quote on a close request of a contract you wrote, an accept of a quote on a close request of a contract you hold), and that read consumes one unit of your read budget whatever its outcome; any other, another member's contract or close request or one that does not exist, answers 1003, and so does your own while your read budget is empty, because the venue then does not read. While the venue sheds order entry it does not read, and the request answers 5001; it also answers 5001 when the read meets a full engine queue, after the read unit is spent; both are the answers below the budget while order-budget units remain. An amendment, single or batch, is still refused, and so is a placement batch with even one entry that is not reduce-only (the whole batch is refused). A 5005 is a fault of the venue: never resend such a command unchanged, and tell venue operations which command it was. Such a refusal names its cause: msg reads rate limit: repeated engine faults (5005) on REST and in the WebSocket ack, and FIX Text(58) begins with the same words before the wait (every other 1003 keeps its text; both begin with rate limit). When the budget trips, the venue mails your trading contact on file (without one, your incident or compliance contact) once. Ten 5005 answers in 60 seconds is a published rule of the venue: production runs exactly these values on every node.

Reads of the venue's history store (revision 3.73). The event stream routes (GET /v1/stream/events, GET /v1/stream/cursor and its WebSocket twin get.streamCursor, GET /v1/margin/calls) and the history routes GET /v1/executions, GET /v1/orders/history, GET /v1/account/ledger, GET /v1/adl/events and GET /v1/reconciliation/end-client-positions are served from the venue's history store. Beside your read bucket four technical bounds apply to them, on each node of the venue that serves you: how many reads of yours of each of the two groups run at once, a budget of these reads in flight on the node that every member shares, how long one read may run, and a yield to the venue's own writes to the store. The event stream group covers every stream reader of yours, all keys and all filters together: combine your filters into one reader or serialise your readers. A read beyond the in-flight bound or the shared budget waits briefly for its turn, then answers HTTP 429 with 1003 and a SUGGESTED retryAfterMs (a turn of the in-flight bound frees when another read of yours ends, a turn of the shared budget when any member's read on the node ends, never at a token refill: a 1003 while no other read of yours of the group is in flight comes from the shared budget, which serialising cannot help, so sleep the wait and back off if it repeats). A read that exceeded the time bound, a read of a group held after such a read, a read while the store is catching up with the venue, and a read the store could not answer (HTTP 500 with 5000 before this revision) answer HTTP 503 with 5007 and retryAfterMs. Nothing changed on your account, and the read token was spent. Keep one request in flight per stream, set your client timeout to at least 15 seconds, and after repeated 5007 on the same read narrow it (a smaller limit, a later cursor or from, no symbol).

Public endpoints

No signature, no headers, not charged to your key buckets.

Instruments

GET/v1/instruments

The reference data every other call depends on. Fetch it at startup, cache it, and drive all rounding from it. Hard-coding tick sizes and quantity steps is the second most common cause of avoidable 2002 rejections, and sandbox and production instrument configurations can differ.

{
  "code": 0, "msg": "ok",
  "data": [
    {
      "symbol": "BTC-USD-PERP", "kind": "PERP", "mode": "ORDER_BOOK",
      "isin": "CH1300000001", "cfi": "FFICSX",
      "baseAsset": "BTC", "quoteAsset": "USD", "settlementCcy": "USD",
      "tickSize": "0.5", "qtyStep": "0.0001", "minQty": "0.0001",
      "minNotional": "5",
      "maxOrderQty": "5000", "maxPositionQty": "50000",
      "maxPositionNotional": "2000000000",
      "maxOrderNotional": "10000000",
      "priceBandBps": 500, "marketMaxSlippageBps": 200,
      "makerFeeRate": "0.0001", "takerFeeRate": "0.0005",
      "fundingIntervalHours": 8,
      "funding": { "schedule": "GRID", "intervalHours": 8, "nextSettlementTs": 1785009600000 },
      "riskTiers": [{ "notionalCap": "max", "imRate": "0.02", "mmRate": "0.01" }],
      "maxLeverage": "50", "leverageMode": "FIXED", "status": "ACTIVE", "tradingHours": "CONTINUOUS"
    },
    {
      "symbol": "SP500-USD-PERP", "kind": "PERP", "mode": "ORDER_BOOK",
      "baseAsset": "SP500", "quoteAsset": "USD", "settlementCcy": "USD",
      "tickSize": "0.1", "qtyStep": "0.001", "minQty": "0.001", "minNotional": "5",
      "makerFeeRate": "0.0002", "takerFeeRate": "0.0005",
      "fundingIntervalHours": 24,
      "funding": { "schedule": "LOCAL_DAILY", "zone": "NEW_YORK", "snapshotLocalTime": "16:00",
                   "vwapWindow": "15:55-16:00", "vwapSnapshots": 5,
                   "nextSettlementTs": 1786464000000, "nextSnapshotTs": 1786464000000 },
      "riskTiers": [{ "notionalCap": "max", "imRate": "0.1", "mmRate": "0.05" }],
      "maxLeverage": "10", "leverageMode": "FIXED", "status": "ACTIVE", "tradingHours": "CONTINUOUS"
    }
  ],
  "asOfSeq": 1041, "ts": 1785000000123
}
FieldNotes
kindPERP | SPOT | BARRIER | SWAP | SECURITY. Spot is fully prefunded delivery, no leverage, no funding, no liquidation. Security (Member API revision 3.66) is a DLT financial instrument settled on the Kronos X chain, prefunded like spot, on the book or by RFQ per its mode; see the DLT Market section below. Barrier is a cash-settled knock-out contract (see RFQ trading below); barrier markets always run in RFQ mode. Swap (revision 3.51) is a writer-first fixed yield contract in the market's baseAsset (see "Yield contracts" below); swap markets always run in RFQ mode, have no price, no mark and no margin, and their tickSize is 1 because every price field on them carries a rate in basis points.
modeORDER_BOOK | RFQ. How the market trades. An order-book market runs the central limit order book documented on this page. An RFQ market has no order book at all: order entry there gives 2022, and trading runs through POST /v1/rfq (see RFQ trading below). Read the mode here and route accordingly.
tickSize, qtyStepPrice and quantity grids. Prices are quoted per base-asset unit; quantities are decimal base-asset units and must be exact multiples of qtyStep (integer strings remain valid; on a barrier market the step of the perpetual of the same underlying since revision 3.71). Misalignment is code 2002.
minQty, minNotionalSmallest accepted quantity (equals qtyStep on every current listing) and smallest accepted order value, price x qty (5 USD on every current listing, 100 USD on the barrier markets since revision 3.71, a close request included at the contract's whole quantity). Below the notional floor is code 2028; priced orders are valued at the limit price, market orders at the current mark, and amends re-check the full amended quantity. Exempt: venue-initiated closes and reduceOnly orders within your reducible position, so a sub-floor residual stays closable with a reduce-only order of exactly the position size.
contractMultiplierThe contract value: base units per contract, a decimal string, "1" on every listed market and fixed at 1 on BARRIER markets by listing rule (Contract Specifications, Barrier Contracts, section 6), so every barrier figure written as distance x qty x contract value is distance x qty; fractional barrier sizing is the qtyStep, never the multiplier.
maxOrderNotional, maxOrderQty, maxPositionNotional, maxPositionQtyOrder and position limits, both notional first with base-unit fallbacks. The per-order cap is maxOrderNotional where published (decimal settlement-currency string; absent or null where not set): one single order's price x qty, valued at its effective price (priced orders at the limit price, market orders and RFQ creation at the current mark, quotes at the quoted entry; amends on the FULL amended quantity), rejects 2013 beyond the cap. It is a fat-finger guard on a single message, so reduceOnly orders are deliberately NOT exempt (closing a large position may take several orders) while venue-initiated closes are; where no notional cap is set, the legacy base-unit maxOrderQty applies unchanged, informational otherwise. Independently of either, one order's price x qty must stay within the venue's exact-money range, 1000000000000000000 settlement-currency units, or it rejects 2013: an absolute ceiling on every market, not published per market, orders of magnitude above every maxOrderNotional in force. The position cap is maxPositionNotional where published (same string contract): the worst-case member net position, net plus same-direction working orders plus the incoming order, is valued at the CURRENT mark price and rejects 2011 beyond the cap. Notional denomination gives every market the same dollar capacity, so smaller markets no longer pin first in a correlated move. Where no notional cap is set, the legacy base-unit maxPositionQty applies unchanged; where both appear, the notional cap takes precedence and maxPositionQty is informational legacy. Risk-reducing flow is never blocked by the POSITION cap, and a position pushed above the cap by mark drift alone is never forced to reduce: the cap gates increases only.
fundingHow and when the market's funding is set and settled (revision 3.42); omitted on spot and barrier markets, which never fund. schedule is GRID (fixed UTC anchors every intervalHours, the crypto perpetuals) or LOCAL_DAILY (one settlement per calendar day at a local wall-clock window in zone UTC | LONDON | NEW_YORK: positions and the index are captured at snapshotLocalTime, one mark per minute is recorded over vwapWindow, vwapSnapshots of them, and the window end settles the mean against the index at the snapshot; the equity index, gold, oil and FX perpetuals, and since revision 3.75 on the sandbox silver and the USD/JPY quanto). nextSettlementTs is the next settlement (the same instant as nextFundingTs), nextSnapshotTs the next capture on LOCAL_DAILY. The schedule set is open. funding.rateCap never exceeds 1 and funding.rateFloor never lies below -1 (revision 3.70): the venue refuses to list or reconfigure a perpetual outside that band; the per-market caps in force remain Trading Parameters under the Contract Specifications. See Funding below.
riskTiersBracketed margin schedule. Each notional slice pays its tier rate. notionalCap is the string "max" on the open-ended top tier.
maxLeveragePresent only when the instrument runs a single open-ended tier whose IM rate has an exact reciprocal. Omitted otherwise.
leverageModeFIXED when maxLeverage is published, otherwise TIERED (margin is size-dependent, so no single leverage number is true for every order).
knockOutRebateBps, strictTakeProfitPremiumBpsBARRIER markets only (revision 3.45), omitted on perp and spot. The knock-out REBATE in basis points of the barrier distance (1000 = 10 percent on every barrier market listed today): the holder's premium on a normal contract is 10000 - knockOutRebateBps bps of |entry - barrier| x qty x contract value, and at a knock-out the holder recovers the rebate share from the writer. The STRICT take-profit premium share in basis points (5000 = 50 percent today; 0 = strict take profit not offered, barrier.strictTakeProfit rejects 2027). Both are frozen onto each contract when it opens (premiumBps), so a later change never re-prices an open contract.
swapMinTenorDays, swapMaxTenorDays, swapMaxRateBpsSWAP markets only (revision 3.51), omitted on the other kinds: the shortest and longest tenor an offer may name, whole days, and the highest annualised rate in basis points. On a swap market baseAsset is the asset every leg settles in, minNotional the minimum single take in that asset, maxOrderQty caps one offer and one take, maxPositionQty caps a member's open swap size on the market in either role.
statusACTIVE | HALTED | REDUCEONLY | POSTONLYSESSION | DELISTED. The spellings are frozen for deployed clients; prefer the tradingState vocabulary on /v1/status.
tradingHoursCONTINUOUS on every listing without an auction schedule (revision 3.70): no daily open, close or auction; SCHEDULED on a SECURITY market with an auction schedule (its hours are the auction block of the row, its phase in force tradingPhase); interruptions are the suspension, halt or removal, the circuit breaker and the reduce-only and post-only sessions (the Trading page, Halts and suspensions). Open set: parse defensively, never assume the field is absent or fixed.
A grid change is a re-listing (revision 3.83). The venue never changes the tickSize or the qtyStep of a listed market in place: every resting order and open position is held on the market's grid, so the venue's listing controls refuse such a change on every market kind. A re-listing is announced in writing in advance, naming the market, the new values and the date. On the date the market is delisted (venueEvents STOP_TRADING with reason DELISTING; every working and pending-trigger order cancelled with INSTRUMENT_HALT; new orders and amends 3006, reduce-only orders included), open positions are final-settled at one close-out price, and the market is listed again with the new values and an empty book under the symbol and isin the announcement names (2001 for a kept symbol between the removal and the new listing). A resting order is never kept at the old granularity, moved onto the new grid or rounded. Nothing on the wire announces a re-listing before the date and no member surface serves an instrument id: key on the symbol, re-read this route after the announced date and on any unexpected 2001, 2002, 3006 or DELISTED status, and carry no cached grid across the date. minNotional and the other parameters may change in place.

Trading hours. Every market on the Platform other than a SECURITY market with an auction schedule (Member API section 6j) trades continuously, 24 hours a day, every calendar day of the year, weekends and public holidays included. There is no daily open, close or auction on those markets. Trading in a market is interrupted only by a suspension, halt or removal under Section 11 of the Rulebook (including a venue-wide suspension), by the circuit breaker under Section 13 of the Rulebook, or by the reduce-only and post-only sessions (the Trading page, Halts and suspensions). The Company operates no daily or weekly closure. The only scheduled interruption is the venue's standard maintenance window, Saturday 02:00 to 06:00 UTC, published as a standing exception to the trading hours; maintenance inside it is announced at least 48 hours in advance on the Website and in the member portal. Most releases are rolling and need no window; a release that needs one keeps the restart contract of section 10.7 of the Member API reference (advisory frame, close 1001, 503 with Retry-After, FIX Logout, cursor backfill). A release that requires the venue to stop is announced on the Website at least five Business Days before the window (Business Day as defined in the Rulebook), naming the date, the start time in UTC and the expected duration. A scheduled closure outside that window is published on the Website in advance with the notice the Rulebook requires for a material amendment; an unplanned stop is a suspension under Section 11 of the Rulebook. The current trading state of each market is published at all times on GET /v1/status, GET /v1/instruments and the venueEvents channel. Funding on perpetual contracts settles on the schedule published per market whether or not the underlying reference market is open. A connection interruption during a software release (the venue restart advisory) is not a closure: orders, positions and contracts persist and the market resumes in the trading state it had. Every instrument row carries tradingHours, CONTINUOUS on every listing without an auction schedule and SCHEDULED on a SECURITY market with one (Member API section 6j); the set is open.

Order book

GET/v1/orderbook?symbol=&depth=
ParamTypeRequiredMeaning
symbolstringyesInstrument symbol. Unknown symbol returns 2001.
depthintnoBucketed UP to 1, 5, 25 or 100 levels per side. A request for 10 returns 25 rows, anything above 25 returns 100. Default 100.
{
  "code": 0, "msg": "ok",
  "data": {
    "symbol": "BTC-USD-PERP",
    "bids": [["64999.5", "12"], ["64999", "3"]],
    "asks": [["65000", "7"], ["65000.5", "20"]],
    "checksum": 2016409727
  },
  "asOfSeq": 1041, "ts": 1785000000123
}

Rows are [price, qty], both exact decimal strings, best price first. checksum is a CRC32C over the canonical string of the levels this snapshot carried, capped at the top 25 per side (so min(depth, 25)): at depth 1, 5 or 25 it covers exactly the rows you received, and a depth=100 snapshot returns 100 rows but its checksum still covers only the top 25, so verify a snapshot against the rows you hold (at most 25), never against all 100. WebSocket DELTAS are different: their checksum is always the top 25 of the full book, so continuous delta verification needs a subscription depth of 25 or 100. It is computed the same way, byte for byte, as on the WebSocket orderbook channel, so you can validate a locally maintained book against a REST snapshot with a single integer comparison.

Ticker, mark and index

GET/v1/ticker?symbol=
{
  "code": 0, "msg": "ok",
  "data": {
    "symbol": "BTC-USD-PERP", "lastPrice": "65000",
    "bestBid": "64999.5", "bestAsk": "65000.5",
    "markPrice": "65001.25", "indexPrice": "65002",
    "fundingRate": "0.0001", "predictedFundingRate": "0.00012",
    "fundingRatePct": "0.01", "predictedFundingRatePct": "0.012",
    "fundingRateAnnualizedPct": "10.95",
    "nextFundingTs": 1785009600000,
    "openInterest": "18420", "volume24h": "93110"
  },
  "asOfSeq": 1041, "ts": 1785000000123
}

Price fields are omitted when the venue has no value yet (no trade, no sequenced mark). On spot instruments fundingRate, predictedFundingRate, their Pct twins and openInterest are "0" and nextFundingTs is 0: those are perpetual concepts and are zeroed rather than removed, so one parser handles both kinds. The single exception is fundingRateAnnualizedPct, which is OMITTED on a market with no funding grid rather than zeroed, because a fabricated zero would read as a market that genuinely costs nothing to hold.

GET/v1/mark-price?symbol=
{ "code": 0, "msg": "ok",
  "data": { "symbol": "BTC-USD-PERP", "markPrice": "65001.25", "srcTs": 1785000000090 },
  "asOfSeq": 1041, "ts": 1785000000123 }

Mark price is what risk uses: margin, liquidation, unrealized PnL and MARK-sourced triggers all read this value, not last trade. srcTs is the source timestamp of the underlying observation in milliseconds, so you can detect a stale feed on your side. GET /v1/index-price?symbol= is the identical shape with an indexPrice field. When no mark exists yet, order entry on that instrument returns 3005, which is deliberately distinct from a halt (3003) and from a delisting (3006).

Funding

GET/v1/funding/current?symbol=
{ "code": 0, "msg": "ok",
  "data": { "symbol": "BTC-USD-PERP", "fundingRate": "0.0001",
            "predictedFundingRate": "0.00012",
            "fundingRatePct": "0.01", "predictedFundingRatePct": "0.012",
            "fundingRateAnnualizedPct": "10.95", "fundingIntervalHours": 8,
            "settleCcy": "USD", "nextFundingTs": 1785009600000,
            "funding": { "schedule": "GRID", "intervalHours": 8, "nextSettlementTs": 1785009600000 } },
  "asOfSeq": 1041, "ts": 1785000000123 }

Every perpetual publishes HOW its funding is scheduled in the funding block, here and on GET /v1/instruments (revision 3.42). Two schedules exist today and the set is open:

On a LOCAL_DAILY market predictedFundingRate, predictedFundingRatePct and predictedFundingPerUnit (here, on GET /v1/ticker and on the WebSocket funding rate frame) are the rate the marks recorded so far would settle at, and they are OMITTED, not null and never a fabricated zero, before the day's window opens.

GET /v1/funding/history?symbol=&from=&to=&limit= returns settled rates as {settleTs, settledTs, fundingRate, fundingRatePct, fundingRateAnnualizedPct, fundingPerUnit, markPrice, settleCcy} rows; settleTs is the anchor settled and settledTs (revision 3.70) the time of the settlement command, equal to settleTs plus the timer latency in steady state and later than it for an anchor settled after a stale-mark deferral or a venue outage, omitted on rows recorded before that revision; LOCAL_DAILY rows additionally carry settlementValue (the mean the day settled at), indexAtSnapshot, snapshotsUsed and snapshotTs. Since revision 3.70 includeSkips=true interleaves, newest first, the LOCAL_DAILY days that closed WITHOUT a settlement as rows shaped {settleTs, skipped: true, skipReason, snapshotTs, settleCcy}, skipReason NO_POSITION_SNAPSHOT (no position snapshot was held when the window closed, for example after an outage across the snapshot time) or NO_MARK_SNAPSHOTS (no mark inside the VWAP window), an open set; no funding is exchanged for such a day and none is charged later. The default list is byte-identical to before (never a skipped field on a settled row); a value that is not a boolean answers 400.

The rate is a fraction, not an amount. fundingRate is a dimensionless fraction of position notional per interval: "0.0001" means 0.01% per interval, so a 100,000 USD long pays 10 USD at each settlement while it holds. It has never been a currency figure. fundingRatePct is the same number scaled by 100, and fundingRateAnnualizedPct is rate x (365 days / fundingIntervalHours) x 100, simple and uncompounded, for comparison against other venues.

The currency-denominated figure is the payment, booked in the instrument's settlement currency. Read what a position has actually paid from accFunding on GET /v1/positions or GET /v1/positions/end-clients, and the cash movements from GET /v1/account/ledger?kind=FUNDINGPAY and FUNDINGRECEIVE. Settlement currency is a property of the instrument rather than of the venue, so every surface carrying a funding amount also carries settleCcy. Today every listed market settles in USD; do not hard-code that.

fundingPerUnit is for display; reproduce a charge from the rate and the mark (revision 3.83 states it; nothing changed). fundingPerUnit and predictedFundingPerUnit are the charge of ONE qtyStep at the mark, rounded half-even to the micro, divided by the qtyStep, so a quantity times them can differ from the booked charge by up to half a micro per qtyStep held (and they read 0 while a charge is booked when one lot's charge is at most half a micro); on GET /v1/funding/history the figure is computed when you read the row, with the market's listing at that moment. Every booked charge is reproducible exactly from a settlement's fundingRate and markPrice: netQty x markPrice x contractMultiplier x fundingRate, rounded half-even to 0.000001 of the settlement currency, on your member net position and on each client sub-position, the member charge minus the sum of the client charges added to the sub-position with the largest absolute netQty (the clientUid the venue registered first, then NET, LONG, SHORT, between equal sizes). When your member net position is 0 at the settlement (on a LOCAL_DAILY market, at the snapshot), nothing is booked: no member charge, and no client sub-position is charged, whatever the sub-positions hold. On a LOCAL_DAILY market markPrice is the mean rounded half-even to the tick, the rate is computed from the unrounded mean. The member reference, section 7 "Reproducing a funding charge", has the worked example.

Venue and instrument status

GET/v1/status
{
  "code": 0, "msg": "ok",
  "data": {
    "venue": "ACTIVE", "venueTradingState": "TRADING", "venueSince": 1784900000000,
    "instruments": [
      { "symbol": "BTC-USD-PERP", "status": "ACTIVE", "tradingState": "TRADING" },
      { "symbol": "ETH-USD-PERP", "status": "HALTED", "tradingState": "HALTED",
        "reason": "CIRCUIT_BREAKER", "since": 1785000012000,
        "expectedResumeTs": 1785000042000 }
    ],
    "seqNo": 1041
  },
  "asOfSeq": 1041, "ts": 1785000000123
}

The legacy venue and status fields keep their original spellings byte for byte. The additive venueTradingState, tradingState, reason, since and expectedResumeTs use the same vocabulary as the WebSocket venueEvents channel: tradingState is one of TRADING, HALTED, REDUCE_ONLY, POST_ONLY, DELISTED, and reason is one of CIRCUIT_BREAKER, STALE_MARK_PRICE, DELISTING, ADMINISTRATIVE. reason, since and expectedResumeTs are omitted when they do not apply, which is why the first instrument above has three fewer keys than the second.

Other public routes

EndpointReturns
GET /v1/time{ "serverTime": 1785000000123 }. Use it to measure your clock offset before you start signing.
GET /v1/trades?symbol=&limit=Recent anonymous public trades, {tradeId, symbol, price, qty, side, flags, ts}, with flags in NORMAL | LIQUIDATION | ADL | RFQ | REVERSAL. An RFQ flag marks an off-book negotiated trade and a REVERSAL flag the venue unwinding an erroneous print (reference KX-RV-{originalTradeId}): both print here for transparency but do not move the last-trade print. limit defaults to 100.
GET /v1/klines?symbol=&interval=&from=&limit=OHLCV candles for charting. interval one of 1m, 5m, 15m, 30m, 1h, 4h, 1d (default 1h), limit 1 to 1000 (default 200), rows oldest first; from (Unix ms) is bounded to the most recent limit intervals.
GET /v1/mark-klines?symbol=&interval=&from=&limit=Mark-price candles (revision 3.70): the venue mark folded per interval from its one-minute mark history, the same interval set, limit and from bound as the klines route, rows oldest first {t, open, high, low, close, ticks} (ticks = mark updates in the bucket; no volume, because the mark is not a trade). This is the series a perpetual is margined and funded on and a barrier contract is valued, knocked out and takes profit on; a market without a mark (a swap market, a market never marked) answers an empty array.
GET /v1/insurance-fund{ "enabled": false, "funds": [{ "ccy": "USD", "balance": "0" }] }. This MTF runs no insurance fund, so enabled is always false and balances are 0.

Trading endpoints

Place order

POST/v1/orders
{
  "symbol": "BTC-USD-PERP",
  "side": "BUY",
  "type": "LIMIT",
  "tif": "GTC",
  "price": "65000",
  "qty": "1",
  "reduceOnly": false,
  "clOrdId": "a3f1c9d24b7e4f108c6d2e5b",
  "clientUid": "CLIENT-A",
  "leverage": 50,
  "quoteCcy": "USD"
}
FieldTypeRequiredMeaning and rules
symbolstringyesInstrument symbol from /v1/instruments. Unknown gives 2001. A market whose mode is RFQ has no order book and gives 2022; trade it via POST /v1/rfq instead.
sidestringyesBUY | SELL. Anything else gives 2013.
typestringyesLIMIT | MARKET | STOP_MARKET | STOP_LIMIT | TAKE_PROFIT_MARKET | TAKE_PROFIT_LIMIT. There is no iceberg, no trailing stop and no pegged order type; any such request gives 2004.
tifstringyesGTC | IOC | FOK | POST_ONLY | GTD. Market orders execute as IOC inside the protection collar and never rest.
expireTsint64GTD onlyExpiry, Unix milliseconds. Since Member API revision 3.86 the order never trades at or after it, measured against the time the venue sequences each command; an amendment from that instant answers 2014. The matching engine applies this from its replay-contract line for revision 3.86, the first command of a venue started on the production contract; venue operations announce the date on any environment where that line lies ahead.
pricedecimal stringLIMIT onlyMust align to tickSize or 2002 (a string that is not a decimal numeral at all, an exponent, a blank, letters, is 2013). Omit for MARKET. Not read for the conditional types: a STOP_LIMIT or TAKE_PROFIT_LIMIT takes its resting price from trigger.limitPrice, so setting price there silently does nothing.
qtydecimal stringyesDecimal base-asset units. Must be an exact multiple of qtyStep or 2002 (zero, negative, finer than or off the grid; a string that is not a decimal numeral at all, an exponent such as 1e-4, a blank, letters, is 2013), and price x qty must reach the instrument's minNotional or 2028.
reduceOnlyboolnoPerps only; 2016 on spot. Can never increase your MEMBER position, and resting reduce-only orders are trimmed or canceled automatically as the member position shrinks. The budget is member-scoped, never per end client: see the warning below before using it to close an end client.
positionSidestringnoLONG | SHORT, hedge mode: routes this order's fills to that sub-position bucket of the order's clientUid, so one client can hold a long and a short sub-position in the same instrument at once. ABSENT means netting, exactly as before the field existed. Order-book perpetual markets only: 2016 on a spot instrument, 2013 on any other value; RFQ and barrier flows do not take the field. Echoed on the ack, on open-order reads and on order events, OMITTED (not null) for netting orders. See Hedge mode below.
triggerobjectconditionals only{source, price, direction, limitPrice}. Required for the four conditional types; supplying it on a LIMIT or MARKET order gives 2013.
trigger.sourcestringyesLAST | MARK | INDEX.
trigger.pricedecimal stringyesTick-aligned trigger level.
trigger.directionstringyesAT_OR_ABOVE | AT_OR_BELOW.
trigger.limitPricedecimal string*_LIMIT onlyThe resting price after triggering. Missing gives 2013.
clOrdIdstringrecommendedYour reference, 36 characters or fewer, unique per member per 24 hours, and the idempotency key (see below). Required for idempotency rather than by the validator: an omitted or empty clOrdId is accepted, but the order then has no duplicate protection and a blind retry can double it, so send one on every order. The KX- prefix in any casing is reserved and gives 2020.
clientUidstringyesYour opaque end-client identifier, 64 characters or fewer, [A-Za-z0-9._:-]. Missing gives 2010 unless a house identifier is configured for the member. It rides every response, execution and rejection for the order. Since revision 3.72 (in force from the date venue operations announce) a clOrdId, a clientUid or a Trader ID of the reg block that begins or ends with whitespace rejects with 2013 and a msg naming the rule, per entry on the batch route and on the RFQ routes too: trim the value where you build it.
leverageintnoAssertion, not a control. See below.
quoteCcystringnoAssertion: compared case-insensitively against the instrument's quoteAsset; mismatch gives 2018. Echoed back on the ack.
regobjectnoRTS 24/22 order-record fields: traderId, tradingCapacity (DEAL | MTCH | AOTC), clientIdType (LEI | NATIONAL_ID | INTERNAL), clientId, investmentDecisionId, executionDecisionId, dea, liquidityProvision, riskReducing (boolean, revision 3.70, absent = false, only meaningful on commodity-derivative markets, RTS 22 field 64: the transaction objectively reduces risk; FIX tag 9029). Journaled verbatim, immutable, never affects matching.
reg.tradingCapacitystringstagedThe VENUE trading capacity of the order, spelled in the MiFIR vocabulary but NOT MiFIR field 29: it declares the legal name of execution, and your RTS 22 report label derives from your configured per-member mapping (the only mapping the venue ever configures is MTCH reported as AOTC, section 6f of the reference). DEAL = own account; MTCH = own name for a client account INCLUDING commission business (Finanzkommissionsgeschaeft) whatever your home regime reports; AOTC = agency in the name of the identified client. Recorded verbatim always. When capacity enforcement is at the ENFORCE stage for your member (on production ENFORCE from your first order; in the sandbox staged per member: off, warn, enforce, agreed with venue operations), the field becomes required here, on RFQ creation and on RFQ quotes, and a violation gives 2033: the value must be exactly DEAL, MTCH or AOTC and permitted by your licensing profile, DEAL requires clientUid absent or your house reference, MTCH and AOTC require a real end-client clientUid. Your daily DTF/COH flow statements bucket by these venue semantics (own-name execution accrues to dealing on own account, IFR Article 4(1)(33)); unrecognised history counts as UNKNOWN and shows in the coverage percentage.
reg.traderIdstringonce registeredThe venue-issued Trader ID of the Authorised User entering the order, of the form TR-{memberId}-{8 hex characters}. Optional until your member has registered Authorised Users, mandatory on every order from that point on: missing or unregistered gives 2021. See below.
Strict order entry. An unrecognised JSON field is rejected with code 2017, not ignored. The message names the offending field. This is deliberate: a client asserting a constraint the venue does not model, a trailing-stop parameter or a leverage the instrument does not run, must hear that the assertion did not apply rather than have the field dropped and the order worked without it. The same strictness applies to the amend body and to the nested order objects of POST /v1/margin/preview. Because a 2017 means the body never parsed, it is the one order rejection that cannot echo your clOrdId.
reduceOnly is judged against your MEMBER NET position, not against the end client on the order. The admission check reads your member net for the instrument and nothing else; it never reads an end client's sub-position, because no risk logic at this venue does. The clientUid decides where the fills land, never whether the order is admitted. So an end client closing its OWN position is rejected with 2008 REDUCE_ONLY_VIOLATION whenever your aggregate book leans the other way, and a perfectly netted member book, say client A long 10 against client B short 10, refuses every reduce-only order on that instrument, from either client, on either side. To close an end client, send a PLAIN order with no reduceOnly, sized against that client's own position from GET /v1/positions/end-clients: only you know your client-level book. That is also why POST /v1/lots/{lotId}/close defaults reduceOnly to false. The flag is a venue-enforced guarantee that an order can only SHRINK your house exposure, for cases like a risk desk flattening the member book during a margin call. Full rules on the Trading rules page.

The leverage assertion. The venue margins your member account on the instrument tier schedule and does not model per-end-client leverage at all. The field is validated and then echoed; it never changes the margin applied. It holds only when the instrument runs a single open-ended risk tier whose effective initial margin, that is the tier rate multiplied by your member IM multiplier, is exactly 1 / leverage as an exact integer identity. Two consequences worth building for: on a TIERED instrument every whole-number leverage assertion is false and returns 2018, because effective leverage is size-dependent; and the check is member-aware, so if the venue runs your account at a stricter profile your effective leverage is maxLeverage / imMultiplier (read imMultiplier from GET /v1/account) and that is what the assertion is compared against. If you offer clients a fixed leverage, enforce it on your side before submitting.

Response. The synchronous outcome: final status plus any executions produced while matching. status is one of NEW, PARTIALLY_FILLED, FILLED, CANCELED, EXPIRED, REJECTED, PENDING_TRIGGER. A conditional order that has not yet triggered reports PENDING_TRIGGER on the ack and on every surface (WS order frames, live reads, the event stream) until it triggers, at which point it becomes NEW.

{
  "code": 0, "msg": "ok",
  "data": {
    "orderId": 887711, "clOrdId": "a3f1c9d24b7e4f108c6d2e5b",
    "symbol": "BTC-USD-PERP", "side": "BUY", "type": "LIMIT", "tif": "GTC",
    "price": "65000", "qty": "1", "remainingQty": "0", "reduceOnly": false,
    "clientUid": "CLIENT-A", "status": "FILLED",
    "executions": [
      { "tradeId": 55123, "orderId": 887711, "symbol": "BTC-USD-PERP", "side": "BUY",
        "price": "65000", "qty": "1", "fee": "32.5", "role": "TAKER",
        "flags": "NORMAL", "clOrdId": "a3f1c9d24b7e4f108c6d2e5b",
        "refSource": "MEMBER", "clientUid": "CLIENT-A", "ts": 1785000000118 }
    ],
    "acceptedTs": 1785000000117,
    "leverage": 50,
    "quoteCcy": "USD"
  },
  "asOfSeq": 104873, "ts": 1785000000119
}

Queue position on the ack. When a placement RESTS an order on the book, the ack carries two additional fields: queueQtyAhead, a decimal quantity string, the quantity resting ahead of your order at its price level in FIFO priority, and queueOrdersAhead, an integer, the number of orders ahead of it at that level. The fields are PRESENT exactly when the placement left the order resting: a GTC or GTD limit that rested in full, the resting remainder of a partial fill, a post-only rest. A first order at an empty level reports "0" and 0, present, meaning front of the queue. The fields are ABSENT, not null, wherever no queue position exists: full fills, the canceled remainder of an IOC, FOK or market order, conditionals accepted as PENDING_TRIGGER, rejections, amend responses and order-history reads. Presence means resting AT ACK TIME: when the same command that placed the order also terminates it after the rest, for example a stop released by the placement's own prints crosses into the just-rested remainder, the ack reports the terminal status and the queue fields are cleared. The figures are a snapshot taken at the moment the order rested, measured after any reduce-only trim the same placement caused, and they are immediately stale: the venue never updates them on the order. For a live picture, watch the book channels. The same two fields ride the WS order.place ack and the ORDER_ACCEPTED frame of the private order channel, and on FIX the accept ExecutionReport carries them as venue tags 9017 and 9018 (see the FIX page).

Rejection ordering inside the gateway is fixed and worth knowing when you interpret a failure: authentication, then body parse (2017), then the reserved KX- prefix (2020), then credential scope (1005), then the order-message bucket (1003), then the RTS 9 regime (1004), then field validation (2001, 2021, 2018, 2002, 2004, 2013), then the engine itself (2003, 2005 to 2012, 2015, 2016, 2022, 2028, 3001 to 3006).

Hedge mode (positionSide)

By default the venue keeps ONE netted sub-position per (clientUid, instrument): opposite flow for the same client nets, so a client cannot be long and short the same perpetual at once. The optional positionSide field lifts that, per order, without a second clientUid. Tagging an order LONG or SHORT routes its fills to that bucket of the client's sub-position: a client can hold at most three sub-positions per instrument, one LONG bucket, one SHORT bucket and the netting bucket every untagged order acts on. Netting still applies inside a bucket, so a BUY tagged SHORT reduces the short bucket (and extends through zero, flipping inside the bucket, exactly as netting always did), and tagged and untagged flow never touch each other's buckets. The same field and semantics apply on the WS order.place op, on every batch entry and on FIX (venue tag 9016, see the FIX page).

Worked example. Your client HEDGE-1 buys 0.5 BTC-USD-PERP tagged LONG, then sells 0.3 tagged SHORT. The venue now holds two sub-positions for the client, LONG +0.5 and SHORT -0.3, and your member position moved by the net +0.2, exactly as it would have moved without the field. A later BUY of 0.1 tagged SHORT reduces the short bucket to -0.2. An order without positionSide keeps acting on the client's netting sub-position, which the tagged flow never touched.

Margin is completely unaffected. Margin stays member-level only: your member position is the net sum over all buckets of all clients, and risk never reads sub-positions. Hedged client flow therefore nets at member level for margin exactly as it always did; the buckets change reporting and per-client accounting, never your margin requirement. Funding attribution, liquidation allocation and final settlement operate per sub-position exactly as they always did per client, bucket by bucket, and a venue-initiated close names the bucket it closed. reduceOnly composes with positionSide: the reduce-only budget stays member level, while the fills act on the tagged bucket. STP is unaffected: two orders with the same clientUid trigger your STP policy regardless of their buckets.

GET /v1/positions/end-clients (and the WS clientPosition channel and get.clientPositions op) report each bucket as its own row: rows carry positionSide ("LONG" | "SHORT"), omitted on netting rows, so a client using hedge mode can show up to three rows per instrument. Acks, open-order reads and order events echo positionSide back, OMITTED (not null) for netting orders, so every pre-hedge payload stays byte-identical.

Batch order entry

POST/v1/orders/batch

One signed message, 1 to 50 complete, independent orders. Every entry is a full place-order body with its own clOrdId and its own clientUid; that is the point of the feature: one message, N client-tagged orders, for example one close per end client when you unwind a book. Each entry passes exactly the validation a single POST /v1/orders passes, including the minNotional floor with its reduce-only exemption, the order-size and position caps exactly as they apply to singles (2013 and 2011: each entry is one message against the order-size cap, and an earlier entry's working remainder counts in a later entry's worst case), strict unknown-field rejection (2017) and the reserved KX- namespace guard (2020). The worked example below is the mass-close use case: one message unwinds three end clients' long positions with three reduceOnly sells, each entry tagged with the client it closes, against a member book that is itself net long. The warning under the example explains why that last qualifier is load-bearing.

{
  "orders": [
    { "symbol": "BTC-USD-PERP", "side": "SELL", "type": "LIMIT", "tif": "GTC",
      "price": "65000", "qty": "0.2", "reduceOnly": true,
      "clOrdId": "close-cl-a-1", "clientUid": "CLIENT-A" },
    { "symbol": "BTC-USD-PERP", "side": "SELL", "type": "LIMIT", "tif": "GTC",
      "price": "65000", "qty": "0.5", "reduceOnly": true,
      "clOrdId": "close-cl-b-1", "clientUid": "CLIENT-B" },
    { "symbol": "BTC-USD-PERP", "side": "SELL", "type": "LIMIT", "tif": "GTC",
      "price": "65000", "qty": "0.1", "reduceOnly": true,
      "clOrdId": "close-cl-c-1", "clientUid": "CLIENT-C" }
  ]
}
The mass-close example works because the MEMBER is net long. The reduce-only budget is member-scoped, so those three entries are not three independent client closes: they are three claims on one member-level budget, and together they must stay within your member net long position of 0.8 or the surplus entries reject with 2008. Tagging each entry with the client it closes does not give it a budget of its own. Where your clients hold OFFSETTING positions, this shape does not work at all, and a member book that nets to zero refuses every entry: unwind such a book with PLAIN entries, no reduceOnly, each sized against that client's own position from GET /v1/positions/end-clients. See reduceOnly semantics.

Non-atomic by design. The batch is a transport optimisation, not a transaction: each entry validates and executes independently, one entry's rejection never affects its neighbours, and there is no all-or-nothing execution mode. Entries are submitted to the matching engine in array order, and array order is the engine sequencing order among your entries (two crossing entries behave deterministically: the earlier rests, the later matches it). Other members' traffic may interleave between your entries.

The response is HTTP 200 with envelope code 0 and data.results, one entry per order in array order, even when every entry failed. A successful entry has exactly the shape of the single-order place response data (engine-level rejects such as 2028, an order-size cap 2013 or a position cap 2011 ride it as rejectCode with status REJECTED); a failure entry is {"code", "msg", "clOrdId"}: a gateway refusal (strict fields and wrong-typed values 2017, 2020, 2002 and so on; an entry's member names are case-sensitive, where the single POST /v1/orders body is not), the matching engine's 2009 for a reused clOrdId with a different payload, 5001 for an entry the venue did not accept (after an entry the venue's intake refused as it was submitted, every later entry answers 5001 "not submitted"; on a clustered deployment the replicated intake can also refuse an entry after the later ones were submitted, and those may take effect, so read every row) and, since revision 3.74, 5000 for an entry whose outcome the venue could not confirm: reconcile that row by its clOrdId and resend it only with the SAME clOrdId. Before revision 3.74 such a fault answered the whole request with an undocumented HTTP 500. Envelope-level errors are limited to authentication, credential scope (1005), throttle (1003), a malformed body or an entry count outside 1 to 50 (2002 with a descriptive msg), the engine-busy shed (5001), the RTS 9 block (1004) and 5030 on a clustered deployment's follower node; the 5001 and the 1004 come after the charge, so the N units stay spent.

For the worked example above, with 0.2 resting on the bid at 65000, array order means the first entry takes that bid and fills while the second and third rest working:

{
  "code": 0, "msg": "ok",
  "data": {
    "results": [
      { "orderId": 887731, "clOrdId": "close-cl-a-1", "symbol": "BTC-USD-PERP",
        "side": "SELL", "type": "LIMIT", "tif": "GTC", "price": "65000",
        "qty": "0.2", "remainingQty": "0", "reduceOnly": true,
        "clientUid": "CLIENT-A", "status": "FILLED",
        "executions": [
          { "tradeId": 55131, "orderId": 887731, "symbol": "BTC-USD-PERP",
            "side": "SELL", "price": "65000", "qty": "0.2", "fee": "6.5",
            "role": "TAKER", "flags": "NORMAL", "clOrdId": "close-cl-a-1",
            "refSource": "MEMBER", "clientUid": "CLIENT-A",
            "ts": 1785000000117 } ],
        "acceptedTs": 1785000000117 },
      { "orderId": 887732, "clOrdId": "close-cl-b-1", "symbol": "BTC-USD-PERP",
        "side": "SELL", "type": "LIMIT", "tif": "GTC", "price": "65000",
        "qty": "0.5", "remainingQty": "0.5", "reduceOnly": true,
        "clientUid": "CLIENT-B", "status": "NEW", "acceptedTs": 1785000000117 },
      { "orderId": 887733, "clOrdId": "close-cl-c-1", "symbol": "BTC-USD-PERP",
        "side": "SELL", "type": "LIMIT", "tif": "GTC", "price": "65000",
        "qty": "0.1", "remainingQty": "0.1", "reduceOnly": true,
        "clientUid": "CLIENT-C", "status": "NEW", "acceptedTs": 1785000000117 }
    ]
  },
  "asOfSeq": 104874, "ts": 1785000000119
}

Had one entry failed gateway validation instead, only that row would change, for example {"code": 2002, "msg": "price not on the tick grid, or qty not an exact multiple of qtyStep", "clOrdId": "close-cl-c-1"}; its neighbours stand, and you resend only the corrected entry.

Rate limits. A batch consumes one order-budget unit per entry, charged up front, all or nothing: a denied batch answers 1003 with retryAfterMs and consumes nothing. Every entry counts as one order message under the RTS 9 regime, whatever its per-entry outcome. The identical operation is available on the private WebSocket as op orders.batch; it is not offered over FIX, and FIX members submit singles instead.

Batch amendment and batch cancellation

POST/v1/orders/batch/amend
POST/v1/orders/batch/cancel

Revision 3.74. Two more routes carry 1 to 50 entries in one signed message, for a market maker that requotes many levels: the amendment batch amends and the cancellation batch cancels orders you already have. Every entry the venue submits reaches the matching engine as the ORDINARY single route: an amendment entry exactly as PUT /v1/orders/{orderId}, a cancellation entry exactly as DELETE /v1/orders/{orderId}. The WebSocket twins are orders.batchAmend and orders.batchCancel; neither route is offered over FIX.

POST /v1/orders/batch/amend
{ "amends": [
    { "orderId": 887731, "qty": "0.15" },
    { "orderId": 887732, "price": "64990.5" },
    { "orderId": 887733, "price": "65010", "qty": "0.2" } ] }

POST /v1/orders/batch/cancel
{ "cancels": [
    { "orderId": 887734 },
    { "clOrdId": "q-ask-3" } ] }

The entries. The body carries exactly one member, amends or cancels (anything else is 2002 for the whole message). An amendment entry is {orderId, price?, qty?}: qty is the new TOTAL quantity, fills included; a quantity decrease at the same price keeps queue priority, any price change or quantity increase is cancel plus new, and every engine check of the single amendment applies at the current state, also to an order that already stands at the requested terms. An amendment names its order by orderId only. A cancellation entry is {"orderId": ...} or {"clOrdId": ...}, exactly one; a clOrdId is compared character for character and never refused for padding. Entry member names are case-sensitive and a value of the wrong JSON type answers 2017 "malformed request body"; 2013 answers an entry that is not an object, an orderId absent or below 1, a cancellation entry with both identifiers or neither and an empty clOrdId. An order named by two entries: the first runs, every later entry naming it answers 2013 "order N is already named by entry k of this batch" (k counted from 0) and nothing is submitted for it.

Non-atomic by design. Entries reach the matching engine in array order, other members' traffic may interleave, and a refused entry never stops a later one: every entry is submitted before the first verdict. Send an entry that depends on another's outcome in a SECOND message after you read the first message's rows. An amendment that reprices re-matches as a taker outside a call auction, so when you shift a two-sided quote amend the side moving away from the other first (in a shift up the asks before the bids) and size down before you size up; the advice holds only when every earlier entry takes effect. Under a margin shortfall, a reduce-only session, a band move or the funding catch-up fence a size-down amendment is refused like any other, so de-risk with the cancellation batch.

The response. HTTP 200, envelope code 0, data.results one row per entry in array order, and asOfSeq, even when every entry failed. A row carrying code is a failure row, {"code", "msg", "orderId", "clOrdId"} echoing only what the entry carried (orderId as a JSON integer of at least 1, or the order a reference resolved to; clOrdId as a JSON string). Any other row is the order: for an amendment its state as of asOfSeq, the PUT data (when the venue could not read your orders back after the batch, which is rare, the order's last state change among the batch's own entries as the order channel renders it, and asOfSeq the lowest seqNo at which a still-working row was taken, or the seqNo before it when the command that made that row's change also traded the order, the row then being the order before those fills: every row is then the order as of asOfSeq or later, and the order and execution events after it bring it up to date), which can read REJECTED (2005, a post-only order repriced to cross; 2015 under REJECT_TAKER) or CANCELED (STP_CANCEL): that level is gone, place it again with a placement batch. For a cancellation the cancelled order, the single cancel's data. rejectCode 5005 on an order row means the entry took effect before a contained fault: reconcile, never resend it as it was.

{ "code": 0, "msg": "ok",
  "data": { "results": [
    { "orderId": 887731, "clOrdId": "q-bid-1", "symbol": "BTC-USD-PERP", "side": "BUY",
      "type": "LIMIT", "tif": "GTC", "price": "64995", "qty": "0.15", "remainingQty": "0.15",
      "reduceOnly": false, "clientUid": "H1", "status": "NEW", "acceptedTs": 1785000000000 },
    { "code": 2014, "msg": "order not found", "orderId": 887732 },
    { "orderId": 887733, "clOrdId": "q-bid-3", "symbol": "BTC-USD-PERP", "side": "BUY",
      "type": "LIMIT", "tif": "POST_ONLY", "price": "65010", "qty": "0.2", "remainingQty": "0.2",
      "reduceOnly": false, "clientUid": "H1", "status": "REJECTED", "rejectCode": 2005,
      "acceptedTs": 1785000000118 } ] },
  "asOfSeq": 104880, "ts": 1785000000121 }

Cancellations are looked up first, and free. The venue resolves every cancellation entry to an OPEN order of yours in one read; an entry naming nothing (not found, not yours, terminal, or a reference naming no open order) answers 2014 and nothing is submitted for it. A cancellation entry is never refused by a halt, a suspension, a risk state, a session measure or a DLT freeze. No order-budget unit, never throttled, never blocked by the RTS 9 regime; each submitted entry counts one order message on your account-wide ratio and each cancelled order one on its market. One cancellation batch of yours is processed at a time on a venue node, a later one waits for it; 5001 when eight already wait. A WebSocket session processes one message at a time, so keep a second private socket for urgent cancels.

Amendments are charged. One order-budget unit per entry, charged up front, all or nothing, after the venue's engine-busy check (1003 and an engine-busy 5001 consume nothing); the units of entries refused afterwards (2014, 1004, 2049, a grid refusal) stay spent. An entry on a market where the RTS 9 regime blocks you answers a 1004 row and the other entries run; an entry counts one order message when the venue hands it to the matching engine. Send only the levels that changed: a no-op entry costs a unit and an order message like any other.

Envelope errors and unsettled rows. The envelope refuses only before anything is submitted: 1001, 1005, 2002, 5001 (the shed, the orders could not be looked up, or your cancellation queue is full), 5030 with the leader on a clustered deployment's follower node, and for the amendment batch 2034 (production) and 1003. After the first submission every outcome is a row: 5001 for an entry the venue did not accept (after an entry the intake refused as it was submitted, every later entry answers 5001 "not submitted", so the accepted entries are the first ones of your array; on a clustered deployment the replicated intake can instead refuse an entry after the later ones were submitted, which then answer their own outcomes and may have taken effect, and that entry counts as an order message like every entry handed over: read every row, never infer the rest from the first 5001), 5000 "outcome unknown" for an entry whose outcome it could not confirm (read the order back on the leader before sending it again), 5005 when a contained fault left nothing in effect. A refused RESEND of an amendment does not mean the first attempt failed: read GET /v1/orders/open before acting.

Withdrawal and re-certification. The venue may withdraw either route venue wide during an incident, or for your member as a Rules measure notified by mail with the section 36 appeal route; a withdrawn route answers exactly as before revision 3.74, HTTP 404 with no body (WebSocket 2013 "unknown op"), and your single amendments and cancels stay. Adopting either route adds a surface outside your certified scope: re-certify with the Conformance Test Plan's rows X-07 to X-11 before production use.

Trader ID and Authorised Users

Venue operations register the Authorised Users of each member and issue each one a Trader ID of the form TR-{memberId}-{8 hex characters}, for example TR-12-9F3A21BC. The Trader ID of the person entering the order rides reg.traderId.

Enforcement is staged per member. While your member has no registered Authorised Users you are not gated at all, so an existing integration that never sends reg keeps working unchanged. From the moment your first Authorised User is registered, every order must carry a registered, active reg.traderId. One that is absent, unknown or deactivated is rejected with code 2021, HTTP 400, "reg.traderId missing or not a registered Authorised User". It is deterministic: fix the value, do not retry.

Order entry runs through one code path shared by all three transports, so REST, WebSocket and FIX reject identically and a gated member cannot route around the check by changing protocol. On FIX the Trader ID rides venue tag 9012 on NewOrderSingle. The member web portal's RFQ functions apply the same check (Member API 3.70). POST /v1/margin/preview is never gated, because a preview is a read rather than an order entry, which keeps risk tooling working for a gated member.

Plan for the switchover. Populate reg.traderId on every order before your first Authorised User is registered, not after. The field is accepted and journaled verbatim while you are ungated, so you can ship and verify the change with no behaviour change on either side, and registration then becomes a no-op for your order flow. Code 2021 is listed with every other code on the Reference page.

Cancel and amend

DELETE/v1/orders/{orderId}

Cancel by venue order id. Returns the cancelled order, rendered from the cancel event: status CANCELED, cancelReason USER_REQUESTED, acceptedTs the cancel's time. 2014 if the id is unknown or belongs to another member; the two cases are deliberately indistinguishable so an id probe leaks nothing. An empty id, DELETE /v1/orders/ with a trailing slash (what a path built from this template becomes when the id is empty), answers HTTP 400 5003 naming the empty orderId and cancels nothing, with or without a query (Member API revision 3.84); before, that path was the cancel of every open order of the route below.

DELETE/v1/orders?clOrdId=

Cancel by your own reference, resolved against your member's active reference index. 2014 when no working order carries that reference. This is the reconciliation tool of choice after a timeout: it is safe to call, it is idempotent in effect, and it tells you whether the order existed. Send clOrdId alone: since Member API revision 3.84 a symbol or side beside it is refused 5003 and nothing is cancelled.

DELETE/v1/orders?symbol=&side=

Cancel all your working orders, optionally narrowed by symbol and/or side (BUY or SELL, in any case). Both filters are optional; without either the cancel covers every open order you have on every market, untriggered conditionals included, and an unknown or empty symbol gives 2001. Since revision 3.74 this route and the cancel by clOrdId count one order message on your account-wide order-to-trade ratio, as every other cancel already did (that ratio never blocks). Returns a count, not a list:

{ "code": 0, "msg": "ok", "data": { "canceled": 37 }, "asOfSeq": 104901, "ts": 1785000000500 }
The route reads clOrdId, symbol and side, and nothing else (Member API revision 3.84). A filter the venue cannot read is refused with nothing cancelled: a query parameter the route does not define (?clientUid=, ?orderId=, a misspelt ?symbl=), a parameter given twice, a side other than BUY or SELL (an empty one included) and clOrdId with symbol or side answer HTTP 400 5003 with a msg naming the parameter, and a request body answers 2017. Parameter names match without regard to case. Before revision 3.84, without clOrdId, each of these was ignored or widened the filter and the request ran as a cancel of every open order the remaining filters matched, without one every open order of yours, every end client's untriggered stops included (a repeated symbol answered 2001, and with clOrdId at most that one order was cancelled); the migration table is in section 6 of the Member API. There is no clientUid filter: cancel one end client's orders by orderId with POST /v1/orders/batch/cancel, the ids and their clientUid from GET /v1/orders/open. The correction is in force at publication by the operator's decision of 2026-10-20, the venue being before production go-live, the 30-day notice of the versioning policy of the Member API waived for it. Only /v1/orders without a trailing slash is this route: DELETE /v1/orders/, with or without a query, is the cancel of one order with an empty orderId and answers 5003 with nothing cancelled, in force at publication by the operator's decision of 2026-10-21, the venue being before production go-live, the 30-day notice of the versioning policy of the Member API waived for it.
PUT/v1/orders/{orderId}

Amend price and/or quantity. The body accepts exactly two optional fields and is strict about everything else:

{ "price": "64950", "qty": "2" }

A quantity decrease at an unchanged price keeps queue priority. Any price change or quantity increase is processed as cancel plus new: fresh priority, re-matched immediately. Margin, price band and position limits are re-checked. Outside a call auction the re-match treats the repriced order as a new one: a post-only order that would cross is rejected 2005 and removed, exactly like a post-only order that would cross at entry, and an order that meets your own resting order with the same clientUid follows your self-trade policy (REJECT_TAKER rejects the amended order 2015, or cancels it STP_CANCEL when part filled; CANCEL_TAKER and CANCEL_BOTH cancel it STP_CANCEL; the default CANCEL_RESTING cancels your resting order instead), while your own order with a different clientUid trades under the default end-client scope. These outcomes answer inside HTTP 200 with the order's status REJECTED or CANCELED (revision 3.74 corrects this paragraph, which said nothing of them); during a call auction, or while the market is closed, the repriced order rests for the uncross. Every other refusal leaves the original order untouched. The response is the order read just after the amendment (the open order, else its terminal record), so it can already carry a later fill, while the envelope asOfSeq is the amendment's own sequence number (revision 3.83 states it). An amendment consumes one order-budget unit exactly like a placement or a batch entry and is subject to the RTS 9 block; cancels are never throttled.

Order state

EndpointReturns
GET /v1/orders/{orderId}One order, working or terminal, in the same shape as the place response. 2014 when unknown.
GET /v1/orders/open?symbol=Array of your working orders, including untriggered conditionals. Optional symbol filter. status is the live state: NEW while nothing has filled, PARTIALLY_FILLED once any of the order has, whether it part filled on entry or its resting remainder was later consumed by an aggressor (revision 3.60; before it the latter case read NEW with remainingQty below qty, and the sandbox keeps reading so until venue operations announce the activation of that rule). GET /v1/orders/{orderId} and the WS get.openOrders / get.order reads share the semantics.

acceptedTs per surface (revision 3.83, documentation only: nothing changed). The field names a different instant on different surfaces. On every WebSocket order frame and its rows of the event stream, and on every answer rendered from an order event (the DELETE /v1/orders/{orderId} answer, the rows of a cancellation batch, a PUT answer that ended the order, and a read of an order already terminal), it is the venue's sequencing time of that event: the acceptance, the amendment (a venue reduce-only trim included), the cancel, the trigger, the completing fill, the expiry or the rejection; on FIX, TransactTime(60) is the same instant. On the open-order reads, and on GET /v1/orders/{orderId} and the PUT answer while the order is open, it is the placement time, moved to an amendment's time only when an amendment of a working order changes the price or increases the quantity (a size-down, a venue trim, any amendment of an untriggered conditional and a trigger keep it). On GET /v1/orders/history a FILLED, CANCELED or EXPIRED row carries the first acceptance (and closedTs the terminal event), a REJECTED row the rejection time in both; from and to bound acceptedTs.

Do not poll open orders in a loop. GET /v1/orders/open is a reconciliation and startup tool, not a state feed. Polling it tightly burns your read bucket, hits 1003, and still delivers state later than the WebSocket order channel would have. Consume the socket and use this endpoint to verify.

RFQ trading, barrier contracts and yield contracts

Every instrument on GET /v1/instruments carries a mode: ORDER_BOOK or RFQ. An RFQ market has no order book at all. Order entry there is rejected with code 2022, and trading runs as a request-for-quote negotiation instead: a requester opens an RFQ, other members quote it, and the requester accepts exactly one quote. The routes below are the whole surface. Opening an RFQ on an order-book market is rejected with code 2023, so the two trading styles cannot be confused by accident in either direction. Read the mode from reference data and route accordingly.

Five instrument kinds trade this way. On a PERP market an accepted quote is an off-book perpetual trade, margined and netted exactly like a book fill. On a SPOT market it is prefunded delivery at the quoted price. On a SECURITY market (Member API revision 3.66) it is prefunded delivery exactly like spot, settled on the Kronos X chain (the DLT Market section below). On a BARRIER market it opens a knock-out contract, documented in its own section below; barrier markets are always RFQ. On a SWAP market (revision 3.51) the same verbs run the other way round: POST /v1/rfq is the WRITER's standing offer, POST /v1/rfq/{rfqId}/accept with qty and no quoteId is a TAKE by any other member, and quotes do not exist; see "Yield contracts" below.

Anonymity is structural and symmetric. Makers never learn who is requesting, and the requester never learns who quoted: price, size and validity are the negotiation surface, identity is not. Market Segments are enforced throughout: an RFQ is visible and quotable only to members inside the requester's mutual-acceptance set, and eligibility is re-checked at accept. RFQ is served over REST and the private WebSocket channels only; there is no FIX representation of the flow.

Bucket assignment. POST /v1/rfq, POST /v1/rfq/{rfqId}/quotes and POST /v1/rfq/{rfqId}/accept are order messages: they consume the order bucket (code 1003 on a breach, with the same retryAfterMs contract), require a TRADE credential (1005 on drop copy) and are gated by the Trader ID regime (2021) exactly like POST /v1/orders. The GET routes in this section are charged to the read bucket. The two cancel routes consume neither bucket: withdrawing an RFQ or a quote is never rate limited and never blocked, for the same reason order cancels are not.
Regulatory record. The reg object on the create AND on every quote is journaled verbatim on the venue's regulatory record; an acceptance stamps both legs' records onto the trade, so RFQ and barrier executed value classifies by reg.tradingCapacity in your DTF/COH flow statements (the maker leg by the quote's capacity, the requester leg by the create's). Carry reg.tradingCapacity on every quote: a quote without one classifies its maker leg as UNKNOWN. Under ENFORCE-stage capacity enforcement the field is required on creates and quotes and a violation rejects with 2033.

Open an RFQ

POST/v1/rfq
{
  "symbol": "BTC-USD-KO",
  "side": "BUY",
  "qty": "10",
  "expiresInMs": 60000,
  "barrier": { "level": "60000", "expiryTs": 1785086400000, "takeProfit": "70000" },
  "clOrdId": "rfq-req-0001",
  "clientUid": "CLIENT-A",
  "reg": { "traderId": "TR-12-9F3A21BC" }
}
FieldTypeRequiredMeaning and rules
symbolstringyesAn RFQ-mode market from /v1/instruments. Unknown gives 2001; an order-book market gives 2023.
sidestringyes (ordinary request; OMIT on a close request)BUY | SELL, your side of the eventual trade. On a barrier market this is the holder direction: BUY holds long, SELL holds short. Anything else gives 2013.
qtydecimal stringyes (ordinary request; OMIT on a close request)Decimal base-asset units. Must align to qtyStep or 2002, respect the instrument's order-size cap (2013: maxOrderNotional valued at the current mark where published, else the base-unit maxOrderQty), and reach minNotional valued at the current mark (2028); every quote is re-checked against the floor, and against the notional order cap, at its quoted entry price.
expiresInMsint64noThe quoting window in milliseconds, default 60000. Minimum 1 second, maximum 24 hours; outside gives 2013. When the window closes the RFQ expires and every live quote dies with it.
barrierobjectbarrier marketsRequired on a BARRIER market, forbidden on a perp or spot RFQ market; either violation gives 2027.
barrier.leveldecimal stringyesThe knock-out level, tick-aligned (2002). For BUY it must sit below the current mark, for SELL above it; the wrong side gives 2027. It is re-validated against the quoted entry price at quote time and again at accept.
barrier.expiryTsint64noContract expiry, Unix milliseconds; must be in the future (2027). Omit it for an open-ended contract on a market that publishes no maxContractLifeHours; on a market that publishes one (revision 3.70, GET /v1/instruments) it is required and at most that many hours after the request (2027 otherwise).
barrier.takeProfitdecimal stringnoThe optional take-profit level (revision 3.43), the second exit, tick-aligned (2002). It sits on the PROFIT side of the entry: for BUY strictly above the current mark (and above the quoted entry at quote and accept), for SELL strictly below both; the wrong side, or a level not strictly beyond the entry by at least one tick, gives 2027. The barrier and the take profit therefore always lie on opposite sides of the entry. When the mark reaches it the contract terminates AT this price, fee-free like a knock-out. Omit it for a knock-out-only contract.
barrier.strictTakeProfitbooleannoSTRICT take profit (revision 3.45): the contract pays ONLY if the take profit is touched before the contract expiry; at the expiry without a touch the holder loses the whole premium whatever the mark. Requires barrier.takeProfit, a barrier.expiryTs at most 24 hours after the request, and a market whose strictTakeProfitPremiumBps is nonzero (GET /v1/instruments), else 2027; re-checked at accept. Priced at strictTakeProfitPremiumBps of the barrier distance instead of the normal 10000 - knockOutRebateBps share. Omit or send false for a normal contract.
clOrdIdstringyesYour reference, 36 characters or fewer. The KX- prefix is reserved (2020). Echoed on every response and rejection for the RFQ.
clientUidstringyes (ordinary request; optional on a close request)Same rule as order entry: 64 characters or fewer, missing gives 2010 unless a house identifier is configured for the member. On a close request it defaults to the contract's holder-side client and, when sent, must equal it (2010).
reg, reg.traderIdobjectonce registeredThe RTS 24/22 order-record fields, journaled verbatim. Once your member has registered Authorised Users, reg.traderId is mandatory here exactly as on order entry: missing or unregistered gives 2021, on create, quote and accept alike.
closeContractIdint64noCLOSE REQUEST (revision 3.44): the id of an OPEN barrier contract you HOLD on symbol, from GET /v1/barriers. The request then asks the contract's WRITER to quote a price at which the contract closes early. side, qty and barrier MUST be omitted (2013): the venue derives the closing side (a long holder SELLs, a short holder BUYs), the full contract quantity and the contract's terms. clientUid may be omitted (defaults to the contract's holder-side client) or must equal it (2010). Only the writer sees and may quote it, strictly inside the terms (2027); accepting closes the contract AT the quoted price with terminationReason EARLY_CLOSE, fees like the entry trade. 2035 when the id is not an open contract you hold on this market, or a close request for it is already open (one at a time). Omit for an ordinary request; see the barrier section below.
levelOriginstringnoBarrier OPENING requests only (revision 3.57, writer risk model): where the requested levels came from. CLIENT (the default) = the end client typed them; DERIVED = a routing system derived them from a pattern or template setup. Any other word, a close request or a non-barrier market: 2013. Frozen onto the contract at accept and shown to the WRITER beside capacity/clientTag; never read by any pricing or risk rule.
clientClassstringnoBarrier OPENING requests only (revision 3.57): the end client's MiFID II categorisation, RETAIL | PROFESSIONAL | ELIGIBLE. Only meaningful with a clientUid; a request WITH one and WITHOUT this field defaults to RETAIL. Any other word, a close request or a non-barrier market: 2013. Frozen onto the contract at accept and shown to the WRITER; never read by any pricing or risk rule.

The body is strict like order entry: an unrecognised field is rejected with 2017 naming the field. A member may hold at most 64 open RFQs at once (2012). Creation also requires a tradable market: 3003 halted, 3006 delisted, 3005 no mark price yet, and 3002 during any reduce-only session, because an RFQ opens new risk and there is no reduce-only RFQ.

A close request (revision 3.44) is the same route with the contract in place of the trade description:

{
  "symbol": "BTC-USD-KO",
  "closeContractId": 41,
  "expiresInMs": 60000,
  "clOrdId": "rfq-close-0001"
}
clOrdId is echoed, not deduplicated, on this route. Order entry collapses a same-reference retry onto the original order; RFQ creation does not: a blind resend opens a second, identical RFQ. After a timeout, reconcile with GET /v1/rfq/mine and withdraw the duplicate with the free cancel below rather than retrying blind.
{
  "code": 0, "msg": "ok",
  "data": {
    "rfqId": 3101, "symbol": "BTC-USD-KO", "kind": "BARRIER",
    "side": "BUY", "qty": "10",
    "expiryTs": 1785000060000, "createdTs": 1785000000000,
    "barrier": "60000", "takeProfit": "70000", "contractExpiryTs": 1785086400000,
    "strictTakeProfit": false, "premium": "45000", "premiumBps": 9000,
    "clOrdId": "rfq-req-0001", "clientUid": "CLIENT-A",
    "quotes": []
  },
  "asOfSeq": 104950, "ts": 1785000000001
}

expiryTs is the end of the quoting window. On a perp or spot RFQ market barrier, takeProfit, contractExpiryTs, strictTakeProfit, premium and premiumBps are absent, per the null-dropping serializer; takeProfit is also absent on a knock-out-only barrier request. On a barrier market (revision 3.45) strictTakeProfit echoes the flag, premiumBps is the premium share the request would open at (10000 - knockOutRebateBps, or strictTakeProfitPremiumBps on a strict request) and premium is the holder's maximum loss valued INDICATIVELY at the current mark, |mark - barrier| x qty x contract value x premiumBps / 10000: the entry is the quoted price, so the contract's premium is fixed only at accept (65000 mark, 60000 barrier, qty 10, 9000 bps in the example). A close request (revision 3.44) additionally carries closeContractId, with side the derived closing side, qty the full contract quantity, the terms the contract's own and premium / premiumBps / strictTakeProfit the contract's frozen values; the member is absent on an ordinary request.

The open board

GET/v1/rfq/open?symbol=

Every live RFQ you are eligible to quote, requester anonymous. The board is filtered per caller by Market Segments, and your own RFQs never appear on it: those live on /v1/rfq/mine. A close request (closeContractId set, revision 3.44) is on YOUR board only when you WROTE that contract: nobody else ever sees it, because nobody else may quote it. The optional symbol filter gives 2001 when unknown. data is an ARRAY.

{
  "code": 0, "msg": "ok",
  "data": [
    {
      "rfqId": 3101, "symbol": "BTC-USD-KO", "kind": "BARRIER",
      "side": "BUY", "qty": "10",
      "expiryTs": 1785000060000, "createdTs": 1785000000000,
      "barrier": "60000", "takeProfit": "70000", "contractExpiryTs": 1785086400000,
      "capacity": "FOR_CLIENT", "clientTag": "9f3a21bc00d14e77",
      "quoteCount": 2,
      "myQuote": { "quoteId": 8804, "price": "64950",
                   "expiryTs": 1785000042000, "clOrdId": "rfq-mk-77" }
    }
  ],
  "asOfSeq": 104960, "ts": 1785000010000
}

quoteCount is the number of live quotes on the RFQ; myQuote is present only when you have one, with your own reference. No requester identity, no requester reference and no other maker's quote is ever exposed here.

Writer transparency (revision 3.57). Every board row carries capacity: FOR_CLIENT when the request carries an end-client reference, OWN_ACCOUNT when it rides the requesting member's house account. On FOR_CLIENT rows clientTag is a 16-character lowercase hex pseudonym of that end client, STABLE FOR YOU (the same client of the same requester always tags the same toward you, across requests, contracts and reconnects) and UNLINKABLE ACROSS MEMBERS (your tag and any other member's tag for the same client never agree); the raw clientUid is never derivable from it and never crosses members. clientTag is null on OWN_ACCOUNT rows. The same pair rides your WRITER rows on GET /v1/barriers and the WS frames delivered to you as the writer; your own views as requester or holder never carry it. Since revision 3.57 (writer risk model) the same writer-visible surfaces additionally carry, on barrier markets, levelOrigin (CLIENT | DERIVED) and, on FOR_CLIENT rows only, clientClass (RETAIL | PROFESSIONAL | ELIGIBLE), both frozen onto the contract at accept; and a barrier quote may carry its own premiumBps (see the quote body below), which the requester sees before accepting and the accept freezes onto the contract.
Discovery is polled REST, deliberately. The board is eligibility-filtered per caller, so it cannot be broadcast as one public feed without leaking past Market Segments. Poll /v1/rfq/open at a sensible rate on your read bucket; the private WebSocket rfq channel pushes your own RFQs' lifecycle, your own quote confirmations and cancellations, and any accept you are party to (the recovery note below spells out who is told what).

Your RFQs and your quotes

EndpointReturns
GET /v1/rfq/mineArray of your open RFQs in the shape above, plus your clOrdId and clientUid, and quotes: the live quotes as {quoteId, price, expiryTs}. Makers are anonymous, so a quote carries no identity and no maker reference; pick by quoteId. A close request of yours carries closeContractId.
GET /v1/rfq/quotes/mineArray of your live quotes across all open RFQs: {quoteId, rfqId, symbol, price, expiryTs, clOrdId}, plus closeContractId on a quote against a close request (you wrote that contract). Your maker-side working state in one read.

Quote an RFQ

POST/v1/rfq/{rfqId}/quotes
{ "price": "64950", "expiresInMs": 30000, "clOrdId": "rfq-mk-77",
  "clientUid": "HOUSE", "reg": { "traderId": "TR-31-0C44D2AA" } }
FieldTypeRequiredMeaning and rules
pricedecimal stringyesYour quoted price, tick-aligned (2002). A quote outside the instrument's price band around mark is refused with 2003: the price is negotiated, but the band is a fair-and-orderly backstop. On a barrier RFQ the entry must sit on the correct side of the barrier level or 2027; a contract whose implied maximum loss, entry-to-barrier distance x qty x contract value, would exceed the venue's exact-money range is also 2027 (not reachable at any tradable size). On a CLOSE REQUEST (revision 3.44) only the contract's writer may quote (anyone else is answered 2024) and the price is the one the contract closes at: it must lie STRICTLY INSIDE the contract's terms relative to the holder's direction, barrier < price < takeProfit for a long holder and mirrored for a short one, else 2027.
expiresInMsint64noQuote validity in milliseconds, default 30000. Minimum 1 second, maximum 24 hours (2013). An expired quote vanishes silently; it cannot be accepted.
clOrdIdstringyesYour reference, 36 characters or fewer. Never shown to the requester.
clientUid, regyesExactly as on order entry: 2010 and 2021 apply.
premiumBpsintegernoBarrier OPENING requests only (revision 3.57, writer risk model): YOUR OWN premium share for this quote, integer basis points of the barrier distance. Allowed from the market's effective share for the request's variant (normal: 10000 - knockOutRebateBps; strict: strictTakeProfitPremiumBps) up to 10000, else 2044 PREMIUM_SHARE: you can only make a contract MORE expensive than the market base, never cheaper. On a close request or a non-barrier market: 2013. Omit for the market share. The requester sees your quote's premiumBps before accepting, and the accept freezes YOUR share onto the contract exactly where the market share is frozen: every premium/premiumBps figure of that contract then reads the frozen share.

One live quote per maker per RFQ. A re-quote replaces your earlier quote in place, under a NEW clOrdId since revision 3.85: a request or a quote whose clOrdId you already used (on an order, a swap take, a request or a quote) answers 2009 and opens or replaces nothing, with no restatement of the original (read GET /v1/rfq/mine or GET /v1/rfq/quotes/mine), from the venue's replay-contract line for that release, so a quote retried with the SAME clOrdId is safe and a stale quote can never come back. An RFQ holds at most 64 live quotes (2012). Quoting your own member's RFQ gives 2026. An RFQ that is unknown, closed, expired, or outside your eligibility set gives 2024; the cases are deliberately indistinguishable, so an id probe leaks nothing. The response echoes the quote:

{ "code": 0, "msg": "ok",
  "data": { "quoteId": 8804, "rfqId": 3101, "symbol": "BTC-USD-KO",
            "price": "64950", "expiryTs": 1785000042000, "clOrdId": "rfq-mk-77" },
  "asOfSeq": 104958, "ts": 1785000012000 }

Accept a quote

POST/v1/rfq/{rfqId}/accept
{ "quoteId": 8804, "reg": { "traderId": "TR-12-9F3A21BC" } }

Only the requester can accept, and acceptance is atomic: validation, margin on both parties and execution happen in one sequenced command, and the RFQ closes with the trade. What executes depends on the market kind:

KindOn accept
PERPAn off-book trade at the quoted price through the exact same position, margin, ledger and fee path as a book fill. You are the taker, the quoting member the maker, with ordinary maker and taker fees on the traded notional.
SPOTPrefunded delivery at the quoted price, exactly like a spot book fill. The funded check runs at accept: an unfunded requester gives 3001 (buy) or 3004 (sell), an unfunded maker 3007.
SWAPNot an accept of a quote at all but a TAKE of another member's standing offer (revision 3.51): the body names qty and no quoteId (2037 if one is sent), plus clOrdId / clientUid / reg like order entry. Your principal in the market's asset plus your taker fee move at once (3004 if you cannot pay them), a trade flagged SWAP prints with price = the rate in basis points, and the answer carries contractId, notional, rateBps, tenorDays, reward, maturityTs, asset, fee and the offer's remainingQty. See "Yield contracts" below.
BARRIEROpens a bilateral knock-out contract at the quoted entry price (next section). The response additionally carries contractId and echoes the contract's terms: barrier, takeProfit when set, contractExpiryTs when dated (revision 3.43), and since revision 3.45 the contract's strictTakeProfit, premium (the holder's maximum loss, frozen at the accepted entry) and premiumBps.
BARRIER, close requestCLOSES the named contract at the quoted price (revision 3.44): a closing trade flagged BARRIER_CLOSE with maker/taker fees exactly like the entry (you the taker, the writer the maker), then a BARRIERSETTLE zero-sum settlement with terminationReason EARLY_CLOSE. Margin admission (revision 3.47): closing releases the contract's margin, so a close that costs a party no more than it releases is always admitted, otherwise the party's available must stay non-negative after settlement and fee, else 3001 (holder) or 3007 (writer). The response carries contractId (the CLOSED contract), closeContractId, settlePrice (the quoted price) and your realizedPnl (clamped to [minus the premium, plus the potential profit] since revision 3.45), beside the trade fields, the contract's terms and its strictTakeProfit / premium / premiumBps. 2035 if the contract is no longer open or held, 2027 if the quote no longer lies inside its terms.

Rejections: 2024 when the RFQ is unknown, closed, expired or not yours; 2025 when the quote is unknown, withdrawn, expired, or its maker can no longer trade (including a maker moved to reduce-only after quoting); 2026 self-acceptance; 2027 when barrier terms fail re-validation against the executed entry (or, on a close request, the quote no longer lies strictly inside the contract's terms); 2035 when a close request's contract is no longer open or held; 3002 when your own member is reduce-only or otherwise barred from opening new risk. A quote that has drifted outside the instrument's price band against the CURRENT mark is stale and rejects with 2003: the venue will not execute a negotiated price far off the market it marks against, so re-request and accept a fresh quote. Margin is checked on both parties at accept: your failure gives 3001, the quoting member's failure gives 3007. Position limits likewise: yours 2011, the counterparty's 3007. A margin, limit or staleness rejection executes nothing and leaves the RFQ open with its quotes, so you can accept a different quote inside the window.

Barrier accept guard (revision 3.55; in force on every environment). On the accept of an OPENING barrier request, five additional checks run against the CURRENT mark and the request's own terms: the accepted entry must lie within the market's published barrierGuard.entryBandBps of the mark (GET /v1/instruments; 50 bps unless the market sets its own, revision 3.70) and the mark must be FRESH (younger than the market's staleness timeout, 60 s on the current barrier listings), else 2039 QUOTE_OFF_MARKET (also answered while the mark is stale or absent); a takeProfit the mark has already reached (BUY: at or above it, SELL: at or below) rejects 2040 TAKE_PROFIT_REACHED; a barrier the mark has already reached or crossed (BUY: at or below it, SELL: at or above) rejects 2043 BARRIER_REACHED, the loss-side mirror of 2040 (the contract would knock out on the first tick after opening, forfeiting the holder's premium); and the takeProfit and the barrier must each sit at least the market's barrierGuard.minTakeProfitBps / minBarrierBps of the entry (10 bps unless the market sets its own) away from the accepted entry (2041 / 2042; a distance exactly at the floor passes). Nothing trades on any of these, the request stays open and the quote stays open too. The listing value of the band (50 bps) and its calibration rule are published in the knock-out contract specification; on the sandbox EUR-USD-KO, GBP-USD-KO and JPY-USD-KO carry 20 bps and XAU-USD-KO, SP500-USD-KO and NDX100-USD-KO 30 bps from the date venue operations announce, and since revision 3.75 USDJPY-USD-KO 20 bps and US500-USD-KO and US100-USD-KO 30 bps. The EARLY-CLOSE accept is deliberately not banded against the mark: a fair close price is the contract's remaining value, not the spot. QUOTING MEMBERS: refresh or withdraw your barrier quotes when the mark moves; an unexpired quote can otherwise be accepted against you right up to these fences.

{ "code": 0, "msg": "ok",
  "data": { "rfqId": 3101, "quoteId": 8804, "tradeId": 55900,
            "symbol": "BTC-USD-KO", "side": "BUY", "price": "64950",
            "qty": "10", "contractId": 41,
            "barrier": "60000", "takeProfit": "70000", "contractExpiryTs": 1785086400000,
            "strictTakeProfit": false, "premium": "44550", "premiumBps": 9000,
            "ts": 1785000012345 },
  "asOfSeq": 104990, "ts": 1785000012346 }

The same accept on a close request (revision 3.44), the contract closing at the writer's 67000 quote:

{ "code": 0, "msg": "ok",
  "data": { "rfqId": 3140, "quoteId": 8931, "tradeId": 55977,
            "symbol": "BTC-USD-KO", "side": "SELL", "price": "67000",
            "qty": "10", "contractId": 41,
            "barrier": "60000", "takeProfit": "70000", "contractExpiryTs": 1785086400000,
            "closeContractId": 41, "settlePrice": "67000", "realizedPnl": "20500",
            "strictTakeProfit": false, "premium": "44550", "premiumBps": 9000,
            "ts": 1785003000000 },
  "asOfSeq": 105120, "ts": 1785003000001 }

Every accepted RFQ, a barrier entry included, publishes a trade on the public trades feed and in GET /v1/executions flagged RFQ (BARRIER_CLOSE for the closing trade of an accepted close request), carrying the clOrdId of your RFQ (as requester) or of your quote (as maker), so RFQ fills reconcile through the same pipeline as book fills. A bilaterally negotiated price is not public book liquidity, so an RFQ trade deliberately does not update the last-trade print, does not fire conditional orders and does not feed the circuit breaker.

Since revision 3.70 every PERP and BARRIER execution payload (the synchronous order answer's executions, GET /v1/executions, the WebSocket execution frame) and the RFQ accept answer carry uti, the EMIR unique transaction identifier of the VENUE leg: the venue's LEI followed by the tradeId zero-padded to 12 digits. It is derived at read time from the durable trade id, so the same fill reads the same uti on every surface and on every later read; a SPOT fill and a yield contract take never carry it, and it is OMITTED (never null) until the venue has configured its LEI.

An accept that times out is resolved by reading, never by resending blind. On success the RFQ vanishes from GET /v1/rfq/mine and the trade appears in /v1/executions (and, on a barrier market, the contract in /v1/barriers); a repeated accept of a filled RFQ answers 2024 and changes nothing.

Withdraw an RFQ or a quote

POST/v1/rfq/{rfqId}/cancel

The requester withdraws the whole RFQ; every live quote dies with it. Returns { "rfqId": 3101, "status": "CANCELED" }. 2024 when the RFQ is unknown, already closed, expired or not yours; the cases are deliberately indistinguishable.

POST/v1/rfq/{rfqId}/quotes/{quoteId}/cancel

The maker withdraws its live quote. Returns { "rfqId": 3101, "quoteId": 8804, "status": "CANCELED" }. 2024 when the RFQ is gone, 2025 when the quote is not yours or no longer live.

Like order cancels, neither route consumes a rate-limit token and neither is ever throttled: a member must always be able to withdraw a price. Both still require a TRADE credential (1005 on drop copy).

Barrier (knock-out) contracts

A barrier market is its own instrument with its own symbol and its own mark-price feed (for example BTC-USD-KO on the sandbox), served by the same /v1/mark-price route as every other market. The RFQ requester is always the holder and the quoting member always the writer. BUY makes the holder long with the knock-out barrier below the entry; SELL makes the holder short with the barrier above. The requester fixes barrier.level, the optional barrier.expiryTs and the optional barrier.takeProfit when opening the RFQ; makers quote the entry price.

Knock-out
The first time the mark price touches or crosses the barrier, the contract terminates at the barrier price. That holds even if the mark gaps straight through: it is a guaranteed stop with no gap risk on the holder side.
Take profit (revision 3.43)
The optional second exit. barrier.takeProfit sits on the profit side of the entry, above it for a long holder and below it for a short one, so the two levels always bracket the entry; it is checked against the current mark at create and against the quoted entry at quote and accept, strictly beyond it by at least one tick (2027 otherwise). The first time the mark reaches it (long: at or above, short: at or below) the contract terminates at the take-profit price, fee-free like a knock-out, and the holder banks |takeProfit - entry| x qty in contract value, the writer paying it. A gap through the level still settles at the level, guaranteed on both sides, and one tick can never cross both levels because they lie on opposite sides of the entry. Without a take profit the contract behaves exactly as before.
Expiry
With expiryTs set, the contract settles at the mark on the first price tick at or past that time, take profit or not: the price P&L floored at minus the premium and capped at the potential profit. On a STRICT take-profit contract (revision 3.45) the expiry without a touch settles at minus the premium whatever the mark. Without expiryTs the contract is open-ended and runs until knock-out, take profit, early close or tear-up, on a market that publishes no maxContractLifeHours; a market that publishes one refuses an omitted or later expiryTs with 2027 (revision 3.70).
Premium, knock-out rebate and strict take profit (revision 3.45)
The holder's maximum loss is its PREMIUM, |entry - barrier| x qty x contract value x premiumBps / 10000 (floor), where premiumBps is FROZEN on the contract at the open: 10000 - knockOutRebateBps on a normal contract (the market's knock-out REBATE from GET /v1/instruments; 1000 bps = 10 percent on every barrier market listed today, so the premium is 90 percent of the distance and the writer gives the rebate share back at a knock-out), the market's strictTakeProfitPremiumBps on a STRICT take-profit contract (5000 bps = 50 percent today), 10000 on every contract opened before the rebate existed. barrier.strictTakeProfit: true asks for the strict variant: it pays ONLY if the take profit is touched before the expiry, forfeits the whole premium at expiry otherwise, and needs a takeProfit, an expiryTs at most 24 hours ahead and a market whose strict share is nonzero (2027 otherwise, re-checked at accept). Every RFQ view, the accept response, the GET /v1/barriers rows and the WS barriers frames carry strictTakeProfit, premium and premiumBps.
Early close (revision 3.44)
The holder asks the writer to close the contract before any of the above: POST /v1/rfq with symbol and closeContractId (no side, qty or barrier: the venue derives the closing side, the full quantity and the terms; 2013 if sent, 2035 if the id is not an open contract you hold or already has a close request open). The request is on the writer's board only and only the writer may quote it (2024 to anyone else), at a price strictly inside the terms (2027). The holder's accept closes the contract AT that price: a BARRIER_CLOSE trade with maker/taker fees like the entry, a zero-sum BARRIERSETTLE settlement, terminationReason EARLY_CLOSE, no margin admission; the accept response carries contractId, closeContractId, settlePrice and your realizedPnl. One close request per contract at a time; a contract that terminates first cancels its open close request in the same command; cancelling one is free.
Holder margin
The premium at entry, IM equal to MM, and from then on the REMAINING distance from the mark to the barrier less the knock-out rebate (the premium plus the unrealized P&L, floored at 0); a take profit does not change it. The premium IS the maximum possible loss: no outcome of the contract, knock-out included, can cost the holder more. Because the requirement shrinks exactly as the unrealized loss grows, the contract never moves its holder toward maintenance on its own; a gain instead grows the requirement, so profit stays reserved until the contract closes.
Writer margin
Margined like a perp on the instrument's risk tiers at current notional, initial and maintenance, with mark-to-market P&L the exact negation of the holder's. When the contract carries a take profit the writer's maximum loss is bounded by the take-profit distance times quantity, so the requirement is the SMALLER of the tiered figure and the remaining mark-to-take-profit distance times quantity times contract value: a bounded contract never over-margins the writer. The knock-out rebate adds no writer requirement (revision 3.45): at a knock-out the writer still receives the premium, its worst case stays the take-profit side.
Liquidation tear-up
A member that enters liquidation has its barrier contracts, in both roles, torn up at the mark clamped to the contract's terms (never beyond the barrier or the take profit); the counterparty gets a fair close. The member's open RFQs are canceled and its outstanding quotes withdrawn at the same moment. If the venue delists a barrier market, remaining contracts are closed out at an operator-determined settlement price.
Funding and fees
No funding on barrier markets, ever. Entry pays ordinary maker/taker fees on the entry notional (holder taker, writer maker), and an early close pays them again on the closing notional (same roles); knock-out, take-profit and expiry settlement are fee-free. Settlement P&L books in the ledger as kind BARRIERSETTLE, zero-sum between holder and writer.

Barrier contracts are bilateral. They never appear in /v1/positions, never contribute to open interest and never create end-client sub-positions; the holder's and writer's clientUid attributions ride the contract itself.

Your barrier contracts

GET/v1/barriers?status=&limit=

Your contracts in both roles. With status omitted or OPEN: the live contracts, marked to the live mark, exactly as before revision 3.43. status=CLOSED: your recently settled contracts, newest first, kept since the last venue start (limit default 100, maximum 1000; the durable record is your statement and the ledger). status=ALL: both, live rows first. Any other value gives 2013. data is an ARRAY. Since revision 3.57 your WRITER rows (live and closed) additionally carry capacity and, on FOR_CLIENT contracts, clientTag, your per-viewer pseudonym of the HOLDER's end client (writer transparency, above); holder rows never carry the pair.

{
  "code": 0, "msg": "ok",
  "data": [
    {
      "contractId": 41, "symbol": "BTC-USD-KO",
      "role": "HOLDER", "direction": "LONG", "qty": "10",
      "entry": "64950", "barrier": "60000", "takeProfit": "70000",
      "strictTakeProfit": false, "premium": "44550", "premiumBps": 9000, "status": "OPEN",
      "markPrice": "65400",
      "barrierDistance": "0.082568", "unrealizedPnl": "4500",
      "margin": "49050", "expiryTs": 1785086400000,
      "clientUid": "CLIENT-A", "openedTs": 1785000012345
    }
  ],
  "asOfSeq": 105010, "ts": 1785000020000
}

A closed row (?status=CLOSED or ALL) drops the mark-derived members and carries the settlement instead:

{
  "contractId": 41, "symbol": "BTC-USD-KO",
  "role": "HOLDER", "direction": "LONG", "qty": "10",
  "entry": "64950", "barrier": "60000", "takeProfit": "70000",
  "strictTakeProfit": false, "premium": "44550", "premiumBps": 9000, "status": "CLOSED",
  "expiryTs": 1785086400000, "clientUid": "CLIENT-A", "openedTs": 1785000012345,
  "settlePrice": "70000", "realizedPnl": "50500",
  "terminationReason": "TAKE_PROFIT", "closedTs": 1785003600000
}
FieldNotes
roleHOLDER | WRITER.
directionThe HOLDER's direction, LONG (knocked out at or below the barrier) or SHORT (knocked out at or above). As WRITER your exposure is the opposite side.
takeProfitThe optional take-profit level (revision 3.43); absent on a knock-out-only contract.
strictTakeProfitSTRICT take-profit contract (revision 3.45): pays only if the take profit is touched before expiryTs; the expiry without a touch settles at minus the premium whatever the mark.
premiumThe holder's PREMIUM (revision 3.45), its maximum loss: |entry - barrier| x qty x contract value x premiumBps / 10000, floored to six decimals, frozen at the open (64950 - 60000 = 4950 x 10 x 0.9 = 44550 in the example).
premiumBpsThe frozen premium share of the barrier distance in basis points (revision 3.45): 10000 = the full distance (contracts opened before the knock-out rebate existed), 10000 - knockOutRebateBps on a normal contract, strictTakeProfitPremiumBps on a strict one.
statusOPEN (live) | CLOSED (settled).
barrierDistanceLive rows only. Distance from the current mark to the barrier as a decimal fraction of the mark ("0.082568" means 8.2568 percent away), floored to six decimals; "0" at the barrier.
unrealizedPnlLive rows only. Your P&L on the contract at mark, role-signed: the holder's P&L as HOLDER (floored at minus the premium), its exact negation as WRITER.
marginLive rows only. Your margin against this contract: as HOLDER the most you can still lose, the remaining mark-to-barrier distance less the knock-out rebate, i.e. the premium plus your unrealized P&L floored at 0 (65400 - 60000 = 5400 x 10 - 4950 = 49050 in the example), as WRITER the tier margin at current notional, bounded by the remaining mark-to-take-profit distance when the contract carries a take profit.
expiryTsAbsent on an open-ended contract.
settlePriceClosed rows only. The barrier on a knock-out, the take-profit level on a take profit, the writer's accepted quote on an early close, the terms-clamped mark on expiry, tear-up and final settlement.
realizedPnlClosed rows only. Your role-signed settlement P&L, the exact BARRIERSETTLE ledger amount.
terminationReasonClosed rows only. KNOCK_OUT | TAKE_PROFIT | EXPIRY | TEAR_UP (a party entered liquidation) | FINAL_SETTLEMENT (operator close-out of a delisted market) | EARLY_CLOSE (revision 3.44: the holder's close request accepted at the writer's quoted price). On a strict contract an EXPIRY row settles at minus the premium (revision 3.45).
closedTsClosed rows only. Settlement time, Unix milliseconds.

Yield contracts: offers, takes and your contracts (revision 3.51)

A SWAP market (USD-TBILL-SWAP, EUR-TBILL-SWAP, BTC-STAKE-SWAP, ETH-STAKE-SWAP, SOL-STAKE-SWAP, USD1-STAKE-SWAP on the sandbox) trades a bilateral yield contract in ONE asset, the market's baseAsset: a writer posts an offer, a taker hands over a size of the asset, and principal plus a reward frozen at the take come back at maturity. There is no price, no mark and no margin on a swap market; GET /v1/instruments publishes swapMinTenorDays, swapMaxTenorDays, swapMaxRateBps, baseAsset, qtyStep, minNotional (the minimum single take, in the asset), maxOrderQty (one offer, one take) and maxPositionQty (your open swap size on the market in both roles). The economics, the default and claim mechanics and the disclosure your clients must see are in the member reference section 6h and the contract specification sheet; this page is the route surface.

POST/v1/rfq (swap market: post an OFFER)
{ "symbol": "BTC-STAKE-SWAP", "qty": "0.5", "rateBps": 450, "tenorDays": 30,
  "minQty": "0.01", "expiresInMs": 86400000, "clOrdId": "offer-0001", "clientUid": "HOUSE" }

rateBps is the annualised rate you pay in whole basis points, tenorDays the tenor of every contract taken from the offer, qty the size on offer, minQty the smallest single take you accept (default and floor: the market's minNotional), expiresInMs the validity of the standing offer (default 24 h, 60 s to 30 days). NO side, barrier or closeContractId (2013). Terms outside the market reject 2036; a writer with an outstanding claim rejects 3008. The answer is the RFQ view with kind SWAP, rateBps, tenorDays, minQty, remainingQty, asset, expiryTs and no quotes member.

GET/v1/rfq/open?symbol=BTC-STAKE-SWAP

The OFFERS BOARD: every live offer you may take, writers anonymous, your own offers excluded (they sit on GET /v1/rfq/mine with their live remainingQty), Market Segments applied, sorted rateBps descending then rfqId. Rows carry rfqId, symbol, kind, side BUY (the writer receives the asset), asset, rateBps, tenorDays, qty, remainingQty, minQty, expiryTs, createdTs. Polled, never pushed.

POST/v1/rfq/{rfqId}/accept (swap offer: TAKE a size)
{ "qty": "0.25", "clOrdId": "take-0001", "clientUid": "CLIENT-A" }
{ "code": 0, "msg": "ok",
  "data": { "rfqId": 5102, "quoteId": 0, "tradeId": 88240, "symbol": "BTC-STAKE-SWAP", "side": "SELL",
            "price": "450", "qty": "0.25", "contractId": 17, "notional": "0.25", "rateBps": 450,
            "tenorDays": 30, "reward": "0.000924", "maturityTs": 1789603200000, "asset": "BTC",
            "fee": "0.00005", "remainingQty": "0.25", "ts": 1787011200000 } }

qty must be at most the offer's remainingQty and your maxOrderQty, and at least max(minQty, minNotional) unless it is the whole remainder (2038 otherwise); no quoteId (2037); not your own offer (2026); you must be able to pay qty plus your taker fee in the asset (3004); a writer no longer fit answers 3007 and the offer is withdrawn (a writer at its caps answers 3007 and the offer stays); an offer whose writer is outside your Market Segments answers 2024 before every other check, whatever the body says. reward is floor(notional x rateBps x tenorDays / 3,650,000) in the asset, frozen; maturityTs is the take time plus tenorDays x 24 h. Withdrawing the untaken remainder is POST /v1/rfq/{rfqId}/cancel, the writer's, never rate-limited.

GET/v1/swaps?status=&limit=

Your swap contracts, both roles. status omitted or OPEN: the live ones (status OPEN, or DEFAULTED while a claim is outstanding); CLOSED: your recently settled contracts (newest first, limit default 100, maximum 1000, kept since the last venue start; the durable record is your ledger and the swaps event stream); ALL: both, live first.

{ "code": 0, "msg": "ok",
  "data": [ { "contractId": 17, "symbol": "BTC-STAKE-SWAP", "asset": "BTC", "role": "TAKER",
              "qty": "0.25", "notional": "0.25", "rateBps": 450, "tenorDays": 30, "reward": "0.000924",
              "status": "OPEN", "paid": "0", "outstanding": "0",
              "openedTs": 1787011200000, "maturityTs": 1789603200000,
              "rfqId": 5102, "tradeId": 88240, "clientUid": "CLIENT-A" } ] }
FieldMeaning
roleWRITER (you received the principal and owe it back with the reward) or TAKER (you delivered it). The counterparty is never named.
statusOPEN, DEFAULTED (settled short at maturity, outstanding above 0, the writer blocked from new offers until it is paid), MATURED, CURED (the claim paid to zero later), FINAL_SETTLEMENT (the market was removed from trading and the contract paid at principal plus the pro rata reward).
paid, outstandingCumulative asset the writer has paid, and the claim still owed (0 unless DEFAULTED). Every settlement moves through SWAPSETTLE wallet entries; the take moved SWAPPRINCIPAL and FEE.
closedTsClosed rows only.
Lifecycle pushes and recovery. The private WebSocket channels rfq and barriers push lifecycle changes with guaranteed delivery. On rfq, the requester receives created, quoted, accepted, canceled and expired for its own RFQs; a maker receives myQuote and quoteCanceled for its own quotes, and accepted only when its quote is the one accepted; the writer of a contract additionally receives the created frame of a close request on it (revision 3.44, with role WRITER and closeContractId; since revision 3.56 also capacity and clientTag, writer transparency), the one push beyond the requester, because it is the only member that may quote it. A quote whose RFQ closes any other way, canceled, expired or won by a different maker, ends silently on the maker side: poll the board rather than waiting for a push that will not come. The barriers channel (types opened, knockOut, takeProfit, expiry, earlyClose, liquidation, finalSettlement; every frame carries strictTakeProfit, premium and premiumBps since revision 3.45, closes carry terminationReason in the same vocabulary as GET /v1/barriers) reaches holder and writer alike. Recovery is state-only: neither channel is one of the cursor-backfillable families on /v1/stream/events, so after a socket gap re-read GET /v1/rfq/mine, GET /v1/rfq/open and GET /v1/barriers and continue from live rather than attempting a backfill. Quotes expire silently inside their RFQ; the RFQ itself emits expired at the end of its window. Schemas are on the WebSocket page.

The codes introduced with RFQ, with their exact wire wording:

CodeHTTPmsg
2022400RFQ market: no order book, trade via POST /v1/rfq
2023400not an RFQ market: use the order book
2024404RFQ not found, closed or expired
2025404quote not found, withdrawn or expired
2026400cannot quote or accept against your own member
2027400barrier level or expiry invalid for this market and side (also a strict take-profit request without a take profit, without an expiry, with an expiry beyond 24 h, or on a market that does not offer it, revision 3.45)
2035400closeContractId does not name an OPEN barrier contract you hold on this market, or a close request for it is already open
3007400quoting member no longer passes margin or limits (on a swap take: the writer is no longer fit and the offer is withdrawn, or the writer is at its caps and the offer stays)
2036400swap offer terms outside the market (rate, tenor, size, minimum take, validity or the exact-money ceiling); revision 3.51
2037400swap markets have offers and takes, never quotes (a quote on a swap offer, or a swap accept with a quoteId)
2038400swap take size not admissible (above the remainder, above your per-order cap, or below the minimum take without being the remainder; a zero or off-grid qty is 2002)
3008400swap writer blocked: a contract you wrote carries an outstanding claim
3009400market access (revision 3.61): no new risk on a market that requires admission you do not hold, or where your access is DENIED; reduce-only flow stays admitted (GET /v1/account/parameters markets[].access)
3010400DEA client blocked (revision 3.62): the clientUid or reg.traderId of this reg.dea order is an identifier venue operations stopped on your account (a Rules measure with a ground and a case reference, notified by mail with the section 36 appeal route); unmarked flow and your other DEA clients are unaffected; on REST, the WS ack and the FIX report alike
3011400DEA flow limit (revision 3.62): the day's allowance of reg.dea orders (count or notional) your DEA class publishes is reached; only reg.dea orders count; resets at 00:00 UTC
3014400funding catch-up (revision 3.70): this GRID perpetual's funding settlement is more than 60 s overdue and the venue is settling the missed anchors; opening orders, non-reduce-only amends and RFQ accepts are refused until the catch-up lands, reduce-only flow stays admitted, and a conditional order whose trigger fires inside the window is canceled FUNDING_CATCH_UP; uniform per market, never per member, normally seconds. Retry after the next FUNDING_SETTLED event or once GET /v1/funding/current nextFundingTs lies in the future (Trading page, the funding catch-up fence)

As everywhere on the API: log msg, branch on code. All twelve join the full registry on the Reference page.

DLT Market: tokenised securities on the Kronos X chain

Since Member API revision 3.66 (2026-09-10). PM MTF Ltd operates a DLT trading and settlement system under Regulation (EU) 2022/858. A DLT financial instrument (a share, a bond, a money-market instrument or a fund unit) is a market with kind: SECURITY on GET /v1/instruments; its baseAsset is the ISIN and equals isin. It trades exactly like a SPOT market (fully prefunded, no positions, no margin, no funding, no reduceOnly, unitScale above 1 for fractional shares) and settles on the Kronos X chain, the venue's permissioned ledger.

Every fill creates a settlement obligation. The venue folds it into a DvpSettle transaction from the seller's client wallet to the buyer's client wallet; validators re-execute the block and sign it; the block is final when two thirds of the validator set including one validator not operated by the venue have signed, typically within two seconds. Entry and irrevocability are the acknowledged fill; finality is the attested block. Your engine balance in the ISIN is the mirror of what the chain holds in trading for you.

RoutePurpose
GET /v1/chain/walletsYour wallets: one custody wallet per client clientUid you traded or registered, your house wallet, and the external wallets you sponsor, with holdings per ISIN (free, locked = in trading). READ_ONLY may call it.
POST /v1/chain/wallets/custody{ clientUid, professional }: register a client's custody wallet ahead of its first trade.
POST /v1/chain/transfers{ clientUid, isin, to, units }: move units out of one of your custody wallets to any wallet. Your engine balance is debited first (a debit above your free balance in the ISIN is refused with 3001 INSUFFICIENT_MARGIN, the engine's own code, and nothing moves); a chain refusal re-credits you and answers 5003 with chain.reason. Since revision 3.70 every refusal of the operation is 5003 with chain (state Rejected, reason): an unknown isin, a clientUid you hold no custody wallet for, an unknown destination wallet or non-positive units, each refused before any debit; the source wallet is derived from your credential and the clientUid, never from a field.
POST /v1/chain/unlock{ wallet, isin, units }: return units of an external wallet you sponsor from trading to its free balance; engine debited first (3001 above your free balance); a wallet you do not sponsor, an unknown isin or non-positive units answers 5003 with chain.reason.
POST /v1/chain/instructionsAn instruction signed by an external wallet's own P-256 key (WalletRegister, Lock, Transfer): the transaction fields plus signerKey and signature (DER ECDSA P-256 over SHA-256 of "KXC:instr:" and the canonical body; the complete language-neutral encoding, field order, widths, byte order, string and hash encodings, enumeration values and the transaction hash, is "Signing an instruction: the canonical encoding" in section 6i of the Member API reference, revision 3.69). 200 with the status; poll GET /chain/v1/instructions/{hash}.
GET /v1/chain/settlementsYour settlement obligations: state Pending, Settled or Failed, block height, transaction hash, houseCoveredUnits when your house wallet covered a client's shortfall.

The public record needs no authentication: GET /chain/v1/head, /chain/v1/blocks, /chain/v1/blocks/{height}, /chain/v1/tx/{hash} (with a Merkle inclusion proof), /chain/v1/tokens, /chain/v1/tokens/{isin} (with the integrity of issue), /chain/v1/wallets/{id}, /chain/v1/wallets/{id}/proof?isin=, /chain/v1/validators, /chain/v1/integrity, /chain/v1/settlements; the explorer at https://explorer.perpetuals.com renders the same data. Wallets are key hashes; no member or client identity is on the public record.

Since revision 3.68 the public record also serves GET /chain/v1/valuation, the Article 3 figures of Regulation (EU) 2022/858 (the explorer's Valuation page): status (OK, WATCH, ADMISSION_CAP, TRANSITION), aggregateEur, the thresholds (EUR 500 million issuer market capitalisation per share, EUR 1 billion issue size per bond or money-market instrument, EUR 500 million assets under management per fund unit, EUR 6 billion aggregate admission cap, EUR 9 billion transition figure), the ECB USD per EUR rate in use, and per instrument the circulating units, the price with its source (AUCTION, the closing or opening auction price, else LAST_TRADE, else MARK), the market value in EUR, the declared basis and the class cap. The venue admits a new instrument only with a recorded declaration inside its class cap and with room under the aggregate cap; nothing in this changes your order entry.

Since revision 3.70 the same record publishes monthlyAverage, the Article 3(4) monthly average aggregate market value of the last complete calendar month (month, days, aggregateEur, status, perIsin[]), and monthlyAverageRule. The arithmetic is the Regulation's own and there is exactly one: per instrument the mean of its daily EUR closing prices over the days of the month it was priced times the units recorded on its last day (an instrument unpriced all month at its declared basis), perIsin[].averageEur being that contribution and aggregateEur their sum, the same figure the admission refusal projects from. The Regulation applies that average to admissions in the following month and to the transition decision: the venue refuses a new instrument when either the live aggregate at the moment of admission or the monthly average would reach the EUR 6 billion cap, and decides the transition strategy on the monthly average, a single day above EUR 9 billion being a warning and not an activation. The figure is frozen once its month closes; while fewer than one complete month is recorded the field is null and the rule sentence says so.

While the validator quorum is not reached (an unattested tail) trading continues and fills stay irrevocable; only movements out of trading wallets wait, answered 3012 LEDGER_FINALITY_PENDING (retry when GET /chain/v1/head shows unattestedTail: false). 5006 LEDGER_UNAVAILABLE is answered only when the chain host runs without its engine bridge, a venue-side fault: retry after a minute, then contact venue operations. A venue that does not run the Kronos X chain does not serve the /v1/chain/* and /chain/v1/* routes at all (HTTP 404).

Call auctions on SECURITY markets

Since Member API revision 3.67 (2026-09-10). A SECURITY market may carry an auction schedule: one opening and one closing call per calendar day at local times in the market's zone. GET /v1/instruments publishes it as auction (schedule, zone, openCallStart, openUncross, closeCallStart, closeUncross) with the live tradingPhase and, after the first uncross that traded, lastAuctionPrice and lastAuctionTs. A SECURITY market without the block trades continuously exactly as before.

tradingPhaseMeaningOrder entry
CLOSEDBefore the opening call and after the closing uncrossGTC and GTD LIMIT orders rest for the next call; cancel and amend work; IOC, FOK, MARKET and conditional orders reject 2046
CALL_OPENThe opening call: orders accumulate, nothing matchesas CLOSED
CONTINUOUSAfter the opening uncross until the closing callevery order type as today
CALL_CLOSEThe closing callas CLOSED

At the scheduled time the venue computes one price and executes every crossing order at it: the price with the largest executable volume, ties broken by the smallest surplus, then by closeness to the reference (the last trade, which an auction print also updates, else the mark), then the lower price; a candidate outside the market's priceBandBps around the reference is not eligible. Orders pair in price and time priority; every print carries flags: "AUCTION" and both sides pay the maker fee rate. If no price trades, the call ends with an empty uncross and the phase advances; resting orders stay. Two rules since revision 3.69: a due uncross on a halted market is deferred, the phase stays in the call and the uncross runs when the halt lifts; and when a call ends with orders still crossing (the collar excluded every candidate, a same-member or ineligible pair), the entry into continuous trading replays those orders in time priority through ordinary matching, so the earlier order rests and the later one takes at the resting price with the ordinary fees and no AUCTION flag. The public status.{symbol} channel carries { tradingPhase, nextPhaseTs } on each phase change and { auction: { price, qty, surplusQty, surplusSide } } on each uncross. The closing auction price is the DLT Market's daily valuation.

Client identity for transaction reporting (RTS 22)

PM MTF Ltd, as the operator of this MTF, owes a transaction report under MiFIR Article 26(5) for every transaction executed through its systems by a member that is not itself a MiFID II investment firm. Where such a member's trade carries client flow, the venue's report must identify the underlying client per RTS 22 Annex I Table 2. This surface is how members supply that identification, keyed by the same clientUid they already stamp on every order. Members that ARE MiFID II investment firms report their own transactions: the venue does not report for them and nothing here applies to them. A market maker that is in scope can register its own LEI against its house clientUid (the configured house identifier applied when it omits clientUid on order entry) so own-account sides resolve too.

Trading is never blocked on identity data. Order entry does not read this store, and no order, quote or RFQ acceptance is ever refused because a clientUid has no identity record. The coupling is temporal instead: the transaction report is due by close of the FOLLOWING WORKING DAY, and the missing report below is the tool for chasing the backlog inside that window.

All three routes are signed like every private route and metered on the read bucket (the PUT is reference data, not order entry, so it never consumes an order-budget unit). They are served from the venue's history store: a deployment without it answers 5004 with HTTP 503 on all three. A path clientUid that begins or ends with whitespace rejects with 2013 on the PUT and on the GET since revision 3.72. The identity store sits outside the engine sequence, so the envelope asOfSeq is 0 on the PUT and the single-record GET; the missing report's asOfSeq is the projection watermark instead.

PUT/v1/clients/{clientUid}/identity

Create or update the record for one clientUid (the identifier exactly as you stamp it on orders, 1 to 64 characters). An upsert: the same route is the update mechanism, and every PUT both replaces the current record and appends an immutable audit revision, so a corrected report can show what was on file at any earlier revision. Requires a trading-capable credential; a READ_ONLY (drop copy) key is refused with 1005. The body is STRICT like order entry: an unknown JSON field rejects with 2017, never a silent discard.

{
  "type": "NATURAL_PERSON",
  "countryCode": "DE",
  "identifierPriority": 2,
  "nationalIdType": "PASSPORT",
  "nationalId": "C01X00T47",
  "firstName": "Jürgen",
  "surname": "Müller",
  "dateOfBirth": "19840217"
}
FieldRules
typeNATURAL_PERSON | LEGAL_ENTITY. Required.
countryCodeISO 3166-1 alpha-2, uppercase. Required for natural persons (the nationality that selects the Annex II identifier priority); optional for legal entities.
identifierPriorityOptional integer 1 to 3: the Annex II priority rank the supplied national identifier holds for that country. Since revision 3.53 it defaults to the table rank and must agree with it when sent.
nationalIdTypeYour scheme label for the national identifier, 1 to 32 chars (for example NATIONAL_ID, PASSPORT, TAX_ID). Required when nationalId is set, and only then.
nationalIdThe national identifier itself, 1 to 64 chars. Natural persons only.
firstName, surname1 to 140 chars each. Required for natural persons. Submit the name as you hold it in Latin script; the venue removes titles and listed surname prefixes itself when it derives the CONCAT code (revision 3.49).
dateOfBirthYYYYMMDD. Required for natural persons.
leiISO 17442 LEI, checksum validated. Required for legal entities, and only they may carry it.

For a natural person who holds more than one nationality, choose countryCode as RTS 22 Article 6(3) and the ESMA transaction reporting guidelines (ESMA/2016/1452, section 5.5.1) direct: an EEA nationality over a non-EEA one, and among several nationalities of the same kind the ISO 3166-1 alpha-2 code that sorts first (a German and United States national is DE, a Brazilian and United States national is BR, a British and German national is DE because the United Kingdom is outside the EEA); the venue evaluates the identifier against that country's row and never re-derives the choice from data it does not hold.

Cross-field rules: lei only on LEGAL_ENTITY; dateOfBirth, nationalId and nationalIdType only on NATURAL_PERSON. Any validation failure rejects with 2030 and a msg naming the offending field. The response echoes the stored record:

{
  "code": 0, "msg": "ok",
  "data": {
    "clientUid": "CLIENT-A", "type": "NATURAL_PERSON",
    "countryCode": "DE", "identifierPriority": 2,
    "nationalIdType": "PASSPORT", "nationalId": "C01X00T47",
    "firstName": "Jürgen", "surname": "Müller", "dateOfBirth": "19840217",
    "lei": null,
    "mifirId": "C01X00T47", "mifirIdSource": "NATIONAL_ID",
    "revision": 1, "updatedAt": 1785000000123
  },
  "asOfSeq": 0, "ts": 1785000000123
}
Revision 3.53 (18 August 2026): the Annex II identifier table, mifirIdScheme, warnings, and trader identities. For the legs the venue reports, RTS 22 Annex II binds the venue, so a natural person's identifier choice is now checked against the per-country priority table (printed in full in the Member API reference, section 6d): nationalIdType must be a label the country designates (NATIONAL_ID stays accepted as the generic alias of the country's first national number where the row lists one, not for AT, CY, DE, FR, HU, IE, LU or an unlisted country, PASSPORT where the country lists the passport, TAX_ID where it lists a tax number, plus per-country labels such as PESEL, FISCAL_CODE, NATIONAL_INSURANCE), identifierPriority defaults to the table rank and must agree with it when sent, checksummed identifiers (IT, ES, PT, PL, EE, LT) are verified, and CONCAT is accepted only where the Annex lists it (not for EE, ES, IS, IT, MT, PL). A lower-priority choice draws a warning; a contradiction is, in the sandbox WARN stage until 2026-09-21, stored as submitted and reported in a warnings array on the PUT echo (omitted when empty and on GET), and rejects with 2030 in the ENFORCE stage, in force on production from the venue's first day and on the sandbox from Tuesday 2026-09-22. The echo and GET carry mifirIdScheme, the ISO 20022 scheme the identifier is reported under: LEI, NIDN (national identification number), CCPT (passport) or CONCAT; empty on pre-3.53 records until the venue's re-validation fills it. The GB row applies to UK nationals as the Annex prints it (NATIONAL_INSURANCE, then CONCAT; revision 3.70). A sibling route PUT / GET /v1/trader-ids/{traderId}/identity files the natural person (the same NATURAL_PERSON rules plus a mandatory branchCountry) or algorithm (kind: "ALGO", algoId) behind a Trader ID, so RTS 22 fields 57 to 60 resolve reg.traderId, reg.executionDecisionId and reg.investmentDecisionId to a person under the Annex II scheme; unresolved codes are reported as sent, which is correct for MiFID II firm members. In the venue's RTS 22 transaction reports a code that resolves to a person is reported as that person (identifier under its scheme, with the branch country in fields 58 and 60), an algorithm as its code, an unresolved code as an algorithm code (up to 50 characters); an execution code left out on an AOTC or MTCH order is reported as the client's decision (NORE), and a DEAL order must carry BOTH reg.investmentDecisionId and reg.executionDecisionId (or reg.traderId for the execution): RTS 22 field 57 is mandatory when dealing on own account (ESMA CON-570) and no client exists to attribute the execution to, so a DEAL leg without them cannot be filed and becomes a chase item.

mifirId is the identifier the venue's transaction report will carry for this client, resolved at submission time and stored resolved, so what the GET shows is exactly what will be reported. mifirIdSource says how it was chosen: LEI (legal entities), NATIONAL_ID (a natural person with a designated national identifier), or CONCAT. When a natural person has no nationalId, the venue derives the RTS 22 Annex II fallback identifier exactly as the ESMA transaction reporting guidelines (ESMA/2016/1452, section 5.5.1) prescribe: countryCode + dateOfBirth + the first five letters of the first name + the first five of the surname, each name part padded to five with #. The venue applies the four ESMA steps itself: titles are removed from both fields (Dr, Prof, Mr, Mrs, Ms, Sir, Madame and the rest of the ESMA list, case-insensitive, with or without a trailing dot); the listed surname prefixes are removed when they stand as separate leading words (van, van der, von, de, de la, di, da, dos, du, mac, mc, o, ó, ní and the rest of the list, longest match first; a prefix written into the name is kept, so McDonald, O'Brian and Vandenberg keep it; a listed prefix or title that is the whole field is the name and stays, Le derives LE###), and only the FIRST first name is used (Jon Ian derives JON##; a hyphenated first name is one name, Amy-Ally derives AMYAL); then the ESMA transliteration table is applied character by character and uppercased: every character in the table folds to its listed Latin letter (Ø to O, ß to S, Æ to A, Þ to T, Œ to O), and every character outside the table, apostrophes, hyphens, spaces, digits and letters of other scripts included, is removed. The body above submitted without nationalId would store "mifirId": "DE19840217JURGEMULLE" (Jürgen folds to JURGE, Müller to MULLE). A name with no letter the table can fold (a name written only in Greek or Cyrillic, for example) is refused with 2030: resubmit the names in their transliterated Latin form, or supply nationalId and nationalIdType. Identifiers stored under the earlier derivation are re-derived by the venue under a new audit revision.

GET/v1/clients/{clientUid}/identity

The current record, in exactly the shape above; a clientUid with no record answers 2031 (HTTP 404). A READ_ONLY credential may call it, so a back-office reporting process can verify coverage without being able to trade.

PUT/v1/trader-ids/{traderId}/identity
GET/v1/trader-ids/{traderId}/identity

The identity behind one Trader ID (revision 3.53; RTS 22 fields 57 to 60, the Member API reference section 6d): every PUT appends an immutable audit revision (immutable until the retention floor), needs a trading-capable credential (1005 otherwise), and the path id is 1 to 64 characters of A-Z a-z 0-9 . _ -; it need not be a registered Authorised User, because decision codes may name persons who never enter orders. The GET answers the current record, or 2031 (HTTP 404) when none is on file or after the venue pseudonymised it.

GET/v1/clients/identity/missing?from=&to=&limit=

The chase list for the reporting window: every distinct clientUid of your member that traded on MiFIR-scope markets (the perpetual, the barrier since revision 3.52 and the swap markets from the date venue operations announce under revision 3.70: barrier contracts and yield contracts are derivatives traded on the MTF and their trades are transaction-reportable) inside the window and has no identity record on file. Spot activity does not appear. A member whose licensing profile records it as a MiFID II investment firm always receives an empty clients array (revision 3.70): the venue reports nothing for it and keeps no chase list for it. The identity record serves the venue's own regulatory reporting only, its MiFIR Article 26(5) transaction reports and, where a surveillance alert concerns the client, the suspicious transaction and order report (STOR) the venue files with CySEC; it is never read by matching, margin or any risk path and is disclosed to no other member.

ParamRules
fromUnix ms, inclusive. Default: to minus 48 hours. Pass it yourself to sweep further back, for example on the working day after a weekend.
toUnix ms, exclusive. Default: now.
limit1 to 1000, default 500.
{
  "code": 0, "msg": "ok",
  "data": {
    "from": 1784913600000,
    "to": 1785000000000,
    "clients": [
      { "clientUid": "CLIENT-B", "tradeSides": 7,
        "firstTradeTs": 1784920000123, "lastTradeTs": 1784999911456 }
    ]
  },
  "asOfSeq": 104870, "ts": 1785000000456
}

Rows are ordered oldest first trade first, so the entries closest to the deadline come first. tradeSides counts your trade sides carrying that identifier inside the window; firstTradeTs and lastTradeTs bound them. An empty clients array means every reportable side in the window is covered. The envelope asOfSeq is the projection watermark: the report sees trades only up to that sequence, so a trade acked a moment ago may not be visible in it yet. For an end-of-day sweep this does not matter; for a trade-then-poll integration test it does.

The codes introduced with this surface, with their exact wire wording (the 2030 msg is usually more specific and names the offending field):

CodeHTTPmsg
2030400client identity payload invalid
2031404no identity record on file for this clientUid

The record exists solely to populate buyer and seller identification in the transaction reports the venue owes; it is never read by matching, margin or any risk path, and it is disclosed only through the venue's own regulatory reporting, never to another member. REST only: there is no WebSocket op and no FIX surface for identity data.

Account, positions and margin

GET/v1/account
data is an ARRAY, and the maintenance-margin key is uppercase. One row per settlement currency. Read data[0].balance, never data.balance, and read data[0].MM, not mm, while every neighbouring key is camelCase. Both are frozen for deployed clients. A third trap: assets is attached to the first settlement-currency row only, because base-asset holdings belong to the member and not to any one currency account. Do not sum assets across rows.
{
  "code": 0, "msg": "ok",
  "data": [
    {
      "ccy": "USD", "balance": "1250000", "equity": "1261430.5",
      "unrealizedPnl": "11430.5",
      "positionIM": "184000", "orderMargin": "22500", "MM": "92000",
      "available": "1054930.5", "spotReserved": "0",
      "marginRatio": "0.0729", "riskStatus": "NORMAL", "imMultiplier": "1",
      "feeTier": "TIER_2", "makerFeeRate": "0", "takerFeeRate": "0.0004",
      "assets": [{ "asset": "BTC", "total": "3.5", "reserved": "0.5", "free": "3" }]
    }
  ],
  "asOfSeq": 104905, "ts": 1785000000600
}

riskStatus is one of NORMAL, MARGIN_CALL, LIQUIDATION, DEFAULTED. imMultiplier is "1" unless the venue runs your account at a stricter margin profile. feeTier, makerFeeRate and takerFeeRate are omitted when you trade at per-instrument default fees; a negative maker rate is a rebate. spotReserved is cash promised to open spot buy orders and is already excluded from available. MM is the same quantity the WS margin channel reports as lowercase mm; the spellings differ, the number does not. GET /v1/account/margin serves the identical payload and exists as a cheap polling alias.

Your effective parameters, GET /v1/account/parameters (revision 3.61)

Since revision 3.62 the gateway-side blocks carry the published CLASS you sit in and the values in force: account.rateLimits (the rate-limit ladder: per-minute rates and the per-SECOND burst ceiling ordersPerSecBurst, enforced beside the per-minute bucket where your class publishes one, published 2026-09-10: STANDARD 1,000, DMA 3,000, MM 5,000 orders/s on production, 0 on the sandbox; one source per value, VENUE the STANDARD class, CLASS a published class, MEMBER a tighter value of your own; a raise is only ever a class assignment), account.otr (the order-to-trade class and thresholds in force, the published windowMinutes 60 and minOrders 100, and the prepared surcharge row, not in force by venue decision and applied to nobody while enabled is false: no order-to-trade surcharge applies, one may be introduced only by an amendment of the published fee schedule announced at least seven Business Days ahead), account.dea (the DEA-flow class and limits on reg.dea orders per UTC day, usedToday, and the identifiers stopped on your account: reject 3010 on their reg.dea orders, 3011 at the flow limit) and account.fees (the tier the published measurement cycle put you in with criteria, volume30d, measuredAt and cycle, and pending: every rate change of the schedule that will touch you with its effectiveFrom; a rate never changes before the published date, and a member-market rate exists only as an enrolment in a published programme). account.fees itself is never null, but its measuredAt, volume30d and cycle read null until the first measured fee cycle that includes your account, and so do tier and criteria unless you are enrolled on a Market Maker Scheme tier, which they then name with its obligations (the first-of-month 00:00 UTC boundary: a member admitted after the last cycle, and every member of a venue started after it, reads null until the next one, unless venue operations run a measurement before it); a null there means not measured yet, never a fee of zero, and the fee deducted on each market is on every markets[] row, makerFeeRate and takerFeeRate with their source (revision 3.83 states it; nothing changed). Your own self-trade prevention election is PUT /v1/account/stp.

Since revision 3.63 every market row also carries stpSource and stpScopeSource (MEMBER the account policy, SELF a value you set under a self-service scope, MEMBER_MARKET a per-market value venue operations set at your request, SELF_MARKET your own per-market election on PUT /v1/account/stp with symbol) and eligibility { restricted, source, memberLayer, marketLayer }, the Market Segments layers in force on that market (source is VENUE for open to all, MEMBER for your account configuration, MEMBER_MARKET for your per-market layer; marketLayer is absent while you never wrote one); the account block carries stpSource and stpScopeSource.

Your own tighter limits (PUT /v1/account/parameters, revision 3.64). Beside the values venue operations set, you may set your own pre-trade limits within the venue's rules (RTS 7 Article 20, RTS 6 Article 15): TRADE credential, body { "symbol"?, "set": { name: value }, "clear": [ name ] }. Without symbol the write is account-wide and may name maxOpenOrders, imMultiplier, priceBandBps, marketMaxSlippageBps, maxOpenRfqs and maxOpenContracts; with symbol it is for that market alone and may also name maxOrderNotional, maxOrderQty, maxPositionNotional and maxPositionQty (quantities, notionals and the multiplier as decimal strings, counts and bps as integers). The venue resolves the TIGHTEST of the market rule, any value venue operations set for your account and your own value, so a value of yours can only tighten what is in force: a looser one is refused with 5003 and a sentence naming the rule before anything is written (for example maxOrderQty 60 would loosen the value venue operations set for your account on BTC-USD-PERP (50): a self-set value can never loosen the venue's; the multiplier is a maximum, so there a smaller value is the loosening). A per-market value without symbol and a name that is not self-service (withdrawalBuffer, sessionStatus and access, which are venue measures, and stp / stpScope, which are PUT /v1/account/stp) are refused the same way; an unknown symbol is 2001. A cleared name reverts to the venue's rule; venue operations never edit your own values; a value of yours that a later, tighter venue rule beats stays stored and is listed under inert. The answer is { scope: ACCOUNT | MARKET, symbol, set, cleared, parameters }, parameters being exactly the GET read afterwards. On the read a value of yours in force shows as source SELF (account-wide) or SELF_MARKET (that market), and self { account, markets: [ { symbol, values } ] } lists every value you hold raw, including one that merely ties the rule in force. Your other sessions receive a MEMBER_PARAMS frame with origin MEMBER. The member portal's "API and limits" page offers the same controls.

The rules of every market are public on GET /v1/instruments (bands, caps, the circuit breaker, the staleness timeout, the liquidation parameters, the funding band and rate source, the per-market open-order ceiling, the access mode, the barrier guard, the swap offer validity, the trading hours). This read answers what applies to YOU after every member-specific tightening, and only ever your own values. Every row is { value, source }: value is written even when null (no rule at any scope), source names the scope that supplies it, one of VENUE, MARKET, MEMBER (a value venue operations set for your account on every market), MEMBER_MARKET (on this market), LEGACY (the historical per-account quantity override) and, on the fee rows, INSTRUMENT, TIER, PROGRAMME (on swap market rows always INSTRUMENT since revision 3.70); the set is open. A stored value a tighter scope beats is listed under inert on the same row. The account block carries maxOpenOrders, imMultiplier, withdrawalBuffer, priceBandBps, marketMaxSlippageBps, maxOpenRfqs, maxOpenContracts, stp, stpScope, your rateLimits and otr classes, dea, fees, marginWatchPct and legacy; every markets[] row carries access (ALLOWED / DENIED), sessionStatus, the per-market open-order ceiling, the order and position caps in force for you, imMultiplier, band, collar, the RFQ and contract caps, the fee rates with their source, STP and eligibilityRestricted. A member-specific value can only ever TIGHTEN a market rule; it is written by venue operations against an objective published class, or as a measure with a stated rulebook ground and a case reference that the venue notifies you of by mail with the section 36 appeal route. The withdrawalBuffer row is the fraction of the amount withdrawn that must remain on your account after the withdrawal, over and above Initial Margin (with your multiplier, open orders that may increase a position included) and cash or assets reserved for your spot orders, met from your cash balance after those reservations and never from unrealised profit; the venue-wide value is published on the Website and is 0; a higher value applies to you only as the published value of a class you sit in or as a Rules measure with a ground and a case reference notified by mail and appealable under Rules section 36 (Rules section 14); a withdrawal that fails the test is refused and may be resubmitted for a lower amount, and withdrawals stay refused during liquidation or default management. Changes arrive on venueEvents as MEMBER_PARAMS frames. A READ_ONLY credential may call it.

EndpointPurpose
GET /v1/account/parametersEvery parameter the venue applies to YOUR account with the scope that supplies each value (revision 3.61): account-wide values, one row per market, your rate-limit and order-to-trade classes, your fee resolution, the margin-watch level and the legacy quantity override; see "Your effective parameters" below. A READ_ONLY credential may call it.
PUT /v1/account/parametersSet or clear your OWN tighter limits (revision 3.64, "Your own tighter limits" below): { "symbol"?, "set": { name: value }, "clear": [ name ] }, account-wide without symbol (maxOpenOrders, imMultiplier, priceBandBps, marketMaxSlippageBps, maxOpenRfqs, maxOpenContracts) or for one market with it (those plus maxOrderNotional, maxOrderQty, maxPositionNotional, maxPositionQty). A value of yours can only TIGHTEN what is in force (the market rule and any value venue operations set for your account); a looser one is 5003 with a sentence naming the rule; a cleared name reverts to the venue's rule. Answers what was written and the effective values afterwards. TRADE credential.
GET /v1/positions?symbol=Member-level positions: {symbol, netQty, avgEntry, markPrice, unrealizedPnl, realizedPnl, accFunding, settleCcy, adlQuintile}. Flat positions are omitted, and spot holdings never appear here (they live in assets on the account). Barrier contracts never appear here either: they are bilateral and live on GET /v1/barriers.
GET /v1/positions/end-clients?symbol=&clientUid=Per-clientUid sub-positions, same shape plus clientUid and, on hedge-mode rows, positionSide (LONG | SHORT, omitted on netting rows): each bucket is its own row, so a client using hedge mode can have up to three rows per instrument. Informational only: margin is enforced at member level, so these rows carry no margin consequence. They exist so your books can be reconciled against the venue's.
GET /v1/account/ledger?kind=&from=&to=&limit=Cash movements: {entryId, kind, ccy, amount, balanceAfter, refId, ts}; ccy (revision 3.70) is the entry's currency exactly as the WebSocket wallet frame and the stream WALLET_ENTRY row name it (the settlement currency on cash rows, the base asset or swap asset on asset rows; an empty string on a row recorded before the venue kept the column). kind filter values are DEPOSIT, WITHDRAWAL, TRADEPNL, FEE, FUNDINGPAY, FUNDINGRECEIVE, LIQUIDATIONLOSS, INSURANCEFUNDFLOW, ADLSETTLEMENT, MANUALADJUST, SPOTTRADE, FINALSETTLEMENT, BARRIERSETTLE, RECOVERYDISTRIBUTION. from/to in ms, limit default 100, max 1000. balanceAfter is the balance after the entry; the WS wallet channel carries the same quantity as both balanceAfter and its legacy alias balance, always equal.
POST /v1/margin/previewPre-trade margin cost of hypothetical orders, priced as one basket (1 to 50 entries). MARKET entries are valued at the mark; accepted applies the admission rules order entry applies, including the reduce-only budget, with the same reduceOnly exemptions.
PUT /v1/account/stpElect your self-trade prevention policy and scope (revision 3.62; since revision 3.63 an optional "symbol" makes it your per-market election on that market, INHERIT removes it, and the answer carries symbol, stpSource and stpScopeSource): { "stp": CANCEL_RESTING | CANCEL_TAKER | CANCEL_BOTH | REJECT_TAKER, "stpScope": END_CLIENT | MEMBER }, either field alone keeps the other; answers the pair now in force, which also reads on GET /v1/account (stp, stpScope) and GET /v1/account/parameters. The same-clientUid wash-trade block is non-optional under every policy. TRADE credential
GET /v1/margin/calls?from=&to=&limit=Margin-call notice history, newest first: {seq, cursor, ts, equity, im, mm, available, ccy}, each value captured at the moment of the call. Default 100, max 1000.
POST/v1/margin/preview
{ "orders": [ { "symbol": "BTC-USD-PERP", "side": "BUY", "type": "LIMIT",
               "tif": "GTC", "price": "65000", "qty": "10",
               "clOrdId": "preview-1", "clientUid": "CLIENT-A" } ] }
{ "code": 0, "msg": "ok",
  "data": { "indicative": true, "accepted": true, "im": "197000",
            "mm": "98500", "available": "857930.5" },
  "asOfSeq": 104906, "ts": 1785000000610 }

Each nested order uses the full place-order schema and is validated the same way, so a malformed probe fails with the same code a real order would. The array is priced as one basket of 1 to 50 entries (more reject with 2002): perpetual entries fold into order margin together, with the same one-sided max(buy, sell) netting per instrument that resting orders receive, spot entries have their prefunding needs summed per currency, and accepted answers every gate at once, independent of array order. The figures describe the WHOLE basket, in the settlement currency of the first entry. The preview is charged to the read bucket and is permitted on a READ_ONLY credential, which is what makes it usable from a risk process that must never be able to trade. The answer is indicative: it is computed against the state at asOfSeq and the market moves.

How entries are priced. A priced entry is valued at its own limit, a conditional at the limit it would work at once armed (trigger.limitPrice), and a MARKET entry at the current mark, on perpetual and spot markets alike. The mark is what the venue already values a market order at for the per-order size cap and the minNotional floor. It is not the price a market order would get: the protection collar forms from the book at the instant of entry, so a market entry is an indication at today's mark rather than a quote. Size a market order against the book, or send a limit. On a spot market BUY the difference is a funding one: order entry prefunds it at the collar, which is normally above the mark, so a spot market buy sized to the last micro of your cash can preview accepted: true and still be rejected 3001 when you send it.

What accepted answers, entry by entry, is the same set of admission rules order entry applies: available (perpetual order margin, spot buy prefunding in cash, spot sell prefunding in base-asset units, summed per currency), the per-order size cap (2013), the minNotional floor (2028), the position cap (2011) and the reduce-only budget (2008). Reduce-only entries carry the same exemptions they carry on order entry: a reduceOnly entry is exempt from the available gate, from minNotional and from the position cap, so a reduce-only basket previewed during a margin call answers accepted: true, which is what sending it does; it is not exempt from the per-order size cap, because that cap guards closes too. What accepted does not answer is venue state that is not a property of your basket: halts, member or instrument status, and whether a market order would find liquidity.

The reduce-only budget applies to the whole basket, and it is MEMBER-level. A reduceOnly order may only close what your MEMBER book holds: order entry admits one while the reduce-only quantity already working on that market, plus the new order, stays within your member net position on the reducing side, and rejects the surplus with 2008. The preview measures your entries together against the same budget, so a basket that would close MORE than you hold answers accepted: false rather than promising an acceptance the second entry would not get, and an entry on the side that would ADD to your position, or on a market where you hold nothing, is rejected the same way. A clientUid on an entry never narrows the budget to that client's sub-position, here or on order entry: see the reduce-only warning, which matters most for an end-client close.

Entries the venue would refuse outright answer with a code, not with accepted. accepted is for the admission gates above; an entry order entry would reject before it ever reaches them fails the preview with the same code the order gets: 2016 for reduceOnly or a market-type conditional BUY on a spot market, 2013 for an out of range trigger or post-trigger limit price, 3005 for a market with no sequenced mark yet, 2002 for a limit priced at zero or a misaligned value, 2018 for a leverage or quoteCcy assertion the instrument cannot satisfy. The basket answers with the first such entry.

History

History routes are served from the venue's projection store rather than live engine memory. Their envelope asOfSeq is the store watermark, the highest sequence with no hole beneath it, not the engine head. That is an honest signal: if the watermark is behind, the answer is complete only up to the watermark, and you should poll rather than conclude that nothing happened. While the store has a recorded hole (store.status REPAIRING, see Stream recovery) GET /v1/executions and GET /v1/account/ledger serve only the rows at or below the watermark, with and without a symbol or kind filter, as the event stream routes and the rejection rows of GET /v1/orders/history do (the two since revision 3.83; before, they could carry rows from after the hole while the hole's own rows were missing). The closed-order rows of GET /v1/orders/history are not held: until the status is COMPLETE again they can include orders closed after the hole, above asOfSeq, while orders closed inside it are missing.

GET/v1/orders/history?symbol=&from=&to=&limit=

Closed orders and rejections. This is the only place a rejection that never reached the book can be re-read after the fact, which makes it the right source for a daily reconciliation of everything you sent.

{ "code": 0, "msg": "ok",
  "data": [
    { "orderId": 887711, "clOrdId": "a3f1c9d24b7e4f108c6d2e5b", "symbol": "BTC-USD-PERP",
      "side": "BUY", "price": "65000", "qty": "1", "remainingQty": "0",
      "status": "FILLED", "clientUid": "CLIENT-A",
      "acceptedTs": 1785000000117, "closedTs": 1785000000118 },
    { "orderId": 0, "clOrdId": "a3f1c9d24b7e4f108c6d2e5c", "symbol": "BTC-USD-PERP",
      "side": "BUY", "price": "65000", "qty": "1", "remainingQty": "0",
      "status": "REJECTED", "rejectCode": 3001, "clientUid": "CLIENT-A",
      "acceptedTs": 1785000000200, "closedTs": 1785000000200 }
  ],
  "asOfSeq": 104870, "ts": 1785000000700 }

Rows carry the optional positionSide field exactly like the live order surfaces: LONG or SHORT for an order placed with hedge mode, OMITTED (not null) on netting orders, so every pre-hedge row is byte-identical. Both rows in the example are netting orders, which is why the field does not appear. See Hedge mode under order entry.

cancelReason is one of a closed set: USER_REQUESTED, IOC_REMAINDER, FOK_UNFILLABLE, GTD_EXPIRED, STP_CANCEL, LIQUIDATION_CANCEL, ADMIN_CANCEL, INSTRUMENT_HALT, MEMBER_SUSPENDED, REDUCE_ONLY_VIOLATION, SPOT_UNFUNDED, POSITION_SIZE_LIMIT, MARKET_ACCESS (revision 3.65: the venue cancelled the order on a DENIED access decision or an admission flip), FUNDING_CATCH_UP (revision 3.70, venue-initiated: a conditional order that triggered while the market's GRID funding catch-up was pending; reduce-only conditionals execute), POST_ONLY_WOULD_CROSS (revision 3.70, venue-initiated: a post-only order that rested through a call on a scheduled SECURITY market and would cross on the entry into continuous trading). USER_REQUESTED covers every cancel of your own: a cancel on any transport, a member-wide cancel-all, the member portal's kill switch and the cancel on disconnect you armed, which the value does not tell apart (revision 3.83 states it); ADMIN_CANCEL is reserved for cancels by venue operations. The full table with which reasons are venue-initiated is on the Reference page.

GET/v1/executions?symbol=&from=&to=&limit=

Your fills, one row per leg you were party to, with fee, role (MAKER | TAKER), flags (NORMAL | LIQUIDATION | ADL | RFQ | BARRIER_CLOSE | REVERSAL), clOrdId, refSource and clientUid. Default limit 100, maximum 1000. Executions are anonymous: an execution record, here, on the WS execution channel, the FIX drop copy and the event stream, names your own side only (your order, your clOrdId, your clientUid, your fee and your role); no field identifies the member on the other side, and the venue does not disclose it except where Applicable Laws or the Rules provide for it.

Venue-initiated closes. When the venue closes a position there is no member order, so no member reference exists. Those executions carry "refSource": "VENUE" and a synthetic reference in the reserved namespace: KX-LQ-{orderId} for a liquidation slice and KX-ADL-{tradeId} for an ADL close. Both are pure functions of durable identifiers, so the identical string appears on the live socket, on a REST re-read and in history. Member references can never start with KX- (code 2020), so the two namespaces cannot collide, and refSource alone is enough to route a fill to the right side of your books. clientUid on such a close is the end client whose sub-position was closed, so the tuple (clientUid, symbol) resolves it to exactly one sub-position. Venue-generated orders never produce an accept event: the first thing you see is a fill or a cancel.

Lots: a per-client lot view

A CFD-style display of an end client's open position as the individual opening fills, "lots", that built it, with a targeted per-lot close. Lots are a DISPLAY CONVENTION folded on demand from your immutable fill history: the engine keeps no lots, and margin, liquidation, funding attribution and the venue's booked realized PnL remain average-cost per sub-position. The fold changes reporting, never economics.

The fold. Lots exist per (clientUid, hedge bucket, instrument), where the bucket is the netting bucket or the hedge-mode LONG/SHORT bucket. Every fill that INCREASES the bucket's position opens a lot: its lotId is the tradeId of the opening fill and its entry price is that fill's price. Every fill that REDUCES the bucket consumes open lots: lots explicitly assigned to the reducing order first (the targeted close below), then FIFO from the oldest. A fill that crosses the bucket through zero closes every open lot and opens one new lot in the opposite direction with the remainder. The open lots' qty sums exactly to the bucket's net quantity. Per-lot unrealizedPnl is measured from the lot's OWN entry to the mark, the bucket's unrealizedPnl on GET /v1/positions/end-clients from its average cost, so the two sums agree only until a partial close leaves lots at different entry prices; from then on they differ by exactly the realized PnL the lot attribution has taken over the booked figure since the bucket was last flat (lots of 10 at 100 and 10 at 120, 4 of the first closed at 130: the closure shows 120 realized, the venue books 80, and the remaining lots' unrealizedPnl sums 40 below the bucket's). Before revision 3.80 this page said the sum was exact. The fold is CLAMPED to the live sub-position: a venue-side close the fold cannot attribute (a liquidation on an RFQ-mode market) trims the oldest open lots of the bucket, so you never see a lot the venue already closed. ADL allocations are attributed from the per-client ADL record, and since revision 3.80 a hedge-mode client's book liquidation is attributed to its own LONG or SHORT bucket (before, it was folded into the netting bucket and its lot was trimmed instead). Lots exist on order-book perpetual and spot markets; BARRIER symbols are refused by both routes with 2013, because barrier contracts are bilateral and have no lots.

Reading lots

GET/v1/lots?clientUid=&symbol=&positionSide=

clientUid and symbol are required; positionSide optionally filters to one bucket (NET, LONG or SHORT). Served from the history store like GET /v1/orders/history, so the envelope asOfSeq is the store watermark.

{ "code": 0, "msg": "ok",
  "data": {
    "clientUid": "CLIENT-A", "symbol": "BTC-USD-PERP", "mark": "69000",
    "settleCcy": "USD", "accFunding": "-12.5",
    "openLotsFunding": "-9.2", "closedLotsFunding": "-3.3",
    "lots": [
      { "lotId": 990312, "direction": "LONG", "entryPrice": "68000",
        "qty": "0.002", "unrealizedPnl": "2", "accFunding": "-6.1",
        "fundingIntervals": 7, "openedTs": 1785470168892,
        "openClOrdId": "my-open-17", "openOrderId": 774120 },
      { "lotId": 990840, "positionSide": "LONG", "direction": "LONG",
        "entryPrice": "68500", "qty": "0.001", "unrealizedPnl": "0.5",
        "accFunding": "-3.1", "fundingIntervals": 4,
        "openedTs": 1785470201230,
        "openClOrdId": "my-open-18", "openOrderId": 774131 }
    ]
  },
  "asOfSeq": 104870, "ts": 1785470201400 }

Funding on lots

The venue charges funding on the client sub-position net, once per settlement anchor, never per lot, so a per-lot figure is an attribution of a booked charge and not itself a booked figure. The attribution is exact: each anchor charges exactly the lots that were open when it settled, and the shares sum to the charge on the bucket net to the micro, with the rounding remainder booked to the largest lot, the same rule the venue uses to split a member's charge across its clients. That exactness is against the charge on the bucket net: the booked accFunding of the one client that receives the remainder of splitting YOUR member charge across your clients can differ from the sum of its lots by that remainder, which then shows in closedLotsFunding. Financing follows the quantity actually held, so a partially closed lot keeps what it incurred while it was larger, and a fully closed lot takes its financing with it.

The envelope therefore carries three figures, and all three matter:

FieldMeaning
accFundingAuthoritative. Lifetime funding the venue BOOKED on this sub-position, over the buckets the response covers.
openLotsFundingSum of accFunding over the lots listed. Attribution, not a booked figure.
closedLotsFundingThe difference: financing already paid by quantity since closed. Non-zero is normal on any client that has ever reduced.
Reconcile client statements against accFunding, not against the sum of the lots. Exactly the same caution as per-lot realized PnL: the lot figures present a booked total, and only the booked total is what the venue settles with you.

The fold is bounded, and the bounds are published: at most 100,000 fills per (clientUid, symbol) scope, at most 20,000 funding settlements to attribute (about 18 years of an 8 hour grid, counted from the first fill the fold covers), and at most 5,000,000 attribution steps (settlements x lots open at each). Past a bound the request refuses with 2013. positionSide narrowing folds ONLY that bucket, so it reduces the attribution work in proportion; past the settlements bound, or when a single bucket alone exceeds the work bound, there is no in-API remedy: reconcile that client by positions and executions instead, and note the lot close endpoint is refused for such a scope too.

Reading closures

GET/v1/lots/closures?clientUid=&symbol=&positionSide=&from=&to=

The close-attribution feed: every CLOSING fill in the scope, mapped to the exact lot or lots it consumed by the SAME fold that builds GET /v1/lots. The feed folds WITHOUT the clamp, so the one close it cannot list is a close the fold cannot attribute (a liquidation on an RFQ-mode market): the lots view trims that lot and no closure row names it. clientUid and symbol are required, positionSide filters to one bucket, and from and to are optional Unix ms bounds on the closing fill's ts. Rows are oldest first, and one closing fill that consumed several lots appears as one row per lot, in consumption order.

{ "code": 0, "msg": "ok",
  "data": {
    "clientUid": "CLIENT-A", "symbol": "BTC-USD-PERP", "settleCcy": "USD",
    "closures": [
      { "closeTradeId": 991402, "lotId": 990312, "qty": "0.002",
        "openPrice": "68000", "closePrice": "69100", "realizedPnl": "2.2",
        "openClOrdId": "my-open-17", "closeClOrdId": "my-close-31",
        "flags": "NORMAL", "unsolicited": false, "ts": 1785470880110 },
      { "closeTradeId": 991540, "lotId": 990840, "positionSide": "LONG",
        "qty": "0.001", "openPrice": "68500", "closePrice": "67200",
        "realizedPnl": "-1.3", "openClOrdId": "my-open-18",
        "closeClOrdId": "KX-ADL-991540", "flags": "ADL",
        "unsolicited": true, "ts": 1785471002480 }
    ]
  },
  "asOfSeq": 104903, "ts": 1785471002500 }

Closing one lot

POST/v1/lots/{lotId}/close
{ "clientUid": "CLIENT-A", "symbol": "BTC-USD-PERP", "reduceOnly": false, "clOrdId": "close-lot-1" }

Closes a specific open lot: the venue submits a MARKET IOC order on the opposite side for the lot's remaining quantity, tagged with the lot's hedge bucket, and records an ASSIGNMENT of that order to that lot, so the fold consumes THAT lot first rather than the FIFO-oldest one. The response is { "lot": {...}, "order": {...} }: the lot as the fold saw it at request time and the ordinary place-order response for the submitted close, executions included. The envelope asOfSeq here is the ENGINE sequence of the close order's ack, exactly as on POST /v1/orders; use it as the watermark the GET route must reach before the fold reflects the close.

Risk and unwind

EndpointReturns
GET /v1/adl/indicator?symbol=[{ "symbol": "BTC-USD-PERP", "adlQuintile": 3 }], only for instruments where you hold a position. 1 is safest, 5 is first in the auto-deleverage queue, ranked by the published score (Trading rules, ADL). The queue ranks every holder on a side by unrealized profit ratio (unrealized PnL over the position's entry notional) multiplied by account leverage (position notional at the mark over your equity in the settlement currency), highest first, ties by ascending member id; a position without unrealized profit ranks below every profitable one and is reached only by the terminal backstop; recomputed every 5 seconds.
GET /v1/adl/events?from=&to=&limit=ADL executions affecting you, newest first: {tradeId, episodeId, symbol, price, qty, qtyLots, side, role, closeSide, positionSide, clientUid, ts} where qty is decimal base-asset units (qtyLots is the raw engine lot count, kept for older consumers), side is YOUR relation to the EPISODE (DEFAULTER on every row of an episode you were unwound in, your own per-client rows included; COUNTERPARTY on a slice taken from you), role (revision 3.70) is the row's attribution (DEFAULTER, the defaulter's own per-client allocation; DELEVERAGED, the counterparty allocation), closeSide (BUY | SELL) is the direction the venue closed the member named by the row in, omitted on rows recorded before revision 3.70 (every one of them a counterparty row), and positionSide is the hedge bucket, omitted for netting. clientUid is present on your own rows only (a deleveraged member's allocations, a defaulter's own per-client rows) and omitted on the slices a defaulter sees taken from other members. Several rows per tradeId are normal, one per allocation. The defaulter rows are in force on the dev venue from engine sequence 1,500,000,000 (announced by venue operations; from genesis on production).
GET /v1/unwindsLiquidation and ADL episode reports. CLOSED episodes are retained live up to 10,000 venue-wide, oldest released first; older history stays available via the event stream and GET /v1/adl/events. Open episodes are never released.
{ "code": 0, "msg": "ok",
  "data": [
    { "episodeId": 4417, "role": "COUNTERPARTY", "trigger": "MARGIN_BREACH",
      "status": "CLOSED", "canceledOrderIds": [], "tradeIds": [],
      "adlAllocations": [
        { "counterpartyClientUid": "CLIENT-A", "symbol": "BTC-USD-PERP",
          "qty": "4", "price": "63120" }
      ],
      "insuranceFundDelta": "0", "residualLoss": "0" }
  ],
  "asOfSeq": 104910, "ts": 1785000000800 }

The payload is asymmetric by design. As the DEFAULTER you see the full episode: every canceled order id, every trade id, all allocations, the insurance fund delta and any residual loss. As a COUNTERPARTY you see only your own allocations, with insuranceFundDelta and residualLoss reported as "0", because another member's default economics are not yours to read.

Stream recovery

Every event-class message the private WebSocket delivers is also written to a durable, sequence-ordered store and can be re-read over REST. Even a REST-only integration should page this at startup to prove that nothing happened while the process was down.

GET/v1/stream/cursor
{ "code": 0, "msg": "ok",
  "data": {
    "headCursor": "000000000000010487000000000090", "headSeq": 104870,
    "retentionFromCursor": "000000000000009800000000000000", "retentionFromSeq": 98000,
    "store": { "contiguousThroughSeq": 104870, "engineSeq": 104882,
               "lagSeq": 12, "status": "CATCHING_UP", "complete": false }
  },
  "asOfSeq": 104870, "ts": 1785000000900 }

headCursor is the newest row visible to you at the watermark, omitted when you have none. retentionFromCursor is the oldest row still retained (default retention 35 days): if your stored cursor is older than that, a complete backfill is impossible and you must fall back to snapshot reads plus the dated history routes.

That 35 day window is the event stream's own retention. The venue's record of every transaction (orders, executions, ledger postings and the contract lifecycle) is kept for at least seven years from the creation of each record on a durable medium that keeps it unaltered and replayable, and it is available to you on request: use the history routes, the ledger and your monthly statements first, and for anything beyond them write to venue operations, which answers from the venue's archive with your own records only, never counterparty identities, within ten Business Days (revision 3.70).

GET/v1/stream/events?afterCursor=&limit=
ParamRules
afterCursor30 ASCII digits, EXCLUSIVE: returns strictly greater deliveries. The fields are seq (19 digits, a signed 64-bit integer, 0 to 9223372036854775807), eventIndex (10 digits, a signed 32-bit integer, 0 to 2147483647: the 0-based position of an event among the events of one engine command, restarting at 0 for every seq) and leg (1 digit). Malformed gives 2019; since revision 3.83 a well-formed cursor with a field above its range answers 2019 with a msg naming the field and its range (afterCursor seq (characters 1 to 19) is out of range: it must be 0 to 9223372036854775807, afterCursor eventIndex (characters 20 to 29) is out of range: it must be 0 to 2147483647), anything that is not 30 ASCII digits afterCursor must be 30 numeric characters. Branch on the code, never on the msg.
fromSeqint64 >= 1, INCLUSIVE lower bound, for a client that kept only asOfSeq.
toSeqOptional int64 >= 1, INCLUSIVE upper bound. Must be >= fromSeq.
channelsOptional CSV of the ten event-class families: order, execution, wallet, margin, adl, unwind, eligibility, funding, status, swaps (revision 3.51). Anything else gives 2019; since revision 3.73 a value that names no family (empty, commas only) gives 5003.
symbolOptional; excludes rows that are not instrument-scoped.
limitDefault 500, clamped to [1, 2000].
waitMsDefault 0, clamped to [0, 2000]: a bounded wait for the store to catch up. The answer is authoritative either way.

Exactly one of afterCursor or fromSeq is required; supplying both or neither gives 5003 with a message naming the parameter. Ordering is strictly ascending by (seq, eventIndex, leg), the only ascending history route on the venue, because a backfill has to replay in engine order.

{ "code": 0, "msg": "ok",
  "data": {
    "fromCursor": "000000000000010486000000000040",
    "nextCursor": "000000000000010486800000000011",
    "hasMore": true, "count": 500,
    "events": [
      { "channel": "execution", "eventType": "EXECUTION", "asOfSeq": 104862,
        "eventIndex": 0, "leg": 1, "cursor": "000000000000010486200000000001",
        "data": { "tradeId": 55123, "orderId": 887711, "symbol": "BTC-USD-PERP",
                  "side": "BUY", "price": "65000", "qty": "5", "fee": "162.5",
                  "role": "TAKER", "flags": "NORMAL", "clOrdId": "a3f1c9d24b7e4f108c6d2e5b",
                  "clientUid": "CLIENT-A", "ts": 1785000000512 } } ],
    "store": { "contiguousThroughSeq": 104870, "engineSeq": 104882,
               "lagSeq": 12, "status": "CATCHING_UP", "complete": false }
  },
  "asOfSeq": 104870, "ts": 1785000001000 }

Page by feeding nextCursor back as afterCursor until hasMore is false, and stop on hasMore false or count 0, never on nextCursor equal to the afterCursor you sent. On a page with hasMore true nextCursor is the cursor of the last returned event; on a page with hasMore false (revision 3.73) it is a position at the store watermark the page was read at, or at your toSeq when that is lower (eventIndex 2147483647 and leg 9, a bound, not a delivery), never lower than your own cursor and valid only with the same channels and symbol, so a filter that matches nothing new advances instead of re-reading the same range. The venue serves that last-page position on an environment from the date venue operations announce for it there (revision 3.83 states the condition, which revision 3.73 left out); before that date, and on a page read while the store recorded or cleared a hole, a last page answers the cursor of its last event, else the cursor you sent, and the loop rule above is correct under both. Persist that nextCursor per filter and resume from the ordinal maximum of it and the cursor of the last event you processed. Reads are clamped at the watermark: no returned event ever carries an asOfSeq above store.contiguousThroughSeq, and nextCursor never passes it either. A page can therefore come back short of the engine head, but never out of order and never with a hole in it. store.status is an exhaustive three-value enum: COMPLETE (level with the engine), CATCHING_UP (poll again), REPAIRING (the store has a recorded hole and reads stall below it rather than skipping it, so keep polling and raise an operational alert). Because the live socket flush runs marginally ahead of the store write, a backfill issued microseconds after a live message can legitimately come back short with CATCHING_UP; that is what lagSeq and waitMs are for. On a deployment without an event store these three routes answer 5004 with HTTP 503 after authenticating, never a 404.

REPAIRING in detail (revision 3.83 states it; nothing changed). The store block describes the store of the NODE that answered. REPAIRING starts at every leader change of a clustered venue, the sandbox included (the new leader records as a hole the commands between its store's durable position and its own head), when the store falls further behind than its write buffer absorbs, and when a store write fails beyond a short retry. No timeout ends it and the venue sets no maximum duration: it ends when the hole is re-folded from the venue's journal, by the new leader after a leader change and by venue operations otherwise, a hole that opens on a running leader included, and the older the hole, the longer its repair. Meanwhile contiguousThroughSeq and the envelope asOfSeq stop just below the hole and lagSeq counts the commands from the start of the hole to the engine head, not the size of the hole, so it grows while the hole stands. Until it clears you cannot backfill past the hole: keep consuming live frames, recover the state you need from the state reads, and never skip the hole; trading, the live feeds and the open-order and position reads do not depend on the store. A node that is not the leader answers the event stream reads from its own store position, which no longer advances; while it records no hole, its GET /v1/executions and GET /v1/account/ledger read the venue's shared store whole, so their rows can run ahead of the asOfSeq it answers. It closes the connection after its answer, so your next request reaches the leader.

Reconciliation

GET/v1/reconciliation/end-client-positions?date=&format=

date is required, in the form YYYY-MM-DD; a value that is no date gives 5003. Without format, or with any value other than csv, the response is the standard envelope with an array of {date, clientUid, symbol, positionSide, netQty, avgEntry, mark, upnl}: positionSide (LONG or SHORT) is omitted on netting rows, mark when the market had none and upnl when the row holds none. With format=csv the response is raw text/csv with no envelope, which is the one place on the API where that is true, so route it before your generic JSON parser:

date,clientUid,symbol,positionSide,netQty,avgEntry,mark,upnl
2026-10-18,CLIENT-A,BTC-USD-PERP,NET,4,64110.5,65001.25,3563
2026-10-18,CLIENT-B,ETH-USD-PERP,SHORT,-20,3210.25,3198.5,235
2026-10-18,"CL,C",BTC-USD-PERP,NET,0.5,65200,65001.25,

The CSV prints date as YYYY-MM-DD, positionSide as NET on netting rows and an absent value as an empty field, and quotes a field that holds a comma, a double quote, CR or LF per RFC 4180 (wrapped in double quotes, every inner quote doubled); nothing else is escaped, so a spreadsheet may evaluate a clientUid that begins with = or @. Before revision 3.81 the CSV printed date month first (MM/DD/YYYY), quoted no field and left upnl empty on every row.

upnl (revision 3.81) is the unrealized PnL at mark in the market's settlement currency, exact to the micro: the unrealizedPnl that GET /v1/positions/end-clients reports at that mark, computed from the booked cost basis, so it can differ by micros from netQty x (mark - avgEntry). The venue writes it only where the history store's net quantity for the row equalled the matching engine's at the snapshot's instant; otherwise (the third line above), without a mark, and on rows recorded before revision 3.81 it is empty in the CSV and omitted in JSON. Rows are immutable once written, and a day without a snapshot answers an empty list.

This is the end-of-day snapshot to diff against your own sub-position ledger. The venue takes it at its daily anchor, within about five minutes after 00:00 UTC for the day that just ended, while it keeps trading, so a fill of the first minutes of the new day can be in it. If the venue restarts, or another of its nodes takes over, before the next anchor, the anchor runs again for the previous day and adds the rows of the sub-positions that day does not hold yet, from the state at that moment, mark and upnl included (all of the day's rows when the anchor had not run at midnight); a snapshot venue operations take by hand does the same, and a row already written never changes. Run it daily: per-clientUid positions are informational at the venue and authoritative at your firm, and a silent divergence found a month later is a regulatory problem rather than a bug.

Market Segments

Market Segments is the venue's counterparty-acceptance mechanism. Matching requires mutual acceptance and the default is open to all. Restricted liquidity stays visible in the full book; the matchable book shows what you can actually hit.

EndpointBehaviour
GET /v1/market-segments{restricted, allow: [memberCode...], version}; since revision 3.63 also markets: [{symbol, restricted, allow, version}], every per-market layer you hold.
GET /v1/market-segments?symbol=Revision 3.63: your PER-MARKET layer on that market, {symbol, restricted, allow, version} (version 0 and restricted false while you never wrote one). Unknown symbol gives 2001.
PUT /v1/market-segments?symbol=Revision 3.63: replaces your per-market layer on that market with the same body and the layer's own version; answers { "version": 2, "symbol": "BTC-USD-PERP" }. A layer only ever narrows: on that market a counterparty must be admitted by the account configuration of both sides AND by the market layers of both sides, on the book, in FOK counting, in the matchable book, in RFQ and in swap takes alike. The eligibility channel frame carries symbol for a layer change.
GET /v1/market-segments/matchable-members?symbol=Revision 3.63: the member codes you can match with on that market (account configurations and market layers of both sides).
PUT /v1/market-segmentsReplaces the whole config. Requires a TRADE credential. Optimistic concurrency: send the version you read, and a stale version gives 4001 with HTTP 409. Returns { "version": 8 }. An unknown member code gives 5003.
GET /v1/market-segments/matchable-membersArray of member codes you can currently match with, after mutual acceptance is applied.
GET /v1/orderbook/matchable?symbol=&depth=Signed. Rows are [price, matchableQty, ownQty]. Your own orders are always visible to you.

GET/PUT /v1/eligibility and GET /v1/eligibility/matchable-members are compatibility aliases with identical handlers and payloads, kept for the life of /v1. Price-time priority is honoured within your accepted counterparty set: if better-priced liquidity exists from members outside your mutual acceptance, your order executes at the best price matchable for you, which is why a fill can print worse than the top of the full book.

Idempotency and safe retries

clOrdId is the idempotency key for order entry. The venue indexes it per member, and the three outcomes are exhaustive:

You sendVenue doesYou see
A new referencePlaces the ordercode: 0, the new order
The same reference, byte-identical payloadNothing. Returns the original ordercode: 0, the ORIGINAL orderId and its current state, plus "duplicate": true
The same reference, different payloadRejectscode: 2009, HTTP 400, data.clOrdId echoing the reference

The duplicate response is the current state of the original order, not a replay of the original response, so a retry that lands after the order has filled returns FILLED with duplicate: true. Treat duplicate: true as success and reconcile against the returned orderId.

How long the venue holds a reference. Uniqueness for 24 hours is your obligation. What the venue retains is the most recent 4,000,000 references venue-wide, or 24 hours, whichever is reached first, so on a day of exceptional venue-wide volume the effective duplicate-detection horizon is shorter than 24 hours. A released reference is simply unknown, so the first row of the table above applies to it: a resend places a NEW order. A reference whose order is still working or pending trigger is never released, so DELETE /v1/orders?clOrdId= resolves for the whole life of the order. Retries act within seconds and are unaffected; reconcile older state by orderId or GET /v1/orders, never by resending.

Never retry order entry with a fresh clOrdId. A timeout, a connection reset or a 5xx does not mean the order was not accepted. Acknowledgement follows durability, so the order may already be journaled, matched and filled while your socket was dying. Retrying with the SAME reference is safe by construction, because the idempotency key collapses it. Retrying with a NEW reference is how a firm doubles a position on a flaky link. If you must resolve the ambiguity before resubmitting, call DELETE /v1/orders?clOrdId= or GET /v1/orders/{orderId} first: the answer tells you whether the order exists.

Two things are never backfillable and must be handled synchronously: the duplicate rejection above (it reaches you only as the order response) and venueEvents frames (resubscribe and apply the fresh snapshot). Do not wait for either to appear in the event stream.

Errors and the retry matrix

CodesHTTPRetry?How
1002 clock skew401yesResync your clock and resend, SIGNED AGAIN with a fresh timestamp. If it persists, you are signing a timestamp you did not transmit. Since revision 3.85 also request already received (the same signed bytes sent again: sign the retry afresh), request signed before this node began to accept writes (sign it again), and an X-RECV-WINDOW outside 1 to 60000 or a timestamp more than 5 s ahead (fix the header or the clock).
1003 rate limit, 5001 engine busy429, 503yesExponential backoff with jitter. On order entry reuse the same clOrdId so the idempotency key protects you. During a leader change on a clustered deployment a SINGLE write answered 5001 may already be in the venue's log and may still execute on the new leader: read it back by clOrdId on the leader before resending it, or resend it with the SAME clOrdId (revision 3.83 states it; a batch row's 5001 never is such a write). On 1003 sleep data.retryAfterMs (for a history read the wait is a suggestion, and a repeated 1003 means your reads overlap: serialise them; one while no other read of yours of the group is in flight is the node's shared budget: back off).
1004 order-to-trade block429yesOnly after the ratio recovers. Cancels are never blocked, so you can always flatten your working orders first.
3010 DEA client blocked400noNot for that identifier while the stop stands; cancel its working orders, appeal under Rules section 36. Unmarked flow and every other identifier trade on.
3011 DEA flow limit400yesAfter 00:00 UTC, when the day's DEA-flow allowance resets; only reg.dea orders count.
4001 version conflict409yesRe-read the current version, merge, resend.
5000 internal error500reads onlyReads: retry. Order entry: reconcile by clOrdId BEFORE resubmitting. On a clustered deployment also a write whose commit the venue did not confirm within 10 seconds (on the WebSocket since revision 3.83, where it acked 2013 before): its outcome is unknown and it may still execute, so read it back on the leader or send it again with the SAME clOrdId.
5030 not the leader503redirectClustered deployments, among them the clustered sandbox of revision 3.83 from the date venue operations announce: a node that is not the leader answered a request that is not a GET under /v1 (a read-only POST such as POST /v1/margin/preview included) before the route ran and never forwarded it, so nothing took effect. leader is an https base URL, on every venue deployment the environment's published entry host; leaderId 0 and leader null while no leader is known. The node closes the connection after its answer, so your next request reaches the leader. A redirect, never a rejection and never a backoff: accept leader only when it equals the entry host you configured and send the request again there with the SAME clOrdId, a cancel, cancel-all or amendment at once; if it names another host, send no credentials there and tell venue operations. Never count it as a reject. A single write's 5001, a 5000 or a lost answer during the same leader change may still execute: reconcile by clOrdId.
A batch ROW with 5001, 5000 or 5005 (revision 3.74)inside a 200that entry onlyThe other rows stand. 5001: the entry was not accepted, resend it alone (a placement with the same clOrdId). 5000: outcome unknown, read the order back on the leader first, and resend a placement only with the SAME clOrdId. 5005: reconcile, never resend it as it was.
3003 halted, 3005 no mark price, 5002 venue halted400not a retry matterWait for the state change. Watch GET /v1/status or the venueEvents channel; do not spin.
1001 auth, 1005 read-only credential, 2001 to 2032, 3001, 3002, 3004, 3006, 3007, 5003401, 403, 400, 404noDeterministic. The identical request will fail identically. Fix the cause. (2031 clears once the identity record is PUT, not by resending the GET; 2032 means the lot is gone or was never open, re-read GET /v1/lots.)
5004 event store not configured503noA property of the deployment, not a transient fault. On the event stream routes the same HTTP 503 also carries 5007, which is retried: branch on the code.
5007 history store busy503yesReads only (revision 3.73). Sleep data.retryAfterMs (also Retry-After), then retry the same read with backoff; after repeated 5007 on the same read, narrow it (a smaller limit, a later cursor or from, no symbol). Nothing changed on your account.
5005 command failed500, on order entry a rejectCode inside a 200noThe venue accepted and recorded the command, then could not finish it. Never resubmit unchanged: the venue is deterministic. The response reports what the command DID achieve before it stopped, so an orderId there is a real order; reconcile against it and GET /v1/orders/open, cancel what you did not want, then send a different request. Report the clOrdId to operations.

The full registry, including every enumerated string value the API returns and the WebSocket close codes, is on the Reference page.

Integration checklist

Acceptance tests worth writing before you go live: the signature vector matches; a far-from-market LIMIT GTC comes back NEW and cancels to CANCELED with USER_REQUESTED and your identifiers echoed; an identical resend returns duplicate: true and the SAME orderId; the same reference with a changed payload returns 2009; an invented extra field returns 2017 naming the field; MARKET plus FOK larger than visible depth returns a single cancel with FOK_UNFILLABLE and zero partial fills; an unknown symbol returns 2001 with your clOrdId echoed; and your account parser survives both the array shape and the uppercase MM key.