🔗
LP Gateway / Integration Guide
Mode:
Order Book Trading
00 — INTRO

Overview

One gateway. Two execution modes. Zero floating-point math.

In Order Book Trading (OBT) mode the gateway exposes a live WebSocket order book. Subscribe to a pair, read the top-of-book price, and fire orders directly against the book — all in the same connection..

New Instance Available A new OBT instance is ready for development . Endpoints and login session validation follow the same authentication flow as documented in the Authentication section.
📡

Live Book

Subscribe to real-time bid/ask snapshots for any supported pair.

Direct Orders

Send LIMIT or MARKET orders in-band over the same WebSocket.

💯

Precision

12-decimal strings end-to-end. No silent rounding, ever.

In Request for Quote (RFQ) mode you first lock a price with a quote request, then execute against it before the TTL expires. The REST API handles the full lifecycle; a WebSocket connection receives push events for order and quote updates.

New Instance Available A new RFQ instance is ready for development . Endpoints and login session validation follow the same authentication flow as documented in the Authentication section.
🔐

Price Lock

Quotes are locked for a client-specified window (up to 60 s).

📡

Push Events

WebSocket streams order fills and quote expiries as they happen.

💯

Precision

12-decimal strings end-to-end. No silent rounding, ever.

Important Always use decimal strings (e.g. "10000.000000000000") for monetary values. Parsing as float64 will silently corrupt your books.
01 — SETUP

Prerequisites

Get your credentials and generate an RSA key pair.

Contact your admin to request:

  • api_key_id — UUID, e.g. 6536b3d3-5a3b-4e1a-910e-49478b9efe20
  • RSA-2048 private key — used to sign challenges. Keep secret.

Generate a key pair with OpenSSL if you don't have one. Share the public key with your admin.

openssl genrsa -out client_private.pem 2048
openssl rsa -in client_private.pem \
  -pubout -out client_public.pem

# Share client_public.pem with your admin.
# Keep client_private.pem secret — never commit or log it.
02 — AUTH

Authentication

5-step challenge-response handshake + Diffie-Hellman key exchange.

POST /api/v1/login/attempt

Step 1 — Obtain a challenge

Returns a one-time challenge and DH parameters for deriving the shared session secret.

Request Body
api_key_idrequiredYour API key UUID.
Response Fields
FieldPurpose
session_idStore in memory for all subsequent requests.
challengeBase64-encoded bytes to sign with your RSA key.
dh_base, dh_modulusDH parameters (RFC 3526 group 14).
ttl_msChallenge validity in ms. Complete Step 4 before expiry.
curl -X POST http://localhost:8080/api/v1/login/attempt \
  -H "Content-Type: application/json" \
  -d '{
    "api_key_id": "6536b3d3-5a3b-4e1a-910e-49478b9efe20"
  }'
{
  "session_id": "049c1bf2-0fff-4267-93ac-e78bf6d7f613",
  "challenge":  "<base64>",
  "dh_base":    "<base64>",
  "dh_modulus": "<base64>",
  "ttl_ms": 30000
}
POST /api/v1/login/confirm

Steps 2–5 — Sign & Confirm

Step 2: Decode challenge (base64), sign SHA256(bytes) with RSA-2048 PKCS#1 v1.5.
Step 3: Generate a DH keypair using RFC 3526 group 14.
Step 4: POST signature + DH public key.
Step 5: Compute session_secret = server_dh_pub ^ client_priv mod dh_modulus.

Keep Secure Store session_id and session_secret in memory only. Never persist to disk or write to logs.
import base64, secrets, hashlib
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
import requests

API_KEY_ID = "6536b3d3-5a3b-4e1a-910e-49478b9efe20"

# Step 1
resp = requests.post("http://localhost:8080/api/v1/login/attempt",
    json={"api_key_id": API_KEY_ID})
data = resp.json()
session_id  = data["session_id"]
challenge   = data["challenge"]
dh_modulus  = data["dh_modulus"]

# Step 2 — Sign challenge
with open("client_private.pem", "rb") as f:
    private_key = serialization.load_pem_private_key(f.read(), password=None)
challenge_bytes = base64.b64decode(challenge)
signature = private_key.sign(challenge_bytes, padding.PKCS1v15(), hashes.SHA256())
signature_b64 = base64.b64encode(signature).decode()

# Step 3 — DH keypair
RFC3526_PRIME = int("FFFFFFFF" * 32, 16)  # group 14 (abbreviated)
priv_bytes  = secrets.token_bytes(256)
client_priv = int.from_bytes(priv_bytes, 'big') % (RFC3526_PRIME - 3) + 2
client_pub  = pow(2, client_priv, RFC3526_PRIME)
dh_key_b64  = base64.b64encode(client_pub.to_bytes(256, 'big')).decode()

# Step 4 — Confirm
resp = requests.post("http://localhost:8080/api/v1/login/confirm",
    json={"session_id": session_id,
          "signature":  signature_b64,
          "dh_key":     dh_key_b64})
confirm        = resp.json()
server_dh_pub  = int.from_bytes(base64.b64decode(confirm["dh_key"]), 'big', signed=True)

# Step 5 — Derive shared secret
session_secret = pow(server_dh_pub, client_priv, RFC3526_PRIME).to_bytes(256, 'big')
print(f"Session: {session_id}")
POST /api/v1/login/confirm
Content-Type: application/json

{
  "session_id": "049c1bf2-0fff-4267-93ac-e78bf6d7f613",
  "signature":  "<base64-rsa-sig>",
  "dh_key":     "<base64-dh-pub>"
}
{
  "dh_key":              "<server-base64-dh-pub>",
  "keepalive_timeout_ms": 300000
}

Signing every request

Attach four headers to every /v1/* call.

HeaderValue
X-Session-IdUUID from login/confirm
X-NonceUnique random string per request
X-TimestampUnix epoch ms as string
X-SignatureBASE64(HMAC-SHA384(canonical, session_secret))
Canonical Payload

UTF-8, concatenated without separators. Headers in this exact order: X-Nonce, X-Session-Id, X-Timestamp. Append request body for POST/PUT.

METHOD
+ urlPath
+ sortedQueryParams        # sorted ascending by lowercase key
+ "X-Nonce=...&X-Session-Id=...&X-Timestamp=..."
+ requestBody              # POST / PUT only

# Example — WebSocket upgrade (no query, no body):
CONNECT/v1/wsX-Nonce=abc&X-Session-Id=<uuid>&X-Timestamp=1700000000000
import hmac, hashlib, base64, uuid
from datetime import datetime

def sign_request(method, path, session_id, session_secret,
                 query="", body=""):
    nonce     = str(uuid.uuid4())
    timestamp = str(int(datetime.now().timestamp() * 1000))
    canonical = (method + path + query
                 + f"X-Nonce={nonce}&X-Session-Id={session_id}"
                 + f"&X-Timestamp={timestamp}" + body)
    sig = hmac.new(session_secret, canonical.encode(), hashlib.sha384).digest()
    return {
        "X-Session-Id": session_id,
        "X-Nonce":      nonce,
        "X-Timestamp":  timestamp,
        "X-Signature":  base64.b64encode(sig).decode()
    }
03 — SESSION

Session Lifecycle

EndpointPurpose
POST /api/v1/keepalive?session_id=<uuid>Reset inactivity timer. Call more frequently than keepalive_timeout_ms.
POST /api/v1/logout?session_id=<uuid>Destroy the session immediately.
04 — TRADING

Request for Quote

RFQ Lock a price before executing.

POST /v1/rfq

Request a quote

Returns a quote_id with a TTL. Execute via POST /v1/orders before expires_at.

Request Body
buyrequiredCurrency to buy, e.g. "USDT"
sellrequiredCurrency to sell, e.g. "USD"
amountrequiredDecimal string
referenced_unitrequiredCurrency the amount refers to — must match the value of buy or sell. Also accepted as referencedUnit.
quote_for_secondsrequiredQuote validity window in seconds (default: 30). Also accepted as quoteForSeconds.
tagsoptionalmap[string]string — client metadata, returned verbatim in all events
Response Fields
quote_idPass to POST /v1/orders to execute
referenced_amountThe amount as submitted
quote_amountThe opposing-side amount at the locked rate
expires_atISO-8601 — execute before this time
POST /v1/rfq
X-Session-Id: 049c1bf2-...
X-Nonce: abc123
X-Timestamp: 1700000000000
X-Signature: <hmac>

{
  "buy":               "USDT",
  "sell":              "USD",
  "amount":            "10000.000000000000",
  "referenced_unit":   "USD",
  "quote_for_seconds": 30
}
{
  "quote_id":          "<uuid>",
  "buy":               "USDT",
  "sell":              "USD",
  "referenced_amount": "10000.000000000000",
  "quote_amount":      "9150.000000000000",
  "expires_at":        "2024-01-01T00:00:30Z"
}
# Sell 10,000 USD, receive USDT
python lp_client.py rfq \
  --buy USDT \
  --sell USD \
  --amount 10000 \
  --unit USD \
  --seconds 30

# --unit must match the value of --buy or --sell
# --seconds defaults to 30 if omitted
05 — TRADING

Execute Order

RFQ Single endpoint, two modes. Quote-based references a prior RFQ; direct submits the whole order in one call.

POST /v1/orders

Submit an order

Always include an Idempotency-Key. If the response is lost, do not retry — poll GET /v1/orders/{orderId} using the same key.

Direct-mode Fields
>
client_order_idrequiredYour internal order ID. Max 64 chars
quote_idrequiredAsset pair, e.g. order ID. Max 64 chars.
Do not retry POST /v1/orders If the response is lost, poll GET /v1/orders/{orderId} using the same Idempotency-Key.
POST /v1/orders
Idempotency-Key: 550e8400-...
X-Session-Id: ...

{
  "quote_id":       "QT-12345678",
  "client_order_id": "my-order-001"
}
POST /v1/orders
Idempotency-Key: 550e8400-...
X-Session-Id: ...

{
  "instrument":      "USD/EUR",
  "side":            "BUY",
  "order_type":      "LIMIT",
  "time_in_force":   "FOK",
  "quantity":        "10000.000000000000",
  "price":           "0.920000000000",
  "client_order_id": "my-order-001"
}
{
  "order_id":        "f0e1d2c3-...",
  "client_order_id": "my-order-001",
  "status":          "FILLED",
  "buy":             "USDT",
  "sell":            "USD",
  "amount":          "10000.000000000000",
  "lp_order_id":     "",
  "created_at":      "2024-01-01T00:00:00Z",
  "filled_at":       "2024-01-01T00:00:01Z",
  "filled_qty":      "10000.000000000000",
  "avg_price":       "0.915000000000",
  "last_fill_qty":   "10000.000000000000",
  "last_fill_price": "0.915000000000"
}
04B — TRADING

Tags & Metadata

Attach client-defined metadata to orders without maintaining a separate mapping.

All POST endpoints (POST /v1/rfq and POST /v1/orders) accept an optional tags field—a flat map[string]string for client-defined metadata. Tags are stored with the request and returned as-is in all subsequent responses and WebSocket events for that resource.

No effect on execution Tags have no effect on gateway or LP behaviour. Use them to attach your own reference IDs, routing labels, or audit metadata without maintaining a separate mapping on your side. Any string key and string value are accepted; there is no schema enforcement.
POST /v1/rfq
X-Session-Id: ...

{
  "buy":              "USD",
  "sell":             "EUR",
  "amount":           "10000.000000000000",
  "referenced_unit":  "sell",
  "quote_for_seconds": 30,
  "tags": {
    "desk":   "fx-london",
    "source": "algo-001",
    "ref":    "TRD-99887766"
  }
}
{
  "quote_id": "...",
  "tags": {
    "desk":   "fx-london",
    "source": "algo-001",
    "ref":    "TRD-99887766"
  }
}
{
  "type": "order_update",
  "orderId": "f0e1d2c3-...",
  "tags": {
    "desk":   "fx-london",
    "source": "algo-001",
    "ref":    "TRD-99887766"
  }
}
06 — TRADING

List Orders

Paginated history with filters.

GET /v1/orders

Query order history

Query Parameters
fromRFC3339received_at >=
toRFC3339received_at <=
client_order_idstringExact match on your internal order ID
statusstringPENDING, OPEN, FILLED, REJECTED, CANCELLED
pageint1-based, default 1
limitintDefault 20, max 100
Mode differences In quote-based mode, filled orders return filledQty and lastFillQty (equal amount); avgPrice and lastFillPrice are absent. Streaming mode returns full execution detail.
GET /v1/orders?status=FILLED&limit=20
X-Session-Id: ...
X-Nonce: abc123
X-Timestamp: 1700000000000
X-Signature: ...
{
  "orders": [
    {
      "order_id":       "f0e1d2c3-...",
      "client_order_id": "my-order-001",
      "status":         "FILLED",
      "trade_id":       "TR-987654",
      "buy":            "USD",
      "sell":           "EUR",
      "amount":         "9150.000000000000",
      "filled_qty":     "9150.000000000000",
      "last_fill_qty":  "9150.000000000000"
    }
  ]
}
07 — TRADING

Get Order

Single-order detail. Safe to poll indefinitely.

GET /v1/orders/{orderId}

Retrieve one order

Returns full execution detail when filled, or errorCode/errorMessage when rejected.

{
  "order_id":       "f0e1d2c3-...",
  "client_order_id": "my-order-001",
  "status":         "FILLED",
  "trade_id":       "TR-987654",
  "buy":            "USD",
  "sell":           "EUR",
  "amount":         "9150.000000000000",
  "balances": {
    "USD": "59150.000000000000",
    "EUR": "40850.000000000000"
  },
  "filled_qty":     "9150.000000000000",
  "last_fill_qty":  "9150.000000000000"
}
{
  "order_id":      "f0e1d2c3-...",
  "status":        "REJECTED",
  "error_code":    "INSUFFICIENT_BALANCE",
  "error_message": "insufficient balance for USD"
}
07B — TRADING

Cancel Order

Streaming mode only. Returns 204 No Content on success.

DELETE /v1/orders/{orderId}

Cancel a resting order

Eligible statuses: RECEIVED, OPEN, PARTIALLY_FILLED. Terminal orders return 409.

Mode restriction Quote-based mode does not support resting orders. Cancelling returns 409 NOT_IMPLEMENTED.
DELETE /v1/orders/550e8400-...
X-Session-Id: ...
X-Nonce: abc123
X-Timestamp: 1700000000000
X-Signature: ...
HTTP/1.1 204 No Content
{
  "error": {
    "code":    "CONFLICT",
    "message": "order cannot be cancelled: unsupported in quote mode",
    "traceId": "<uuid>"
  }
}
08 — TRADING

Order Fills

Aggregated fill statistics for an order.

GET /v1/orders/{orderId}/fills

Get fill statistics for an order

Returns aggregated fill data and execution summary.

Fill Fields
order_iduuidThe order ID
filled_qtydecimalTotal quantity filled
avg_pricedecimalVolume-weighted average fill price
last_fill_qtydecimalQuantity in the last fill event
last_fill_pricedecimalPrice of the last fill
transacted_atRFC3339Timestamp of last fill execution
{
  "fills": [
    {
      "order_id": "",
      "filled_qty": "10000.000000000000",
      "avg_price": "0.915000000000",
      "last_fill_qty": "10000.000000000000",
      "last_fill_price": "0.915000000000",
      "transacted_at": "2024-01-01T00:00:01Z"
    }
  ]
}
09 — ACCOUNT

Markets

All tradable pairs with minimum trade sizes.

GET /v1/markets

List available markets

GET /v1/markets
X-Session-Id: ...
X-Nonce: abc123
X-Timestamp: 1700000000000
X-Signature: ...
{
  "markets": [
    { "pair": "USD/EUR", "minTradeSize": "100.00" },
    { "pair": "EUR/GBP", "minTradeSize": "100.00" },
    { "pair": "BTC/USD", "minTradeSize": "0.001" }
  ]
}
10 — WEBSOCKET

WebSocket Connection

OBT Connect with HMAC-SHA384 headers.

WS ws://localhost:8080/v1/ws

Upgrade to WebSocket

Use method CONNECT (not GET) when computing the canonical signature payload.

Canonical Payload

CONNECT/v1/wsX-Nonce=...&X-Session-Id=...&X-Timestamp=...

Max connections Up to 5 concurrent WebSocket connections per session.
import asyncio, websockets, uuid, hmac, hashlib, base64
from datetime import datetime

async def connect_ws(session_id, session_secret):
    nonce     = str(uuid.uuid4())
    timestamp = str(int(datetime.now().timestamp() * 1000))
    canonical = (f"CONNECT/v1/ws"
                 f"X-Nonce={nonce}&X-Session-Id={session_id}"
                 f"&X-Timestamp={timestamp}")
    sig = hmac.new(session_secret, canonical.encode(), hashlib.sha384).digest()
    headers = [
        ("X-Session-Id", session_id),
        ("X-Nonce",      nonce),
        ("X-Timestamp",  timestamp),
        ("X-Signature",  base64.b64encode(sig).decode())
    ]
    ws = await websockets.connect(
        "ws://localhost:8080/v1/ws",
        additional_headers=headers)
    print("✓ Connected")
    return ws
11 — WEBSOCKET

Subscribe to Market Data

OBT Get real-time bid/ask snapshots for a trading pair.

Send Subscribe Message

Request market snapshots with current bid and ask levels.

Message Fields
typerequiredMessage type: "subscribe"
channelrequiredSubscription channel (e.g., "USDC/USD")
sideoptionalOrder book side: "BOTH", "BID", "ASK" (default: "BOTH")
quantityoptionalFor RFQ snapshots, quantity needed (default: 0)
subscription_request_typeoptionalSnapshot type: "snapshot" or "rfqsnapshot" (default: "snapshot")
type_bookoptionalBook type: "SPOT" (default)
agg_bookoptionalAggregation level (default: 0)
market_depthoptionalOrderbook depth (default: 0)
import json

# Basic subscription
payload = {
  "type": "subscribe",
  "channel": "USDC/USD"
}

# Or with advanced options
payload = {
  "type": "subscribe",
  "channel": "USDC/USD",
  "side": "BOTH",
  "quantity": 0,
  "subscription_request_type": "snapshot",
  "type_book": "SPOT",
  "agg_book": 0,
  "market_depth": 0
}

await ws.send(json.dumps(payload))
{
  "type": "subscribe_ack",
  "channel": "USDC/USD",
  "code": 0,
  "message": "Subscribed"
}
{
  "type": "market_update",
  "channel": "BTC/USD",
  "subscription_id": "550e8400-e29b-41d4-a716-446655440000",
  "bids": [
    ["40000.000000000000", "0.500000000000"],
    ["39999.000000000000", "1.000000000000"],
    ["39998.000000000000", "2.000000000000"]
  ],
  "asks": [
    ["40100.000000000000", "0.750000000000"],
    ["40101.000000000000", "1.500000000000"],
    ["40102.000000000000", "3.000000000000"]
  ],
  "ttl": "3000",
  "timestamp": "2026-06-24T21:20:00Z"
}
11B — WEBSOCKET

Unsubscribe from Market Data

OBT Stop receiving market snapshots for a trading pair.

Send Unsubscribe Message

Unsubscribe from market updates for a specific trading pair.

Message Fields
typerequiredMessage type: "unsubscribe"
channelrequiredTrading pair (e.g., "USDC/USD")
subscription_idrequiredThe subscription_id from the market_update response
type_bookoptionalBook type: "SPOT" (default)
import json

payload = {
  "type": "unsubscribe",
  "channel": "USDC/USD",
  "subscription_id": "685810ae-e9f6-4ab6-b298-ffd7099412ff",
  "type_book": "SPOT"
}

await ws.send(json.dumps(payload))
{
  "type": "unsubscribe_ack",
  "channel": "BTC/USD",
  "subscription_id": "550e8400-e29b-41d4-a716-446655440000",
  "code": 0,
  "timestamp": "2026-06-24T21:21:00Z"
}
12 — WEBSOCKET

Place Order

OBT Execute a trade via WebSocket.

Send Order Message

Execute a trade and receive order acknowledgement + trade execution updates.

Message Fields
typerequired"order"
client_order_idrequiredYour unique order identifier (UUID). Use to track order status and match responses.
instrumentrequiredTrading pair (e.g., "BTC/USD")
siderequired"BUY" or "SELL"
order_typerequired"LIMIT" or "MARKET"
time_in_forcerequired"FOK", "IOC", "GTC"
quantityrequiredDecimal string with 12 decimal places (e.g., "100.000000000000")
pricerequiredDecimal string with 12 decimal places (e.g., "40000.000000000000")
Decimal Precision All monetary values (quantity, price) must be strings formatted with exactly 12 decimal places.
import json, uuid

payload = {
  "type": "order",
  "client_order_id": "5318575b-179c-4cdc-9bce-7acfcf9d5d91",
  "instrument": "BTC/USD",
  "side": "BUY",
  "order_type": "LIMIT",
  "time_in_force": "FOK",
  "quantity": "100.000000000000",
  "price": "40000.000000000000"
}

await ws.send(json.dumps(payload))
import json

# Market order - IOC recommended for better fill ratio
payload = {
  "type": "order",
  "client_order_id": "5318575b-179c-4cdc-9bce-7acfcf9d5d91",
  "instrument": "BTC/USD",
  "side": "BUY",
  "order_type": "MARKET",
  "time_in_force": "IOC",
  "quantity": "100.000000000000"
}

await ws.send(json.dumps(payload))
12B — WEBSOCKET

Cancel Order

Send Cancel Order Message

GTC Only Send a cancel request and receive either a cancel_order_ack (success) or cancel_order_reject (failure).

Request Fields
typerequired"cancel_order"
instrumentrequiredTrading pair (e.g., "BTC/USD")
client_order_idrequiredThe client_order_id from the original order request
Response Fields (cancel_order_ack — Success)
typestringAlways "cancel_order_ack"
client_order_idstringYour original client_order_id from the cancel request
codeintegerAlways 0 for success
messagestringStatus message (e.g., "Cancel request accepted")
instrumentstringTrading pair
tsmillisecondsUnix timestamp in milliseconds when cancel was accepted
Response Fields (cancel_order_reject — Failure)
typestringAlways "cancel_order_reject"
client_order_idstringYour original client_order_id from the cancel request
codeintegerHTTP error code (e.g., 400, 404)
statusstringAlways "REJECTED"
messagestringRejection reason (e.g., "Order is in final state: REJECTED")
instrumentstringTrading pair
tsmillisecondsUnix timestamp in milliseconds when rejection occurred
GTC Only Only orders placed with time_in_force: "GTC" can be cancelled. FOK and IOC orders execute immediately and cannot be cancelled.
import json

payload = {
  "type": "cancel_order",
  "instrument": "BTC/USD",
  "client_order_id": "5318575b-179c-4cdc-9bce-7acfcf9d5d91"
}

await ws.send(json.dumps(payload))
{
  "type": "cancel_order_reject",
  "client_order_id": "TRADER-CLI:1234567891245678912345678912345685412584125684264215842526542158412525252358256536042737",
  "code": 400,
  "status": "REJECTED",
  "message": "Order is in final state: REJECTED",
  "instrument": "USDT/USD",
  "ts": 1783920110082
}
12C — WEBSOCKET

Order Update Event

OBT Server-sent event when an order is placed or rejected.

order_update Event

The server sends an order_update event immediately after you place an order, indicating whether it was accepted (PENDING) or rejected. Match events using client_order_id from your request.

Event Fields
typestringAlways "order_update"
client_order_idstringYour original client_order_id from the place order request
statusstringPENDING (accepted, awaiting fills) or REJECTED (validation failed)
instrumentstringTrading pair (e.g., "BTC/USD")
sidestringBUY or SELL
order_typestringLIMIT or MARKET
quantitydecimalOrder quantity (as submitted)
pricedecimalLimit price (present for LIMIT orders)
timestampRFC3339When the order was received
{
  "type": "order_update",
  "client_order_id": "MONITOR:BACKEND-1783938574043",
  "order_id": "A350vnCOSDC",
  "status": "PENDING",
  "instrument": "USDT/AED",
  "side": "BUY",
  "order_type": "MARKET",
  "quantity": "100000000.000000000000",
  "cum_qty": "0.000000000000",
  "avg_price": "0.000000000000",
  "timestamp": 1783938574610
}
{
  "type": "order_update",
  "client_order_id": "MONITOR:BACKEND-1783938574043",
  "order_id": "A350vnCOSDC",
  "status": "PARTIALLY_FILLED",
  "instrument": "USDT/AED",
  "side": "BUY",
  "order_type": "MARKET",
  "quantity": "100000000.000000000000",
  "price": "3.671304820000",
  "cum_qty": "50000000.000000000000",
  "avg_price": "3.669470080000",
  "timestamp": 1783938574723
}
{
  "type": "order_update",
  "client_order_id": "MONITOR:BACKEND-1783938574043",
  "order_id": "A350vnCOSDC",
  "status": "FILLED",
  "instrument": "USDT/AED",
  "side": "BUY",
  "order_type": "MARKET",
  "quantity": "100000000.000000000000",
  "price": "3.671304820000",
  "cum_qty": "100000000.000000000000",
  "avg_price": "3.669470080000",
  "timestamp": 1783938574840
}
{
  "type": "order_update",
  "client_order_id": "TEST-IOC-1783943176881",
  "order_id": "A330vnA8bWr",
  "status": "CANCELLED",
  "instrument": "USDT/USD",
  "side": "BUY",
  "order_type": "LIMIT",
  "quantity": "1.012345670000",
  "price": "1.100000000000",
  "cum_qty": "1.000000000000",
  "avg_price": "0.999016670000",
  "timestamp": 1783943178030
}
{
  "type": "order_update",
  "client_order_id": "MONITOR:BACKEND-1788870170681",
  "order_id": "A330vtjd3LP",
  "status": "REJECTED",
  "instrument": "USDT/USD",
  "side": "SELL",
  "order_type": "MARKET",
  "quantity": "10000000000.000000000000",
  "price": "0.999633000000",
  "leave_qty": "10000000000.000000000000",
  "cum_qty": "0.000000000000",
  "avg_price": "0.000000000000",
  "message": "ORDER_REJECTED_SIZE Size above configured maximum",
  "timestamp": 1788870171532
}
12D — WEBSOCKET

Trade Update Event

OBT Server-sent event for fills and order state changes.

trade_update Event

The server sends a trade_update event for fill execution and for order cancellations.

Event Fields
typestringAlways "trade_update"
order_idstringLP-assigned order ID (assigned by execution system)
trade_idstringLP-assigned trade ID (assigned by execution system)
client_order_idstringYour original client_order_id
last_qtydecimalQuantity filled in THIS event only
last_pricedecimalPrice of THIS fill (may differ from limit price)
cum_qtydecimalCumulative quantity filled (running total)
avg_pricedecimalVolume-weighted average fill price
quantitydecimalOriginal order quantity
leave_qtydecimalRemaining quantity unfilled (0 if complete)
statusstringPENDING (partial fill), FILLED (complete), or CANCELLED
instrumentstringTrading pair
sidestringBUY or SELL
order_typestringLIMIT or MARKET
pricedecimalOriginal limit price
timestampRFC3339When this fill/cancellation occurred
{
  "type": "trade_update",
  "order_id": "A330vnA8bWr",
  "client_order_id": "TEST-IOC-1783943176881",
  "trade_id": "A33S10vnA8bij",
  "status": "PARTIALLY_FILLED",
  "timestamp": 1783943177907,
  "last_qty": "1.000000000000",
  "last_price": "0.999016670000",
  "cum_qty": "1.000000000000",
  "avg_price": "0.999016670000",
  "quantity": "1.012345670000",
  "leave_qty": "0.012345670000",
  "instrument": "USDT/USD",
  "side": "BUY",
  "order_type": "LIMIT",
  "price": "1.100000000000"
}
{
  "type": "trade_update",
  "order_id": "A350vmUxO05",
  "client_order_id": "5318575b-179c-4cdc-9bce-7acfcf9d5d91",
  "trade_id": "A350vmUxO05",
  "last_qty": "50.000000000000",
  "last_price": "40001.000000000000",
  "cum_qty": "100.000000000000",
  "avg_price": "40000.500000000000",
  "quantity": "100.000000000000",
  "leave_qty": "0.000000000000",
  "status": "FILLED",
  "instrument": "BTC/USD",
  "side": "BUY",
  "order_type": "LIMIT",
  "price": "40000.000000000000",
  "timestamp": "2026-06-24T21:25:10Z"
}
12E — WEBSOCKET

Error Response

OBT Server-sent event when an order is rejected or a request fails validation.

Error Response

The server sends an error response when an order request fails validation or cannot be accepted. Match errors using client_order_id from your request.

Response Fields
typestringAlways "error"
client_order_idstringYour original client_order_id from the request
instrumentstringTrading pair (e.g., "USDT/AED")
codeintegerNumeric error code (e.g., 50400, 50103)
messagestringHuman-readable error description
tsmillisecondsUnix timestamp in milliseconds when the error occurred
Error vs order_update An error event is sent for malformed or system-level request failures. An order_update with status: REJECTED is sent after the order passes syntax validation but fails business logic (e.g., insufficient balance). Both use client_order_id to link to your request.
{
  "type": "error",
  "client_order_id": "TRADER-CLI:1234567891245678912345678912345685412584125684264215842526542158412525252358256536042737",
  "instrument": "USDT/USD",
  "code": 50400,
  "message": "Invalid request",
  "ts": 1783920043240
}
{
  "type": "error",
  "client_order_id": "5318575b-179c-4cdc-9bce-7acfcf9d5d91",
  "instrument": "USDT/AED",
  "code": 50101,
  "message": "Invalid quantity format",
  "ts": 1783920043240
}
{
  "type": "error",
  "client_order_id": "MONITOR:BACKEND-1783938574043",
  "instrument": "USDT/AED",
  "code": 50103,
  "message": "Insufficient balance for USDT",
  "ts": 1783938574043
}
13 — REST API

List Orders

OBT Query order history with filters (REST endpoint).

GET /v1/orders

Query order history

REST API This is a synchronous REST endpoint (not WebSocket). Requires HMAC-SHA384 authentication headers.
Query Parameters
fromRFC3339created_at >=
toRFC3339created_at <=
client_order_idstringExact match on your internal order ID
statusstringPENDING, OPEN, FILLED, PARTIALLY FILLED, REJECTED, CANCELLED
pageint1-based, default 1
limitintDefault 20, max 100
GET /v1/orders?status=FILLED&limit=20
X-Session-Id: ...
X-Nonce: abc123
X-Timestamp: 1700000000000
X-Signature: ...
{
  "orders": [
    {
      "order_id": "a1b2c3d4-e5f6-47g8-h9i0-j1k2l3m4n5o6",
      "client_order_id": "order-001",
      "status": "FILLED",
      "instrument": "USDC/USD",
      "side": "BUY",
      "order_type": "LIMIT",
      "quantity": "1000.000000000000",
      "price": "0.999900000000",
      "leave_qty": "0.000000000000",
      "buy": "1000",
      "sell": "999.90",
      "amount": "999.900000000000",
      "lp_order_id": "A350vmUxO05",
      "created_at": "2026-06-25T08:40:00Z",
      "filled_at": "2026-06-25T08:40:02Z",
      "cum_qty": "1000.000000000000",
      "last_qty": "1000.000000000000",
      "last_price": "0.999700000000",
      "avg_price": "0.999700000000"
    },
    {
      "order_id": "c3d4e5f6-g7h8-49i0-j1k2-l3m4n5o6p7q8",
      "client_order_id": "order-002",
      "status": "OPEN",
      "instrument": "BTC/USDT",
      "side": "BUY",
      "order_type": "LIMIT",
      "quantity": "0.250000000000",
      "price": "42000.000000000000",
      "leave_qty": "0.250000000000",
      "lp_order_id": "B401wnVyP16",
      "created_at": "2026-06-25T08:43:15Z"
    }
  ],
  "total_elements": 2,
  "page": 1,
  "limit": 20
}
13B — REST API

Get Order

OBT Single-order detail by ID (REST endpoint).

GET /v1/orders/{orderId}

Retrieve one order

Returns full order detail when filled, or error details when rejected. Safe to poll indefinitely.

REST API This is a synchronous REST endpoint (not WebSocket). Requires HMAC-SHA384 authentication headers.
{
  "order_id": "a1b2c3d4-e5f6-47g8-h9i0-j1k2l3m4n5o6",
  "client_order_id": "order-001",
  "status": "FILLED",
  "instrument": "USDC/USD",
  "side": "BUY",
  "order_type": "LIMIT",
  "quantity": "1000.000000000000",
  "price": "0.999900000000",
  "leave_qty": "0.000000000000",
  "buy": "1000",
  "sell": "999.90",
  "amount": "999.900000000000",
  "lp_order_id": "A350vmUxO05",
  "created_at": "2026-06-25T08:40:00Z",
  "filled_at": "2026-06-25T08:40:02Z",
  "cum_qty": "1000.000000000000",
  "last_qty": "1000.000000000000",
  "last_price": "0.999700000000",
  "avg_price": "0.999700000000"
}
{
  "order_id": "c3d4e5f6-g7h8-49i0-j1k2-l3m4n5o6p7q8",
  "client_order_id": "order-002",
  "status": "OPEN",
  "instrument": "BTC/USDT",
  "side": "BUY",
  "order_type": "LIMIT",
  "quantity": "0.250000000000",
  "price": "42000.000000000000",
  "leave_qty": "0.250000000000",
  "lp_order_id": "B401wnVyP16",
  "created_at": "2026-06-25T08:43:15Z"
}
13D — REST API

Order Fills

OBT Aggregated fill statistics for an order (REST endpoint).

GET /v1/orders/{orderId}/fills

Get fill statistics for an order

Returns aggregated fill data and execution summary.

REST API This is a synchronous REST endpoint (not WebSocket). Requires HMAC-SHA384 authentication headers.
Fill Fields
order_idstringLP-assigned order ID
trade_idstringLP-assigned trade ID (unique per fill event)
instrumentstringTrading pair
sidestringBUY or SELL
order_typestringLIMIT or MARKET
quantitydecimalOriginal order quantity
filled_qtydecimalCumulative quantity filled (running total)
avg_pricedecimalVolume-weighted average fill price
last_fill_qtydecimalQuantity filled in this event only
last_fill_pricedecimalPrice of this fill event
transacted_atmillisecondsUnix timestamp of fill execution in milliseconds
[
  {
    "order_id": "A350vnCOSDC",
    "trade_id": "A35S10vnCOS52",
    "instrument": "USDT/AED",
    "side": "BUY",
    "order_type": "MARKET",
    "quantity": "100000000.000000000000",
    "filled_qty": "100000000.000000000000",
    "avg_price": "3.669470080000",
    "last_fill_qty": "50000000.000000000000",
    "last_fill_price": "3.669470080000",
    "transacted_at": 1783938574390
  },
  {
    "order_id": "A350vnCOSDC",
    "trade_id": "A35S10vnCOS51",
    "instrument": "USDT/AED",
    "side": "BUY",
    "order_type": "MARKET",
    "quantity": "100000000.000000000000",
    "filled_qty": "50000000.000000000000",
    "avg_price": "3.669470080000",
    "last_fill_qty": "50000000.000000000000",
    "last_fill_price": "3.669470080000",
    "transacted_at": 1783938574341
  }
]
14 — ACCOUNT

Balances

GET /v1/balances

Account holdings

{
  "balances": {
    "USD":  "100000.000000000000",
    "EUR":  "50000.000000000000",
    "USDC": "25000.000000000000"
  }
}
15 — MONITORING

Health Check

Always 200. Use for load balancer probes.

GET /healthz

Service liveness

No authentication required.

HTTP/1.1 200 OK
Content-Type: text/plain

OK
16 — MONITORING

Readiness Check

Returns 503 if the LP circuit breaker is open.

GET /readyz

Service readiness

HTTP/1.1 200 OK

OK
HTTP/1.1 503 Service Unavailable

LP unavailable
17 — MONITORING

Prometheus Metrics

GET /metrics

Export metrics

No authentication required. Includes request latency, error rates, queue depth, and circuit breaker state.

# HELP gateway_http_requests_total Total HTTP requests
# TYPE gateway_http_requests_total counter
gateway_http_requests_total{route="/v1/orders",status="200"} 1250

# HELP gateway_http_request_duration_seconds Latency histogram
# TYPE gateway_http_request_duration_seconds histogram
gateway_http_request_duration_seconds_bucket{le="0.050"} 1100

# HELP gateway_response_queue_depth Queue depth
# TYPE gateway_response_queue_depth gauge
gateway_response_queue_depth 12
18 — REFERENCE

Time in Force

Execution behaviour after order submission.

TIFBehaviour
FOK
Fill or Kill
Limit: fill entirely at requested price or better, or reject. Market: fill entirely at TOB, or cancel — no partial fills.
IOC
Immediate or Cancel
Limit: fill any available qty at price or better; cancel remainder. Market: fill any available qty at TOB (partial fills allowed); cancel unfilled. Recommended for market orders.
GTC
Good till Cancelled
Limit: fill any available qty at price or better; remainder stays active until manually cancelled or fully filled. Market: fill any available qty at TOB (partial fills allowed); remainder stays active until manually cancelled or fully filled.
19 — REFERENCE

Supported Trading Pairs

PairStatusMin Trade
USDC/USD✅ Active1000.00
USDT/USD✅ Active1000.00
BTC/USD ✅ Active0.001
BTC/USDT✅ Active0.001
BTC/AED ✅ Active0.001
USDT/AED✅ Active1000.00
20 — REFERENCE

Error Codes

OBT Numeric error codes returned in error events and rejection messages.

CodeMessageCategory
50201Invalid market data messageMarket Data
50202Unknown symbolMarket Data
50203Unsupported subscription typeMarket Data
50204Unsupported type bookMarket Data
50205Duplicated market data request idMarket Data
50206Duplicate market data symbolMarket Data
50250Invalid market data unsubscribe messageSubscription
50251Invalid subscription idSubscription
50300Invalid new order single messageOrder
50301Unsupported sideOrder
50302Invalid quantityOrder
50303Invalid order typeOrder
50304Invalid time in forceOrder
50305Missing time in forceOrder
50306Invalid priceOrder
50307Order throttling exceededOrder
50308Invalid time stampOrder
50400Invalid requestRequest
{
  "type": "error",
  "client_order_id": "5318575b-179c-4cdc-9bce-7acfcf9d5d91",
  "instrument": "BTC/USD",
  "code": 50302,
  "message": "Invalid quantity",
  "ts": 1783920043240
}
20B — REFERENCE

Troubleshooting

Common errors and fixes when using the Python CLI or REST API.

ErrorCauseFix
No active sessionNot logged in or session file missingRun login
HTTP 401 UNAUTHORIZEDSession expired (5 min inactivity)Re-run login
HTTP 400 BAD_REQUESTMissing or invalid fieldRun python lp_client.py <cmd> --help
HTTP 404 NOT_FOUNDUnknown order or quote IDCheck the ID
HTTP 410 QUOTE_EXPIREDQuote TTL elapsed before executionRe-run rfq
HTTP 503 LP_UNAVAILABLELP circuit breaker openRetry after 30 seconds
21 — REFERENCE

Expected Latencies

p50 and p99 from client perspective.

Operationp50p99
Authentication (5 steps)100 ms200 ms
Market snapshot20 ms50 ms
Order placement ACK50 ms200 ms
Order fill200 ms500 ms