# STX Prediction API Documentation Full text of every page. Source: https://docs.stxapp.io --- # Request signing Source: https://docs.stxapp.io/api/authentication/ There are two ways to authenticate: an API key, described on this page, for your own account, and [OAuth](/oauth/) for apps that act for other STX members. Every request to STX is signed. There is no login call, no session, and no token to refresh: you hold an Ed25519 private key, and each request carries a signature proving it came from you. The signature covers the timestamp, the HTTP method and the path. That buys three things: - **Your key never crosses the wire.** Only a signature does, so nothing in a request lets someone else make one. - **A captured request cannot be replayed.** The timestamp is part of what you sign, and must be within 30 seconds of our clock. - **A captured request cannot be repointed.** The method and path are signed too, so a `GET` cannot be replayed as a `DELETE`. One key authenticates both surfaces: the REST API and the account WebSocket channels. Any language with Ed25519 works; the implementations below all produce identical output. ## The scheme Every authenticated request carries three headers: | Header | Value | | --- | --- | | `X-STX-ACCESS-KEY` | your Key ID, from Account -> API Keys | | `X-STX-ACCESS-TIMESTAMP` | current Unix time in **milliseconds**, as a decimal string | | `X-STX-ACCESS-SIGNATURE` | base64 Ed25519 signature of the message below | The message is three values concatenated with **no separator**: ``` message = timestamp_ms + HTTP_METHOD_UPPERCASE + request_path ``` For a REST call, the path is the endpoint you are calling, including any query string: ``` 1700000000000POST/api/v1/me ``` Rules that matter: - **The request body is not signed.** Only timestamp, method and path. - **The path includes its query string** when there is one, and never the scheme or host. `/api/v1/orders?status=open`, not `https://host/api/v1/orders?status=open`. - **Plain Ed25519, not `Ed25519ph`.** Sign the UTF-8 bytes of the message directly. The pre-hashed variant is one word away in several libraries and produces a well-formed signature that always fails, and the only symptom is a `401` indistinguishable from a wrong key. - **Standard base64** with padding. Not URL-safe base64. - **±30 seconds.** The timestamp must be within 30 seconds of our clock, so generate it per request and keep your host on NTP. Do not cache or reuse a signature. The WebSocket handshake signs the same way, against `GET` and the handshake path with the query string dropped: ``` 1700000000000GET/socket/websocket ``` even though you connect to `/socket/websocket?vsn=2.0.0`. See [WebSocket channels](/websockets/). ## Identify your client Send a `User-Agent` naming your software, on REST calls and on the WebSocket handshake: ``` / () ``` For example `acme-mm/1.4 (python/3.13)`. Our own clients follow the same shape: the C# SDK sends `STX.Sdk/1.2.3-net8.0`, and the [examples](https://github.com/stxapp/stx-api-examples) send `stx-api-examples/1.0 (python/3.13.7)`. **A `User-Agent` header is required.** A REST request or WebSocket handshake without one is refused with `403`. Most HTTP libraries send one by default, but some WebSocket clients, such as Node's `ws`, do not unless you set it. Any value is accepted, and one you choose is recorded against your API key, so a recognizable string is what lets us find your calls when you report something, instead of picking your traffic out of every default `python-requests/2.x` in the logs. Name the software rather than the HTTP library, and change the version when you deploy. ## Check your implementation Ed25519 signatures are deterministic, so the same key and message always produce the same signature. Check your implementation against this before doing anything else. This key exists only for testing. It is not registered anywhere and will never authenticate a real request. Do not use it beyond verifying your code. ``` -----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4f -----END PRIVATE KEY----- ``` | | | | --- | --- | | Message | `1700000000000POST/api/v1/me` | | Signature | `ZFJ0qEoHt8TLKbGP+UhJ77BNy/Cdf7+oqbQzwynaM5XM0Yphmq5t1YWumC+5pLaDk/xY9EG3h4brxVdodYloCA==` | Byte-identical output means your key loading, message construction, signing mode and base64 encoding are all correct, and any later `unauthorized` is a timestamp, header or key-id problem rather than a crypto one. ## Implementations Each of these was run against the vector above and produced exactly that signature. ### Python ```python import base64, time from cryptography.hazmat.primitives import serialization with open("test_key.pem", "rb") as fh: key = serialization.load_pem_private_key(fh.read(), password=None) timestamp = str(int(time.time() * 1000)) message = f"{timestamp}POST/api/v1/me".encode("utf-8") signature = base64.b64encode(key.sign(message)).decode() ``` `pip install cryptography`. Full example: [python/stx_quickstart.py](https://github.com/stxapp/stx-api-examples/blob/main/python/stx_quickstart.py). ### Node.js ```javascript import { createPrivateKey, sign } from 'crypto'; import { readFileSync } from 'fs'; const key = createPrivateKey(readFileSync('test_key.pem')); const timestamp = Date.now().toString(); const message = Buffer.from(`${timestamp}POST/api/v1/me`, 'utf8'); const signature = sign(null, message, key).toString('base64'); ``` No dependencies: Ed25519 is in the standard `crypto` module. Passing `null` as the first argument to `sign` selects pure Ed25519. ### Go ```go import ( "crypto/ed25519" "crypto/x509" "encoding/base64" "encoding/pem" "fmt" "os" "time" ) data, _ := os.ReadFile("test_key.pem") block, _ := pem.Decode(data) parsed, _ := x509.ParsePKCS8PrivateKey(block.Bytes) key := parsed.(ed25519.PrivateKey) timestamp := fmt.Sprintf("%d", time.Now().UnixMilli()) message := []byte(timestamp + "POST/api/v1/me") signature := base64.StdEncoding.EncodeToString(ed25519.Sign(key, message)) ``` Standard library only. ### Java ```java import java.nio.file.*; import java.security.*; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Base64; String pem = Files.readString(Path.of("test_key.pem")) .replaceAll("-----[A-Z ]+-----", "").replaceAll("\\s", ""); PrivateKey key = KeyFactory.getInstance("Ed25519") .generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(pem))); String timestamp = String.valueOf(System.currentTimeMillis()); Signature signer = Signature.getInstance("Ed25519"); signer.initSign(key); signer.update((timestamp + "POST/api/v1/me").getBytes("UTF-8")); String signature = Base64.getEncoder().encodeToString(signer.sign()); ``` Java 15 or newer, no dependencies. ### Shell Useful for a one-off curl or for filling in Postman variables: ```bash TS=$(python3 -c 'import time; print(int(time.time()*1000))') SIG=$(printf "%sGET/api/v1/me" "$TS" \ | openssl pkeyutl -sign -inkey ~/.stx/ontario-staging.pem -rawin \ | openssl base64 -A) curl -s https://demo.stxapp.ca/api/v1/me \ -H "X-STX-ACCESS-KEY: $STX_KEY_ID" \ -H "X-STX-ACCESS-TIMESTAMP: $TS" \ -H "X-STX-ACCESS-SIGNATURE: $SIG" ``` A POST is signed exactly the same way. The body is **not** part of the signed message (only the timestamp, method and path are), so it is sent unsigned alongside the three headers: ```bash TS=$(python3 -c 'import time; print(int(time.time()*1000))') SIG=$(printf "%sPOST/api/v1/tnc/accept" "$TS" \ | openssl pkeyutl -sign -inkey ~/.stx/ontario-staging.pem -rawin \ | openssl base64 -A) curl -s -X POST https://demo.stxapp.ca/api/v1/tnc/accept \ -H "X-STX-ACCESS-KEY: $STX_KEY_ID" \ -H "X-STX-ACCESS-TIMESTAMP: $TS" \ -H "X-STX-ACCESS-SIGNATURE: $SIG" \ -H "Content-Type: application/json" \ -d '{ "device_id": "my-device-001", "accept_terms": true, "accept_privacy": true, "accept_house_rules": true }' ``` All three consents must be `true`; anything else is a `400`. This endpoint needs a `read_write` key. `-rawin` is what selects pure Ed25519. OpenSSL 1.1.1 or newer. ## Generating your own key Either let us generate the pair when you create the key, or bring your own and register the public half: ```bash openssl genpkey -algorithm ed25519 -out ~/.stx/ontario-staging.pem openssl pkey -in ~/.stx/ontario-staging.pem -pubout ``` The second command prints the SPKI PEM public key to paste when creating the API key. The private key never leaves your machine, and we cannot recover it. ## When a signature will not verify Work through these in order. The server deliberately returns the same `unauthorized` for every signature failure, so the cause has to come from your side. 1. Is the method uppercase in the message? 2. Does the path match exactly, including any query string, and exclude scheme and host? 3. On the WebSocket: did you sign `GET` and `/socket/websocket` **without** `?vsn=2.0.0`, and use the `X-` prefixed header names? 4. Is the base64 standard and padded, rather than URL-safe? 5. Are you signing the message bytes rather than a hash of them? 6. Is the timestamp in the message byte-for-byte the one in the header? 7. Is your clock within 30 seconds of ours? `curl -sI https://demo.stxapp.ca | grep -i date` If the test vector above reproduces exactly and a real request still fails, the problem is the Key ID, the key's status, or the clock, not the signing. ## Still stuck? Ask in Discord: **https://discord.gg/yF9eVzPzNZ**. Include the operation or channel name, the environment, and the exact error text. [Support](/support/) lists what helps us answer in one round trip. --- # REST API reference > Trade on the STX exchange over HTTP. Source: https://docs.stxapp.io/api/rest/ Every endpoint is signed with your Ed25519 API key. See [Authentication](/api/authentication/) for the scheme. - [Account](/api/rest/account/): Your balance and fee summary, your open positions and your exposure per market: liability, projected fees and worst-case loss. - [Events](/api/rest/events/): The events markets belong to. - [Fills](/api/rest/fills/): Your own executions. - [Identity](/api/rest/identity/): Who your key belongs to. - [Leaderboard](/api/rest/leaderboard/): Ranked members per period (day, week, month, year, all time, on America/New_York days) and per sport, on up to eight boards: volume, profit, contracts settled, markets settled, win rate, biggest win, return and winning streak. - [Markets](/api/rest/markets/): Find something to trade. - [Orders](/api/rest/orders/): Place, inspect and cancel orders, individually or in batches. - [Portfolio](/api/rest/portfolio/): Money that has already moved: settlements, deposits, withdrawals, fees, adjustments and loyalty entries. --- # Account > Your balance and fee summary, your open positions and your exposure per market: liability, projected fees and worst-case loss. Source: https://docs.stxapp.io/api/rest/account/ Your balance and fee summary (`GET /api/v1/account/balance`, the REST twin of the `balances:{user_id}` channel), your open positions (`GET /api/v1/positions`, the REST twin of the `positions:{user_id}` channel) and your exposure per market: liability, projected fees and worst-case loss. Also terms acceptance. 4 endpoints. - [Get account balance](/api/rest/account/get-account-balance/) - [Get account market stats](/api/rest/account/get-market-stats/) - [List open positions](/api/rest/account/list-open-positions/) - [Accept terms and conditions](/api/rest/account/accept-terms/) --- # Accept terms and conditions > Record acceptance of the currently-in-effect terms and privacy version for this account. Source: https://docs.stxapp.io/api/rest/account/accept-terms/ Record acceptance of the currently-in-effect terms and privacy version for this account. All three consents must be true or the request is a 400. Requires a `read_write` key. ```http POST /api/v1/tnc/accept ``` Send it with your own demo key: [Try it](/quick-start/?op=tnc_accept_post#try-it). :::tip[In the SDKs] - TypeScript: [`STX.acceptTerms()`](/sdks/typescript/reference/stx/#acceptterms) - Python: [`STX.accept_terms()`](/sdks/python/reference/stx/#accept_terms) - C#: [`STXTermsAndConditionsService.AcceptTermsAndConditionsAsync()`](/sdks/csharp/reference/account/#stxtermsandconditionsservice) ::: ## Request body | Field | Type | Required | Description | |---|---|---|---| | `accept_house_rules` | boolean,null | no | Acceptance of the house rules. Defaults to `accept_terms AND accept_privacy` when omitted. | | `accept_privacy` | boolean | **yes** | Acceptance of the privacy policy. Must be true. | | `accept_terms` | boolean | **yes** | Acceptance of the terms of use. Must be true. | | `device_id` | string | **yes** | Identifier for the device recording the acceptance. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `400` | A parameter was missing or invalid. | Error | | `401` | Missing, malformed or unrecognized signature, or a timestamp outside the 30-second window. Body: {"error":"Missing or invalid API key credentials"} | Error | | `403` | This API key does not have write access. The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | | `422` | The acceptance was not recorded: no terms are in effect, the terms changed since you read them, or a consent value was refused. The body's `error` says why. | Error | ## Example Request: ```bash curl --request POST \ --url 'https://demo.stxapp.io/api/v1/tnc/accept' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' \ --header 'Content-Type: application/json' \ --data '{ "accept_house_rules": true, "accept_privacy": true, "accept_terms": true, "device_id": "my-device-001" }' ``` Response `200`: ```json { "message": "string" } ``` ### Response fields | Field | Type | Description | |---|---|---| | `message` | string | Always `"Success"` when the acceptance was recorded. | --- # Get account balance > Your balance and fee summary as a point-in-time snapshot, the same object the balances:{user_id} WebSocket channel publishes, so a REST read and a channel update delta are interchangeable. Source: https://docs.stxapp.io/api/rest/account/get-account-balance/ Your balance and fee summary as a point-in-time snapshot, the same object the `balances:{user_id}` WebSocket channel publishes, so a REST read and a channel `update` delta are interchangeable. `available_balance` is rounded down to the cent and the three liabilities up, in both signs, so they need not reconcile to the cent; treat each as authoritative on its own. ```http GET /api/v1/account/balance ``` Send it with your own demo key: [Try it](/quick-start/?op=account_balance_get#try-it). ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `401` | Missing, malformed or unrecognized signature, or a timestamp outside the 30-second window. Body: {"error":"Missing or invalid API key credentials"} | Error | | `403` | The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | ## Example Request: ```bash curl --request GET \ --url 'https://demo.stxapp.io/api/v1/account/balance' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' ``` Response `200`: ```json { "balance": { "account_balance": "0.6700", "account_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "available_balance": "0.6700", "base_fee_percent": null, "buy_order_liability": "0.6700", "escrow": "0.6700", "fee_schedule": "fixed_percent", "loyalty_tier": "rookie", "maker_factor": null, "points": null, "position_premium_liability": "0.6700", "sell_order_liability": "0.6700", "taker_factor": null, "total_adjustments": "0.6700", "total_deposits": "0.6700", "total_fees": "0.6700", "total_settlement_pnl": "0.6700", "total_trade_count": 0, "total_traded": "0.6700", "total_withdrawals": "0.6700", "user_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d" } } ``` ### Response fields | Field | Type | Description | |---|---|---| | `account_balance` | decimal | The account's cash balance. Unaffected by placing an order, but not all of it may be available. In dollars. | | `account_id` | uuid | The account this balance is for. | | `available_balance` | decimal | The balance available to withdraw or place further orders with. Rounded **down** to the cent, so it never overstates what is spendable. In dollars. | | `base_fee_percent` | number | The fee percentage on the `fixed_percent` schedule; `null` for any other schedule. A number, not a money string. | | `buy_order_liability` | decimal | Total liability from buy orders, including the potential trade-fee reserve. Rounded **up** to the cent. In dollars. | | `escrow` | decimal | The account's escrow balance. In dollars. | | `fee_schedule` | string | The account's fee schedule. | | `loyalty_tier` | string | The account's loyalty tier. Every account has one; new accounts start at `rookie`. | | `maker_factor` | number | Per-trade maker fee factor, present only on the `on_trade` and `loyalty_tier_on_trade` schedules; `null` otherwise. A number, not a money string. | | `points` | integer | Loyalty points the account has accumulated. A whole number, not a money string. | | `position_premium_liability` | decimal | Total liability from position premiums. Routinely negative. Rounded **up** to the cent in both signs. In dollars. | | `sell_order_liability` | decimal | Total liability from sell orders, including the potential trade-fee reserve. Rounded **up** to the cent. In dollars. | | `taker_factor` | number | Per-trade taker fee factor, present only on the `on_trade` and `loyalty_tier_on_trade` schedules; `null` otherwise. A number, not a money string. | | `total_adjustments` | decimal | Lifetime total balance adjustments. In dollars. | | `total_deposits` | decimal | Lifetime total deposits. In dollars. | | `total_fees` | decimal | Total fees from all settlements and other fees. In dollars. | | `total_settlement_pnl` | decimal | Total gross profit and loss from all settlements. In dollars. | | `total_trade_count` | integer | The number of trades the account has made across every market. | | `total_traded` | decimal | The risk the account has committed across every market. In dollars. | | `total_withdrawals` | decimal | Lifetime total withdrawals. In dollars. | | `user_id` | uuid | The user that owns the account. | --- # Get account market stats > Your exposure and realized result per market: liability, projected fees and worst-case loss. Source: https://docs.stxapp.io/api/rest/account/get-market-stats/ Your exposure and realized result per market: liability, projected fees and worst-case loss. `status`, `title` and `event_start_time` come from the live market index and are `null` for a market no longer held there. ```http GET /api/v1/account/market_stats ``` Send it with your own demo key: [Try it](/quick-start/?op=account_market_stats_get#try-it). :::tip[In the SDKs] - TypeScript: [`STX.accountMarketStats()`](/sdks/typescript/reference/stx/#accountmarketstats) - Python: [`STX.account_market_stats()`](/sdks/python/reference/stx/#account_market_stats) ::: ## Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `market_ids` | `query` | string | no | Comma-separated market UUIDs. | | `event_ids` | `query` | string | no | Comma-separated event UUIDs. | | `exclude_zero_settlements` | `query` | boolean | no | Omit markets whose settlement count is zero. An unrecognized value is **silently ignored** rather than rejected: the filter is dropped. | | `from_time` | `query` | integer | no | Inclusive lower bound on `last_settled_at`, as UNIX microseconds. | | `to_time` | `query` | integer | no | Inclusive upper bound on `last_settled_at`, as UNIX microseconds. | | `sports` | `query` | string | no | Comma-separated sports, case-insensitive. | | `competitions` | `query` | string | no | Comma-separated competitions, case-insensitive. | | `limit` | `query` | integer | no | Rows per page. Defaults to 100 and is silently clamped to 200; a larger value is not an error. | | `cursor` | `query` | string | no | Cursor from the previous response. Omit for the first page. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `400` | A parameter was missing or invalid. | Error | | `401` | Missing, malformed or unrecognized signature, or a timestamp outside the 30-second window. Body: {"error":"Missing or invalid API key credentials"} | Error | | `403` | The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | ## Example Request: ```bash curl --request GET \ --url 'https://demo.stxapp.io/api/v1/account/market_stats' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' ``` Response `200`: ```json { "cursor": null, "market_stats": [ { "account_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "archived_at": null, "available_position": "2.00", "average_open_premium": "0.6700", "buy_contracts_closed": "2.00", "buy_contracts_expired": "2.00", "buy_contracts_settled": "2.00", "buy_contracts_traded": "2.00", "buy_order_liability": "0.6700", "buy_orders": 0, "buy_original_risk": "0.6700", "buy_settlements": 0, "buy_trade_count": 0, "cancelled_buy_contracts": "2.00", "cancelled_buy_orders": 0, "cancelled_contracts": "2.00", "cancelled_orders": 0, "cancelled_sell_contracts": "2.00", "cancelled_sell_orders": 0, "closed_buy_premium": "0.6700", "closed_fees": "0.6700", "closed_gross_pnl": "0.6700", "closed_net_pnl": "0.6700", "closed_premium": "0.6700", "closed_sell_premium": "0.6700", "competition": "string", "contracts_closed": "2.00", "contracts_expired": "2.00", "contracts_in_buy_orders": "2.00", "contracts_in_orders": "2.00", "contracts_in_sell_orders": "2.00", "contracts_settled": "2.00", "contracts_traded": "2.00", "event_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "event_start_time": null, "expired_buy_premium": "0.6700", "expired_fees": "0.6700", "expired_gross_pnl": "0.6700", "expired_net_pnl": "0.6700", "expired_premium": "0.6700", "expired_sell_premium": "0.6700", "fee_plugin": null, "inserted_at": 0, "last_settled_at": null, "market_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "market_max_price": "1.0000", "max_liability_loss": "0.6700", "max_liability_win": "0.6700", "max_potential_fee": "0.6700", "max_potential_profit": "0.6700", "max_risk": "0.6700", "needs_rebuild": false, "open_order_count": 0, "open_potential_fee": "0.6700", "open_potential_profit": "0.6700", "open_premium": "0.6700", "open_risk": "0.6700", "open_trade_count": 0, "order_liability": "0.6700", "orders": 0, "original_risk": "0.6700", "pending_close_fee": "0.6700", "pending_close_pnl": "0.6700", "position": "2.00", "position_accountability_alert_id": null, "position_premium_liability": "0.6700", "potential_order_trade_fee": "0.6700", "rejected_buy_orders": 0, "rejected_orders": 0, "rejected_sell_orders": 0, "sell_contracts_closed": "2.00", "sell_contracts_expired": "2.00", "sell_contracts_settled": "2.00", "sell_contracts_traded": "2.00", "sell_order_liability": "0.6700", "sell_orders": 0, "sell_original_risk": "0.6700", "sell_settlements": 0, "sell_trade_count": 0, "settled_at": null, "settlements": 0, "sport": "string", "status": null, "title": null, "total_fees": "0.6700", "total_liability": "0.6700", "total_net_pnl": "0.6700", "total_settlement_pnl": "0.6700", "trade_count": 0, "updated_at": 0 } ] } ``` ### Response fields | Field | Type | Description | |---|---|---| | `cursor` | string | Opaque cursor for the next page. Pass the value from the previous response; omit for the first page. `null` once there are no more pages. | | `account_id` | uuid | The account the record belongs to. | | `archived_at` | int64 | When the record was marked as archived. `null` until the market is archived. UNIX microseconds. | | `available_position` | decimal | The amount of the `position` that is not reserved for pending orders. In contracts. | | `average_open_premium` | decimal | The average premium of all open contracts. In dollars. | | `buy_contracts_closed` | decimal | The total number of buy contracts that were closed. | | `buy_contracts_expired` | decimal | The total number of buy contracts that were expired. | | `buy_contracts_settled` | decimal | The total number of buy contracts settled in the position. | | `buy_contracts_traded` | decimal | The total number of buy contracts the account has traded. Only opening trade's contracts are included. | | `buy_order_liability` | decimal | The order liability on this position from buy orders. In dollars. | | `buy_orders` | integer | Count of all buy orders. | | `buy_original_risk` | decimal | The original risk incurred by buy trades. In dollars. | | `buy_settlements` | integer | Count of all settlements from buy trades. | | `buy_trade_count` | integer | The total number of buy trades the account has made on the market. | | `cancelled_buy_contracts` | decimal | Sum of all not matched contracts in buy cancelled orders. | | `cancelled_buy_orders` | integer | Number of buy orders that were cancelled. | | `cancelled_contracts` | decimal | Sum of all not matched contracts in all cancelled orders. | | `cancelled_orders` | integer | Number of orders that were cancelled. | | `cancelled_sell_contracts` | decimal | Sum of all not matched contracts in sell cancelled orders. | | `cancelled_sell_orders` | integer | Number of sell orders that were cancelled. | | `closed_buy_premium` | decimal | Total premium from closed buy trade contracts. In dollars. | | `closed_fees` | decimal | The total value of fees from closed settlements. In dollars. | | `closed_gross_pnl` | decimal | The profit or loss the account has made in `closed` settlements. In dollars. | | `closed_net_pnl` | decimal | The net profit or loss after subtracting `closed_fees`. In dollars. | | `closed_premium` | decimal | Total premium from closed contracts. In dollars. | | `closed_sell_premium` | decimal | Total premium from closed sell trade contracts. In dollars. | | `competition` | string | The competition of this market | | `contracts_closed` | decimal | The total number of all contracts that were closed. | | `contracts_expired` | decimal | The total number of all contracts that were expired. | | `contracts_in_buy_orders` | decimal | Sum of all not matched contracts in buy orders. | | `contracts_in_orders` | decimal | Sum of all unmatched contracts in all orders: each order's `quantity` minus its `filled`. | | `contracts_in_sell_orders` | decimal | Sum of all not matched contracts in sell orders. | | `contracts_settled` | decimal | The number of contracts that have been settled. | | `contracts_traded` | decimal | The total number of contracts traded (the sum of each fill's `filled`). Only opening trades' contracts are counted, not contracts closing other trades. | | `event_id` | uuid | The event the market belongs to. | | `event_start_time` | int64 | The start time of the event. UNIX microseconds. | | `expired_buy_premium` | decimal | Total premium from expired buy trade contracts. In dollars. | | `expired_fees` | decimal | The total value of fees from expired settlements. In dollars. | | `expired_gross_pnl` | decimal | The profit or loss the account has made in `expired` settlements. In dollars. | | `expired_net_pnl` | decimal | The net profit or loss after subtracting `expired_fees`. In dollars. | | `expired_premium` | decimal | Total premium from expired contracts. In dollars. | | `expired_sell_premium` | decimal | Total premium from expired sell trade contracts. In dollars. | | `fee_plugin` | string | Internal. Virtual field naming the fee module in use, not part of the supported contract. | | `inserted_at` | int64 | Creation time, as UNIX microseconds. | | `last_settled_at` | int64 | When the last settlement was created. UNIX microseconds. | | `market_id` | uuid | The market this record relates to. | | `market_max_price` | decimal | The market's maximum price: the settlement value of one winning contract. In dollars. | | `max_liability_loss` | decimal | Worst-case loss if this market settles against the position you hold. In dollars. | | `max_liability_win` | decimal | Worst-case loss if this market settles in favor of the position you hold. In dollars. | | `max_potential_fee` | decimal | The maximum potential fees including all actual fees paid. In dollars. | | `max_potential_profit` | decimal | The maximum potential profit, including all actual profit. In dollars. | | `max_risk` | decimal | The total amount of risk on unsettled contracts. In dollars. | | `needs_rebuild` | boolean | Internal. Consistency flag used by position rebuilds, not part of the supported contract. | | `open_order_count` | integer | The number of open orders the account has on the market. | | `open_potential_fee` | decimal | The amount of potential fee that is related to unsettled contracts. In dollars. | | `open_potential_profit` | decimal | The potential profit from the outstanding contracts. In dollars. | | `open_premium` | decimal | Total premium on the user's current position in the market. In dollars. | | `open_risk` | decimal | The amount of risk associated with outstanding contracts. In dollars. | | `open_trade_count` | integer | The number of open trades the account has on the market. | | `order_liability` | decimal | The order liability on this position from all orders. In dollars. | | `orders` | integer | Count of all orders. | | `original_risk` | decimal | The original risk incurred by all trades. In dollars. | | `pending_close_fee` | decimal | Fees that would be charged if the resting close orders all filled. In dollars. | | `pending_close_pnl` | decimal | Profit or loss that would be realized if the resting close orders all filled. In dollars. | | `position` | decimal | The numerical position of the account in the market. In contracts. | | `position_accountability_alert_id` | uuid | Internal. Links to a position-accountability alert, not part of the supported contract. | | `position_premium_liability` | decimal | The liability of the position that affects available balance. In dollars. | | `potential_order_trade_fee` | decimal | Fees that would be charged if every open order on this market filled. In dollars. | | `rejected_buy_orders` | integer | Number of buy orders that were rejected. | | `rejected_orders` | integer | Number of orders that were rejected. | | `rejected_sell_orders` | integer | Number of sell orders that were rejected. | | `sell_contracts_closed` | decimal | The total number of sell contracts that were closed. | | `sell_contracts_expired` | decimal | The total number of sell contracts that were expired. | | `sell_contracts_settled` | decimal | The total number of sell contracts settled in the position. | | `sell_contracts_traded` | decimal | The total number of sell contracts the account has traded in the market. Only opening trade's contracts are included. | | `sell_order_liability` | decimal | The order liability on this position from sell orders. In dollars. | | `sell_orders` | integer | Count of all sell orders. | | `sell_original_risk` | decimal | The original risk incurred by sell trades. In dollars. | | `sell_settlements` | integer | Count of all settlements from sell trades. | | `sell_trade_count` | integer | The total number of sell trades the account has made on the market. | | `settled_at` | int64 | When the expired settlements were recorded. `null` if the market has not resulted or voided yet. UNIX microseconds. | | `settlements` | integer | Count of all settlements from all trades. | | `sport` | string | The sport of this market | | `status` | string | The status of the **market** this record covers, merged in from the live market index. `null` when the market is no longer held there. | | `title` | string | The title of the **market** this record covers, merged in from the live market index. `null` when the market is no longer held there. | | `total_fees` | decimal | The total fees that have been collected on the settlements. In dollars. | | `total_liability` | decimal | The liability affecting available balance from open orders and position. In dollars. | | `total_net_pnl` | decimal | The net profit or loss after subtracting `total_fees`. In dollars. | | `total_settlement_pnl` | decimal | The total profit or loss on all settlements. In dollars. | | `trade_count` | integer | The total number of trades the account has made on the market. | | `updated_at` | int64 | Time of the last change, as UNIX microseconds. | --- # List open positions > Your open positions as a point-in-time snapshot, largest position first. Source: https://docs.stxapp.io/api/rest/account/list-open-positions/ Your open positions as a point-in-time snapshot, largest `position` first. The body is identical to the `all_positions` frame the `positions:{user_id}` WebSocket channel sends on join, so a REST read can seed state that channel `updated_positions` deltas then keep current. This is a snapshot, not a feed: subscribe to the channel to hear about changes. Not paginated: the list is bounded by the markets you hold a live position in, and there is no `cursor`. A market you traded and closed out can appear with `position` `"0.00"` until it settles. Positions are not marked to market. ```http GET /api/v1/positions ``` Send it with your own demo key: [Try it](/quick-start/?op=positions_get#try-it). ## Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `market_ids` | `query` | string | no | Comma-separated market UUIDs. Returns only positions in these markets; an id you hold no position in matches nothing. Omit for every open position. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `400` | A parameter was missing or invalid. | Error | | `401` | Missing, malformed or unrecognized signature, or a timestamp outside the 30-second window. Body: {"error":"Missing or invalid API key credentials"} | Error | | `403` | The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | ## Example Request: ```bash curl --request GET \ --url 'https://demo.stxapp.io/api/v1/positions' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' ``` Response `200`: ```json { "positions": [ { "account_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "average_open_premium": "0.6700", "buy_order_liability": "0.6700", "contracts_settled": "2.00", "event_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "gross_pnl": "0.6700", "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "market_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "max_potential_fee": "0.6700", "max_potential_profit": "0.6700", "max_risk": "0.6700", "open_potential_fee": "0.6700", "open_potential_profit": "0.6700", "open_risk": "0.6700", "position": "2.00", "position_premium_liability": "0.6700", "premium": "0.6700", "sell_order_liability": "0.6700", "total_fee": "0.6700", "total_settlement_pnl": "0.6700" } ] } ``` ### Response fields | Field | Type | Description | |---|---|---| | `account_id` | uuid | The account the position belongs to. | | `average_open_premium` | decimal | Average premium per contract for the open portion. In dollars. | | `buy_order_liability` | decimal | Liability from your open buy orders on this market. In dollars. | | `contracts_settled` | decimal | Contracts settled in the position so far. | | `event_id` | uuid | The event the market belongs to. | | `gross_pnl` | decimal | `total_settlement_pnl` plus any pending-close profit or loss that has not settled yet. In dollars. | | `id` | uuid | The unique id of the position record. | | `market_id` | uuid | The market the position is on. | | `max_potential_fee` | decimal | Total potential fee across the position's settlements. In dollars. | | `max_potential_profit` | decimal | Total possible profit for the position if everything settles favorably. In dollars. | | `max_risk` | decimal | Account-level risk on the position, netting in already settled profit and loss. In dollars. | | `open_potential_fee` | decimal | Potential fee on the open contracts when they settle. In dollars. | | `open_potential_profit` | decimal | Possible profit on the position's open contracts. In dollars. | | `open_risk` | decimal | Risk on the position's open (unsettled) contracts. In dollars. | | `position` | decimal | Net position in the market: positive if long (bought), negative if short (sold). Can be `"0.00"` for a market you have traded and closed out that has not settled yet. In contracts. | | `position_premium_liability` | decimal | Liability from the position's premium that counts against available balance. Routinely negative. In dollars. | | `premium` | decimal | Total premium paid or received for the open portion of the position. In dollars. | | `sell_order_liability` | decimal | Liability from your open sell orders on this market. In dollars. | | `total_fee` | decimal | Total fees paid across the position's settlements. In dollars. | | `total_settlement_pnl` | decimal | Profit or loss realized from settlements so far. In dollars. | --- # Events > The events markets belong to. Source: https://docs.stxapp.io/api/rest/events/ The events markets belong to. Use these to group markets by game, competition or start time. 1 endpoint. - [List events](/api/rest/events/list-events/) --- # List events > Events, optionally filtered by sport, competition, type, title and status. Source: https://docs.stxapp.io/api/rest/events/list-events/ Events, optionally filtered by sport, competition, type, title and status. Defaults to newest-inserted first. ```http GET /api/v1/events ``` Send it with your own demo key: [Try it](/quick-start/?op=events_get#try-it). :::tip[In the SDKs] - TypeScript: [`STX.events()`](/sdks/typescript/reference/stx/#events) - Python: [`STX.events()`](/sdks/python/reference/stx/#events) - C#: [`STXEventService.GetEventInfosAsync()`](/sdks/csharp/reference/market-data/#stxeventservice) ::: ## Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `event_ids` | `query` | string | no | Comma-separated event UUIDs. | | `sports` | `query` | string | no | Comma-separated sport names, e.g. `Baseball,Basketball`. Exact case-insensitive match. | | `competitions` | `query` | string | no | Comma-separated competition codes, e.g. `MLB,NFL`. | | `event_types` | `query` | string | no | Comma-separated event types, matched as a case-insensitive **substring**. | | `title` | `query` | string | no | A single title fragment, matched as a case-insensitive substring. | | `status` | `query` | `scheduled` \| `in_progress` \| `completed` \| `cancelled` | no | A single event status, lowercase. This filter takes one value, not a comma-separated list. These are *event* statuses, not the market statuses `/markets` takes. | | `promoted` | `query` | boolean | no | Filter to promoted events. Unlike `trading` on `/markets`, an unrecognized value is **silently ignored** rather than rejected: the filter is dropped and every event is returned. | | `sort_by[name]` | `query` | `start_time` | no | Only `start_time` is supported. Unlike `/markets`, an unrecognized value is **silently ignored** and the default newest-inserted-first ordering is used, so a response may not carry the order you asked for. | | `sort_by[direction]` | `query` | `asc` \| `desc` | no | `asc` or `desc`, defaulting to descending. Only meaningful alongside `sort_by[name]`, and silently ignored if unrecognized. | | `limit` | `query` | integer | no | Rows per page. Defaults to 100 and is silently clamped to 200; a larger value is not an error. | | `cursor` | `query` | string | no | Cursor from the previous response. Omit for the first page. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `400` | A parameter was missing or invalid. | Error | | `401` | Missing, malformed or unrecognized signature, or a timestamp outside the 30-second window. Body: {"error":"Missing or invalid API key credentials"} | Error | | `403` | The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | ## Example Request: ```bash curl --request GET \ --url 'https://demo.stxapp.io/api/v1/events' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' ``` Response `200`: ```json { "cursor": null, "events": [ { "archived": null, "competition": null, "event_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "event_type": null, "participants": null, "promoted": null, "short_title": null, "sport": null, "start_time": 0, "start_time_iso": "2026-08-25T04:42:46.242093Z", "status": null, "symbol": null, "title": null } ] } ``` ### Response fields | Field | Type | Description | |---|---|---| | `cursor` | string | Opaque cursor for the next page. Pass the value from the previous response; omit for the first page. `null` once there are no more pages. | | `archived` | boolean | Whether this event is archived. | | `competition` | string | The competition for this event | | `event_id` | uuid | The event the market belongs to. | | `event_type` | string | The type of this event, e.g. `ad_hoc`, `baseball_game`, `basketball_game`. | | `participants` | array | The event's participants, as an array of `{name, role, short_name, abbreviation}`. | | `promoted` | boolean | Whether this event is promoted. | | `short_title` | string | The short title for this event | | `sport` | string | The sport for this event | | `start_time` | int64 | Start time of this event UNIX microseconds. | | `start_time_iso` | date-time | Start time of this event | | `status` | string | The status of this event | | `symbol` | string | The STX symbol for this event | | `title` | string | The title of this event | --- # Fills > Your own executions. Source: https://docs.stxapp.io/api/rest/fills/ Your own executions. One order can produce many fills, each with its own price, fee and liquidity side. Row fields keep their `trade_`-prefixed names (`trade_id`, `trade_fee`, `unrounded_trade_fee`) so a snapshot here lines up with a `fills:` WebSocket delta. Not to be confused with the market-wide `trades` market data WebSocket topic, which carries every account's executions rather than only yours. 1 endpoint. - [List fills](/api/rest/fills/list-fills/) --- # List fills > Fills for the authenticated account, most recent first. Source: https://docs.stxapp.io/api/rest/fills/list-fills/ Fills for the authenticated account, most recent first. One order can produce many fills, each with its own price, fee and liquidity side. ```http GET /api/v1/fills ``` Send it with your own demo key: [Try it](/quick-start/?op=fills_get#try-it). :::tip[In the SDKs] - TypeScript: [`STX.fills()`](/sdks/typescript/reference/stx/#fills) - Python: [`STX.fills()`](/sdks/python/reference/stx/#fills) - C#: [`STXTradeService.GetMyTradesAsync()`](/sdks/csharp/reference/trading/#stxtradeservice) ::: ## Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `market_ids` | `query` | string | no | Comma-separated market UUIDs. | | `order_ids` | `query` | string | no | Comma-separated order UUIDs. Returns only the fills those orders produced. Combines with `market_ids` and `status` (all filters must match), and pages with `cursor` like any other filter. An order id that is not yours matches nothing rather than erroring. | | `status` | `query` | `created` \| `open` \| `settled` \| `cancelled` | no | A single fill status. This filter takes one value, not a comma-separated list. | | `limit` | `query` | integer | no | Rows per page. Defaults to 100 and is silently clamped to 200; a larger value is not an error. | | `cursor` | `query` | string | no | Cursor from the previous response. Omit for the first page. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `400` | A parameter was missing or invalid. | Error | | `401` | Missing, malformed or unrecognized signature, or a timestamp outside the 30-second window. Body: {"error":"Missing or invalid API key credentials"} | Error | | `403` | The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | ## Example Request: ```bash curl --request GET \ --url 'https://demo.stxapp.io/api/v1/fills' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' ``` Response `200`: ```json { "cursor": null, "fills": [ { "account_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "action": "buy", "admin_log": null, "amended": null, "client_order_id": null, "closed_contracts": "2.00", "closed_fee": "0.6700", "closed_net_pnl": "0.6700", "closed_pnl": "0.6700", "closing": "2.00", "device_id": null, "expired_contracts": "2.00", "expired_fee": "0.6700", "expired_net_pnl": "0.6700", "expired_pnl": "0.6700", "expires_at": null, "filled": "2.00", "gross_pnl": "0.6700", "inserted_at": 0, "ip_address": null, "last_modified_at": null, "last_modified_by_admin_id": null, "liquidity_action": null, "market_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "opened_at": null, "order_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "original_filled": "2.00", "original_premium": "0.6700", "original_price": "0.6700", "original_risk": "0.6700", "original_to_win": "0.6700", "pc_premium": "0.6700", "pc_risk": "0.6700", "pc_to_win": "0.6700", "placed_pre_start": null, "points": null, "pre_start": null, "price": "0.6700", "remaining": "2.00", "remaining_potential_fee": "0.6700", "remaining_premium": "0.6700", "remaining_risk": "0.6700", "remaining_to_win": "0.6700", "settled_at": null, "settled_contracts": "2.00", "settlements_count": null, "status": "created", "time": "2026-08-25T04:42:46.242093Z", "total_fee": "0.6700", "trade_fee": "0.6700", "trade_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "traded_contracts": "2.00", "unrounded_trade_fee": "0.012345678", "updated_at": null, "virtual_remaining": "2.00" } ] } ``` ### Response fields | Field | Type | Description | |---|---|---| | `cursor` | string | Opaque cursor for the next page. Pass the value from the previous response; omit for the first page. `null` once there are no more pages. | | `account_id` | uuid | The account the record belongs to. | | `action` | string | The action of the trade relative to the user. | | `admin_log` | object | Internal. Operator audit trail, not part of the supported contract; do not depend on it. | | `amended` | boolean | True if STX has amended this trade. When true, compare `original_price` and `original_filled` against the current values. | | `client_order_id` | string | The client order id of the order that produced this trade, when one was supplied. | | `closed_contracts` | decimal | The number of contracts closed on this trade. | | `closed_fee` | decimal | The fee the user paid when closing the trade. In dollars. | | `closed_net_pnl` | decimal | Realized profit or loss on the closed portion. Negative for a loss. In dollars. | | `closed_pnl` | decimal | The profit or loss the user made by closing the trade. In dollars. | | `closing` | decimal | The amount of contracts (in position) that the trade is closing. | | `device_id` | string | The device associated with this trade. | | `expired_contracts` | decimal | Number of contracts settled when market expired. | | `expired_fee` | decimal | The fee paid by the user when the market expired. In dollars. | | `expired_net_pnl` | decimal | Realized profit or loss on the expired portion. In dollars. | | `expired_pnl` | decimal | The profit or loss on the trade when the market expired. In dollars. | | `expires_at` | int64 | When the trade expires if the market has not settled, as UNIX microseconds. | | `filled` | decimal | The number of contracts that were traded. | | `gross_pnl` | decimal | The gross PNL as a result of the trade. In dollars. | | `inserted_at` | int64 | Creation time, as UNIX microseconds. | | `ip_address` | string | The IP address associated with this trade | | `last_modified_at` | int64 | When STX last amended this trade, as UNIX microseconds. Null unless `amended` is true. | | `last_modified_by_admin_id` | uuid | Internal. Operator audit field, not part of the supported contract; do not depend on it. | | `liquidity_action` | string | Whether the associated order was the `provider` or the `taker` of the liquidity. | | `market_id` | uuid | The market this record relates to. | | `opened_at` | int64 | When the position opened, as UNIX microseconds. | | `order_id` | uuid | The ID of the order that caused the trade. | | `original_filled` | decimal | Contracts filled at execution, before any amendment. | | `original_premium` | decimal | The amount of premium received for the original trade. In dollars. | | `original_price` | decimal | Fill price at execution. Unchanged by later amendments. In dollars. | | `original_risk` | decimal | The original risk introduced for the original trade excluding `closed`. In dollars. | | `original_to_win` | decimal | The original gain if the position wins, for the trade excluding `closed`. In dollars. | | `pc_premium` | decimal | The amount of premium per contract. In dollars. | | `pc_risk` | decimal | The amount of risk per contract. In dollars. | | `pc_to_win` | decimal | What each contract gains if the position wins: `max_price - price` on a buy, `price` on a sell. In dollars. | | `placed_pre_start` | boolean | True if the order was placed before the event started. | | `points` | number | The total number of loyalty points awarded as a result of making the trade. | | `pre_start` | boolean | Whether the trade was based on pre-start activity. | | `price` | decimal | The price that the trade was executed at. In dollars. | | `remaining` | decimal | The number of unsettled contracts in the trade. | | `remaining_potential_fee` | decimal | The potential fee from unsettled contracts. In dollars. | | `remaining_premium` | decimal | Premium as yet unsettled. In dollars. | | `remaining_risk` | decimal | The total current risk for the trade. In dollars. | | `remaining_to_win` | decimal | What the unsettled contracts gain if the position wins. In dollars. | | `settled_at` | date-time | The timestamp when this trade's status was set to `settled` | | `settled_contracts` | decimal | Contracts already settled. Below `traded_contracts` on a partial settlement. | | `settlements_count` | integer | The number of settlements where this trade is the opening trade. | | `status` | string | Fill state. `created` once the matching engine has written the fill, `open` while the position is live, `settled` once it no longer contributes to a position, and `cancelled` if STX reversed it. | | `time` | date-time | The ISO-8601 Date time the trade was created. | | `total_fee` | decimal | Sum of on-trade fee plus fees paid from settlements linked with the trade. In dollars. | | `trade_fee` | decimal | Per-trade fee (on-trade fee schedule). Included in total_fee. In dollars. | | `trade_id` | uuid | Unique identifier for the trade. | | `traded_contracts` | decimal | Number of contracts in this trade. | | `unrounded_trade_fee` | decimal | Fee before rounding. Use the rounded fee for reconciliation. In dollars. Carries up to 9 decimal places; parse money with a variable-scale decimal type, not a fixed-width one. | | `updated_at` | int64 | Time of the last change, as UNIX microseconds. | | `virtual_remaining` | decimal | Remaining contracts including unsettled exposure. | --- # Identity > Who your key belongs to. Source: https://docs.stxapp.io/api/rest/identity/ Who your key belongs to. Returns the user_id and account_id that every account channel topic and account query is keyed on, plus the scope your key was issued with, and lets you change the public profile (handle, avatar, leaderboard opt-in) shown to other members. 2 endpoints. - [Get the authenticated account](/api/rest/identity/get-account/) - [Update your public profile](/api/rest/identity/update-your-public-profile/) --- # Get the authenticated account > The account behind the credential: identifiers, name, how the request authenticated and your public profile. Source: https://docs.stxapp.io/api/rest/identity/get-account/ The account behind the credential: identifiers, name, how the request authenticated and your public profile (`handle`, `avatar_url`, `leaderboard_opt_in`, `handle_changeable_at`). ```http GET /api/v1/me ``` Send it with your own demo key: [Try it](/quick-start/?op=me_get#try-it). :::tip[In the SDKs] - TypeScript: [`STX.me()`](/sdks/typescript/reference/stx/#me) - Python: [`STX.me()`](/sdks/python/reference/stx/#me) - C#: [`STXIdentityService.GetMeAsync()`](/sdks/csharp/reference/account/#stxidentityservice) ::: ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `401` | No credential, a bad signature, or a bearer token that is expired, invalid or not a member's. With any signing header present the API-key rules apply and the body is {"error":"Missing or invalid API key credentials"}; otherwise {"error":"Unauthorized"}. | Error | | `403` | The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | ## Example Request: ```bash curl --request GET \ --url 'https://demo.stxapp.io/api/v1/me' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' ``` Response `200`: ```json { "me": { "account_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "avatar_url": null, "first_name": null, "handle": null, "handle_changeable_at": null, "key_id": null, "last_name": null, "leaderboard_opt_in": false, "method": "api_key", "scope": null, "user_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d" } } ``` ### Response fields | Field | Type | Description | |---|---|---| | `account_id` | uuid | Your account id. Trades, settlements and balances are scoped to it. | | `avatar_url` | string | Path of your avatar, `/avatars/{handle}.svg`, relative to the API host. `null` when `handle` is `null`. | | `first_name` | string | First name on the account, or `null` if no profile is loaded. | | `handle` | string | Your public handle, shown on the leaderboard in place of your name. `null` until one is set. | | `handle_changeable_at` | date-time | When you may next change your handle. `null` means now; otherwise the end of the 30-day window since the last change. | | `key_id` | string | The id of the API key that signed this request: the value sent in the access-key header. `null` on a bearer request. | | `last_name` | string | Last name on the account, or `null` if no profile is loaded. | | `leaderboard_opt_in` | boolean | Whether you have consented to appear on public leaderboards. New accounts start with the jurisdiction's default; accounts from before the leaderboard start at `false`. Change it with `PATCH /api/v1/me/profile`. | | `method` | string | How this request authenticated: `api_key` for a signed request, `bearer` for a session token. | | `scope` | string | Access level granted to this key: `read_only`, or `read_write` for keys that may place and cancel orders. `null` on a bearer request. | | `user_id` | uuid | Your user id. Substitute this into account channel topics such as `orders:{user_id}`. | --- # Update your public profile > Change your handle, avatar or leaderboard_opt_in. Source: https://docs.stxapp.io/api/rest/identity/update-your-public-profile/ Change your `handle`, `avatar` or `leaderboard_opt_in`. Every key is optional but at least one must be sent. A key of the wrong JSON type is a 400. A handle that is taken, reserved, blocked, malformed or changed again inside the 30-day window is a 422 whose `error` names the rule. Returns the same object as `GET /api/v1/me`. A bearer session may always call this; an API key needs `read_write`. ```http PATCH /api/v1/me/profile ``` Send it with your own demo key: [Try it](/quick-start/?op=me_profile_patch#try-it). ## Request body | Field | Type | Required | Description | |---|---|---|---| | `avatar` | object | no | A new avatar. Any style/palette pairing renders; the seed selects the pattern. `null` is refused (`400`). | | `avatar.palette` | string | **yes** | One of `ember`, `forest`, `ocean`, `grape`, `slate`, `mint`, `rose`, `gold`. | | `avatar.seed` | string | **yes** | 1–32 characters. The same seed always renders the same image. | | `avatar.style` | string | **yes** | One of `dots`, `rings`, `stripes`, `grid`. | | `handle` | string | no | New public handle: 3–24 characters of lowercase letters, digits and inner `.` or `_`; case is folded. Must be unused, not a reserved word and not blocked. May be changed once every 30 days; re-sending your current handle is not a change. A blank handle is refused (`422`). | | `leaderboard_opt_in` | boolean | no | Your consent to appear on public boards. `true` joins, `false` leaves; your own standing is unaffected. Each change is recorded with its time. Joining gives you a generated handle and avatar if you have none yet. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `400` | A parameter was missing or invalid. | Error | | `401` | No credential, a bad signature, or a bearer token that is expired, invalid or not a member's. With any signing header present the API-key rules apply and the body is {"error":"Missing or invalid API key credentials"}; otherwise {"error":"Unauthorized"}. | Error | | `403` | This API key does not have write access. The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | | `422` | The new value was refused: a handle that is taken, reserved, blocked, malformed or changed again inside the 30-day window. The body's `error` names the rule. | Error | ## Example Request: ```bash curl --request PATCH \ --url 'https://demo.stxapp.io/api/v1/me/profile' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' \ --header 'Content-Type: application/json' \ --data '{ "avatar": { "palette": "ocean", "seed": "a1b2c3d4", "style": "dots" }, "handle": "swift.fox12", "leaderboard_opt_in": true }' ``` Response `200`: ```json { "me": { "account_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "avatar_url": null, "first_name": null, "handle": null, "handle_changeable_at": null, "key_id": null, "last_name": null, "leaderboard_opt_in": false, "method": "api_key", "scope": null, "user_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d" } } ``` ### Response fields | Field | Type | Description | |---|---|---| | `account_id` | uuid | Your account id. Trades, settlements and balances are scoped to it. | | `avatar_url` | string | Path of your avatar, `/avatars/{handle}.svg`, relative to the API host. `null` when `handle` is `null`. | | `first_name` | string | First name on the account, or `null` if no profile is loaded. | | `handle` | string | Your public handle, shown on the leaderboard in place of your name. `null` until one is set. | | `handle_changeable_at` | date-time | When you may next change your handle. `null` means now; otherwise the end of the 30-day window since the last change. | | `key_id` | string | The id of the API key that signed this request: the value sent in the access-key header. `null` on a bearer request. | | `last_name` | string | Last name on the account, or `null` if no profile is loaded. | | `leaderboard_opt_in` | boolean | Whether you have consented to appear on public leaderboards. New accounts start with the jurisdiction's default; accounts from before the leaderboard start at `false`. Change it with `PATCH /api/v1/me/profile`. | | `method` | string | How this request authenticated: `api_key` for a signed request, `bearer` for a session token. | | `scope` | string | Access level granted to this key: `read_only`, or `read_write` for keys that may place and cancel orders. `null` on a bearer request. | | `user_id` | uuid | Your user id. Substitute this into account channel topics such as `orders:{user_id}`. | --- # Leaderboard > Ranked members per period (day, week, month, year, all time, on America/New_York days) and per sport, on up to eight boards: volume, profit, contracts settled, markets settled, win rate, biggest win, return and winning streak. Source: https://docs.stxapp.io/api/rest/leaderboard/ Ranked members per period (day, week, month, year, all time, on America/New_York days) and per sport, on up to eight boards: volume, profit, contracts settled, markets settled, win rate, biggest win, return and winning streak. STX chooses which boards are shown and how many members each lists; a hidden board comes back empty with `shown: false`. Rows carry a public handle, avatar and top sport, never an account id. Your own standing, including your rank when you are outside the list, is `GET /api/v1/leaderboard/me`. Returns 404 where the feature is not enabled. 2 endpoints. - [Get a leaderboard](/api/rest/leaderboard/get-leaderboard/) - [Get your own standing](/api/rest/leaderboard/get-your-own-standing/) --- # Get a leaderboard > The top members for one period, category and metric, best first. Source: https://docs.stxapp.io/api/rest/leaderboard/get-leaderboard/ The top members for one period, category and metric, best first. `value` is a dollar string for money metrics, an integer for counts and a `0..1` number for ratios. Rows carry a public handle, avatar path and top sport, never an account id. An unknown `category` is not an error; the board is simply empty. A board STX hides is empty with `shown: false`. ```http GET /api/v1/leaderboard ``` Send it with your own demo key: [Try it](/quick-start/?op=leaderboard_get#try-it). ## Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `period` | `query` | `daily` \| `weekly` \| `monthly` \| `yearly` \| `all` | no | The ranking window, on America/New_York calendar days: `daily` is today, `weekly` from Monday, `monthly` from the 1st, `yearly` from 1 January, `all` lifetime. Default `weekly`. | | `category` | `query` | string | no | `all` (default) or a sport key, as in a row's `top_sport`, e.g. `basketball`. An unknown key has no rankings: an empty board, or an unranked standing on `/me`. | | `metric` | `query` | `volume` \| `profit` \| `predictions` \| `markets` \| `win_rate` \| `biggest_win` \| `return` \| `streak` | no | The board: notional traded (`filled × max_price`), net profit, contracts settled, markets settled, win rate, biggest single win, return (profit ÷ volume) or longest winning streak. Default: the board STX opens on. | | `limit` | `query` | integer | no | Rows to return. Defaults to 100 and is silently clamped to the number of members STX shows per board (at most 100). There is no cursor. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | Leaderboard | | `400` | A parameter was missing or invalid. | Error | | `401` | No credential, a bad signature, or a bearer token that is expired, invalid or not a member's. With any signing header present the API-key rules apply and the body is {"error":"Missing or invalid API key credentials"}; otherwise {"error":"Unauthorized"}. | Error | | `403` | The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | | `404` | The leaderboard is not enabled in this environment. Body: {"error":"Leaderboard is not enabled"} | Error | ## Example Request: ```bash curl --request GET \ --url 'https://demo.stxapp.io/api/v1/leaderboard' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' ``` Response `200`: ```json { "category": "string", "leaderboard": [ { "avatar_url": null, "handle": null, "rank": null, "top_sport": null, "top_sport_icon_url": null, "value": null } ], "metric": "volume", "next_reset_at": null, "period": "daily", "refreshed_at": null, "shown": false } ``` ### Response fields | Field | Type | Description | |---|---|---| | `category` | string | `all` or a sport key. | | `metric` | string | The metric the rows are ranked on. | | `next_reset_at` | date-time | When this period's board resets: local midnight in America/New_York on the next boundary, as UTC. `null` for `all`, which never resets, and before the first snapshot. | | `period` | string | The period the board covers. | | `refreshed_at` | date-time | When this snapshot was built. `null` before the first snapshot is published. | | `shown` | boolean | Whether STX shows this board to members. `false` means the board is hidden and `leaderboard` is empty, not that nobody has ranked yet. | | `avatar_url` | string | Path of the member's avatar, `/avatars/{handle}.svg`, relative to the API host. `null` when `handle` is `null`. | | `handle` | string | The member's public handle. `null` for a member who has not set one. | | `rank` | integer | Dense rank, 1-based. Tied values share a rank. | | `top_sport` | string | The sport the member traded most in the period, as a category key. On a sport board it is that sport. `null` when unknown. | | `top_sport_icon_url` | string | Path of the sport's icon, the same category icon the apps use, relative to the API host. `null` when `top_sport` is `null`. | | `value` | | The ranked value. A dollar string on money boards, an integer on count boards and a number from 0 to 1 on ratio boards. | --- # Get your own standing > Your rank and value on every shown board for one period and category, including a rank outside the list, plus your win rate and how many markets you have had decided. Source: https://docs.stxapp.io/api/rest/leaderboard/get-your-own-standing/ Your rank and value on every shown board for one period and category, including a rank outside the list, plus your win rate and how many markets you have had decided. A rank is `null` when you are unranked there (opted out, or no activity in the period) or STX hides that board. ```http GET /api/v1/leaderboard/me ``` Send it with your own demo key: [Try it](/quick-start/?op=leaderboard_me_get#try-it). ## Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `period` | `query` | `daily` \| `weekly` \| `monthly` \| `yearly` \| `all` | no | The ranking window, on America/New_York calendar days: `daily` is today, `weekly` from Monday, `monthly` from the 1st, `yearly` from 1 January, `all` lifetime. Default `weekly`. | | `category` | `query` | string | no | `all` (default) or a sport key, as in a row's `top_sport`, e.g. `basketball`. An unknown key has no rankings: an empty board, or an unranked standing on `/me`. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | LeaderboardStanding | | `400` | A parameter was missing or invalid. | Error | | `401` | No credential, a bad signature, or a bearer token that is expired, invalid or not a member's. With any signing header present the API-key rules apply and the body is {"error":"Missing or invalid API key credentials"}; otherwise {"error":"Unauthorized"}. | Error | | `403` | The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | | `404` | The leaderboard is not enabled in this environment. Body: {"error":"Leaderboard is not enabled"} | Error | ## Example Request: ```bash curl --request GET \ --url 'https://demo.stxapp.io/api/v1/leaderboard/me' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' ``` Response `200`: ```json { "biggest_win": "string", "category": "string", "markets": "string", "opted_in": false, "period": "daily", "predictions": "string", "profit": "string", "return": "string", "settled_markets": 0, "streak": "string", "volume": "string", "win_rate": null, "win_rate_rank": "string" } ``` ### Response fields | Field | Type | Description | |---|---|---| | `biggest_win` | | Your rank and biggest single-market win; `null` when unranked or the board is hidden. | | `category` | string | The category asked for. | | `markets` | | Your rank and markets settled; `null` when unranked or the board is hidden. | | `opted_in` | boolean | Whether you appear on public boards. `false` is why every rank is `null` for an opted-out member. | | `period` | string | The period asked for. | | `predictions` | | Your rank and contracts settled; `null` when unranked or the board is hidden. | | `profit` | | Your rank and net profit; `null` when unranked or the board is hidden. | | `return` | | Your rank and return (profit ÷ volume), `0.0`–`1.0`; needs $500 of volume; `null` when unranked or the board is hidden. | | `settled_markets` | integer | Markets decided (won or lost) in the period. `0` when unranked. | | `streak` | | Your rank and longest winning streak; `null` when unranked or the board is hidden. | | `volume` | | Your rank and notional traded; `null` when unranked or the board is hidden. | | `win_rate` | number | Share of decided markets you won, `0.0`–`1.0`. `null` until at least 10 markets have been decided in the period; a market settled at zero profit is neither a win nor a loss. | | `win_rate_rank` | | Your rank and win rate, `0.0`–`1.0`; needs 10 decided markets to rank; `null` when unranked or the board is hidden. | --- # Markets > Find something to trade. Source: https://docs.stxapp.io/api/rest/markets/ Find something to trade. Markets carry the question, the settlement rule, the current book and the max_price an order must stay below. 1 endpoint. - [List markets](/api/rest/markets/list-markets/) --- # List markets > Markets, filterable by event, competition, sport, status and whether they are currently trading. Source: https://docs.stxapp.io/api/rest/markets/list-markets/ Markets, filterable by event, competition, sport, status and whether they are currently trading. Ordered tradeable-first by default: `open` markets accepting orders, then `pre_open` markets accepting resting limit orders, then `open`/`pre_open` markets with trading paused, then `scheduled`, then `closed`/`cancelled`, and finally settled `resulted`/`voided` markets. Within each of those tiers the soonest event comes first. Use `sort_by` to replace that ordering with a plain event-start sort. ```http GET /api/v1/markets ``` Send it with your own demo key: [Try it](/quick-start/?op=markets_get#try-it). :::tip[In the SDKs] - TypeScript: [`STX.markets()`](/sdks/typescript/reference/stx/#markets) - Python: [`STX.markets()`](/sdks/python/reference/stx/#markets) - C#: [`STXMarketService.GetMarketInfosAsync()`](/sdks/csharp/reference/market-data/#stxmarketservice) ::: ## Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `market_ids` | `query` | string | no | Comma-separated market UUIDs. When present, returns exactly these markets unpaginated, and every other filter, sort and pagination param is ignored. | | `event_ids` | `query` | string | no | Comma-separated event UUIDs. | | `status` | `query` | `scheduled` \| `pre_open` \| `open` \| `closed` \| `resulted` \| `cancelled` \| `voided`[] | no | Market statuses, lowercase. An uppercase value returns 400. Omitted, the response includes every non-archived market, not just open ones; the settled ones sort to the last pages rather than being excluded. Note that a market matched by `status=open` can still render its `status` as `suspended`; see the market status guide. | | `trading` | `query` | boolean | no | Return only markets that are (`true`) or are not (`false`) currently accepting orders. Any other value, including `on`/`off`, returns 400. Note that a market with `trading: false` and status `open` or `pre_open` renders its `status` as `suspended`. | | `sports` | `query` | string | no | Comma-separated sport names, e.g. `Baseball,Basketball`. Case-insensitive. | | `competitions` | `query` | string | no | Comma-separated competition codes, e.g. `MLB,NFL`. | | `sort_by[name]` | `query` | `event_start` | no | Only `event_start` is supported; any other value returns 400. Supplying it replaces the default tradeable-first ordering with a plain event-start sort. The two cannot be combined, because the pagination cursor encodes a single sort direction. | | `sort_by[direction]` | `query` | `asc` \| `desc` | no | `asc` (default) or `desc`; any other value returns 400. Only meaningful alongside `sort_by[name]`. | | `limit` | `query` | integer | no | Rows per page. Defaults to 100 and is silently clamped to 200; a larger value is not an error. | | `cursor` | `query` | string | no | Cursor from the previous response. Omit for the first page. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `400` | A parameter was missing or invalid. | Error | | `401` | Missing, malformed or unrecognized signature, or a timestamp outside the 30-second window. Body: {"error":"Missing or invalid API key credentials"} | Error | | `403` | The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | ## Example Request: ```bash curl --request GET \ --url 'https://demo.stxapp.io/api/v1/markets' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' ``` Response `200`: ```json { "cursor": null, "markets": [ { "archived": null, "bids": null, "competition": null, "description": null, "event_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "event_short_title": null, "event_start": "2026-08-25T04:42:46.242093Z", "event_status": null, "event_title": null, "event_type": null, "featured": null, "featured_home": null, "filters": null, "group_title": null, "grouping_id": null, "grouping_name": null, "home_category": null, "in_play_delay_sec": null, "keywords": null, "last_probability_at": null, "last_traded_price": "0.6700", "manual_probability": null, "market_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "max_price": "1.0000", "offers": null, "open_interest": "2.00", "participants": null, "points_cost": null, "position": null, "powered_by": null, "price": "0.6700", "price_change24h": null, "probability": null, "question": null, "recent_trades": null, "result": null, "rules": null, "settled_at": null, "short_title": null, "specifier": null, "sport": null, "stat_detail": "string", "status": "scheduled", "symbol": null, "timestamp": "2026-08-25T04:42:46.242093Z", "timestamp_int": 0, "title": null, "total_volume": "2.00", "trading": null, "trading_filters": null, "volume24h": "2.00" } ] } ``` ### Response fields | Field | Type | Description | |---|---|---| | `cursor` | string | Opaque cursor for the next page. Pass the value from the previous response; omit for the first page. `null` once there are no more pages. | | `archived` | boolean | Whether the market is archived. | | `bids` | array | The top bids on the market, sorted by price descending, so the best (highest) bid is first. | | `competition` | string | The text to use in describing the competition, e.g. NBA. | | `description` | string | The description of the market. | | `event_id` | uuid | The event the market belongs to. | | `event_short_title` | string | The short title of the event associated with the market. | | `event_start` | date-time | The UTC start date and time of the event. | | `event_status` | string | The status of the event that the market is attached to. | | `event_title` | string | The title of the event associated with the market. | | `event_type` | string | The type of event associated with the market. | | `featured` | boolean | Whether the market is featured. | | `featured_home` | boolean | Whether the market is featured on the home page. | | `filters` | array | The categorizations under which the market appears. A list, not an object. | | `group_title` | string | The human readable group title for the market. | | `grouping_id` | string | Identity of the set of mutually exclusive outcomes this market prices against: the 30 World Series contracts, the 4 AFC East contracts, one fixture's moneyline pair. Opaque: compare for equality, never parse. Stable for the life of the market, and present in every status, so it is the field to map market data on. Unlike `symbol`, it does not move when a fixture is rescheduled. | | `grouping_name` | string | The grouping in words, e.g. `AL East Division`, `Spread - 1st Quarter`. Descriptive rather than stable: for a tournament it is the event title, which can be renamed. Display it; join on `grouping_id`. | | `home_category` | string | The category in which the market appears: `Upcoming`, `Live` or `null`. | | `in_play_delay_sec` | integer | The order delay (in seconds) when the event is in progress. | | `keywords` | array | The keywords that are set for the market. | | `last_probability_at` | int64 | The time that the last probability update was received by the server. UNIX microseconds. | | `last_traded_price` | decimal | The price of the last executed trade. In dollars. | | `manual_probability` | boolean | `true` when the probability was set by hand, `false` when it came from the pricing feed. A flag, not a probability figure. | | `market_id` | uuid | The market this record relates to. | | `max_price` | decimal | The settlement value of one winning contract, and the ceiling on order prices: an order must price strictly below it. Read it per market. In dollars. | | `offers` | array | The top offers on the market, sorted by price descending, so the best (lowest) offer is last. | | `open_interest` | decimal | Current open interest in the market. In contracts. | | `participants` | array | The participants of the market's event, as an array. | | `points_cost` | integer | The cost of each loyalty point for amount risked by the user. | | `position` | string | Text describing the position this market takes, e.g. a participant name. A label, not a numeric ordering. | | `powered_by` | string | The provider used to result this market. | | `price` | decimal | The market price that the market is trading at. In dollars. | | `price_change24h` | integer | The change in price over the last 24 hours, as a percentage. Not money: it stays a number and must not be divided by 100. | | `probability` | number | The market's probability of the outcome, between 0 and 1. | | `question` | string | The question that the market is asking. | | `recent_trades` | array | The last 15 trades on the market. | | `result` | string | The result of the market. | | `rules` | string | The rules for this market. | | `settled_at` | int64 | When the market was resulted or voided. A raw microsecond integer, unlike the ISO-8601 `timestamp` and `event_start` beside it. UNIX microseconds. | | `short_title` | string | The human readable short title for the market. | | `specifier` | string | The specifier for the rules to properly determine the market. | | `sport` | string | The text to use in describing the sport, e.g. Basketball. | | `stat_detail` | | The stat line this market is derived from, or `null` for a market that is not a stat-line prop. | | `status` | string | Market state. See [Market and order status](/concepts/market-status/). Note that a market with `status: open` may still report as suspended when trading is halted. | | `symbol` | string | A unique symbol for this market. | | `timestamp` | date-time | Server time when this payload was generated, as an ISO-8601 string. The `timestamp_int` sibling carries the same instant as UNIX microseconds. | | `timestamp_int` | int64 | The UNIX microseconds timestamp of when this market info was created. | | `title` | string | The human readable title for the market. | | `total_volume` | decimal | Contracts traded on this market across its lifetime. | | `trading` | boolean | Whether the market is accepting orders right now. | | `trading_filters` | array | The categorizations used for organizing trades, settlements and related items. A list, not an object. | | `volume24h` | decimal | Trade volume this market has had in the last 24 hours. In contracts. | --- # Orders > Place, inspect and cancel orders, individually or in batches. Source: https://docs.stxapp.io/api/rest/orders/ Place, inspect and cancel orders, individually or in batches. Cancels are requests, not guarantees: a fill can land while yours is in flight. There is no amend: to change an order, cancel it and place a new one. There is no atomic replace. 7 endpoints. - [List orders](/api/rest/orders/list-orders/) - [Place an order](/api/rest/orders/place-order/) - [Cancel all open orders](/api/rest/orders/cancel-all-orders/) - [Place several orders](/api/rest/orders/place-several-orders/) - [Cancel several orders](/api/rest/orders/cancel-multiple-orders/) - [Get an order](/api/rest/orders/get-order/) - [Cancel an order](/api/rest/orders/cancel-order/) --- # Cancel all open orders > Cancel every resting order on the account. Source: https://docs.stxapp.io/api/rest/orders/cancel-all-orders/ Cancel every resting order on the account. Requires a `read_write` key. ```http DELETE /api/v1/orders/all ``` Send it with your own demo key: [Try it](/quick-start/?op=orders_all_delete#try-it). :::tip[In the SDKs] - TypeScript: [`STX.cancelAllOrders()`](/sdks/typescript/reference/stx/#cancelallorders) - Python: [`STX.cancel_all_orders()`](/sdks/python/reference/stx/#cancel_all_orders) - C#: [`STXOrderService.CancelAllOrdersAsync()`](/sdks/csharp/reference/trading/#stxorderservice) ::: ## Request body | Field | Type | Required | Description | |---|---|---|---| | `geo_location` | string,null | no | Location code from the STX geolocation check, covering this cancel. Read only in environments that enforce geo-fencing, and ignored elsewhere. Where it is read, omitting it is a 422 (`Placing Order(s) from this IP address … is disallowed.`) unless that environment accepts the network location the request arrives from in its place, which it can refuse for an address identified as a VPN, proxy or hosting provider. Do not rely on omitting it: send the code whenever you have one. A code that is sent is always checked, so a bad one is a 422 even where omitting it would have been accepted. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `400` | A parameter was missing or invalid. | Error | | `401` | Missing, malformed or unrecognized signature, or a timestamp outside the 30-second window. Body: {"error":"Missing or invalid API key credentials"} | Error | | `403` | This API key does not have write access. The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | | `422` | The cancel was refused, for example by the location check or because the order can no longer be cancelled. The body's `error` says why. | Error | ## Example Request: ```bash curl --request DELETE \ --url 'https://demo.stxapp.io/api/v1/orders/all' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' \ --header 'Content-Type: application/json' \ --data '{ "geo_location": null }' ``` Response `200`: ```json { "cancellations": [ { "order_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "status": "cancelled" } ] } ``` ### Response fields | Field | Type | Description | |---|---|---| | `order_id` | uuid | The order this outcome relates to. | | `status` | string | The order status after cancelling, or a reason it was not cancelled, such as "Order not found" for an unknown id. | --- # Cancel several orders > Cancel a specific set of orders in one call. Source: https://docs.stxapp.io/api/rest/orders/cancel-multiple-orders/ Cancel a specific set of orders in one call. `batched` is a literal path segment, never read as an `order_id`. Requires a `read_write` key. ```http DELETE /api/v1/orders/batched ``` Send it with your own demo key: [Try it](/quick-start/?op=orders_batched_delete#try-it). :::tip[In the SDKs] - TypeScript: [`STX.cancelOrders()`](/sdks/typescript/reference/stx/#cancelorders) - Python: [`STX.cancel_orders()`](/sdks/python/reference/stx/#cancel_orders) - C#: [`STXOrderService.CancelOrdersAsync()`](/sdks/csharp/reference/trading/#stxorderservice) ::: ## Request body | Field | Type | Required | Description | |---|---|---|---| | `geo_location` | string,null | no | Location code from the STX geolocation check, covering every order in the request. Read only in environments that enforce geo-fencing, and ignored elsewhere. Where it is read, omitting it is a 422 (`Placing Order(s) from this IP address … is disallowed.`) unless that environment accepts the network location the request arrives from in its place, which it can refuse for an address identified as a VPN, proxy or hosting provider. Do not rely on omitting it: send the code whenever you have one. A code that is sent is always checked, so a bad one is a 422 even where omitting it would have been accepted. | | `orders` | object[] | **yes** | The orders to cancel. Each entry is an object carrying an `order_id`, not a bare id string. A flat `{"order_ids": [...]}` is the common first guess and returns `400 {"error": "orders is required"}`, which does not hint that the field exists with a different shape. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `400` | A parameter was missing or invalid. | Error | | `401` | Missing, malformed or unrecognized signature, or a timestamp outside the 30-second window. Body: {"error":"Missing or invalid API key credentials"} | Error | | `403` | This API key does not have write access. The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | | `422` | The cancel was refused, for example by the location check or because the order can no longer be cancelled. The body's `error` says why. | Error | ## Example Request: ```bash curl --request DELETE \ --url 'https://demo.stxapp.io/api/v1/orders/batched' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' \ --header 'Content-Type: application/json' \ --data '{ "geo_location": null, "orders": [ { "order_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d" } ] }' ``` Response `200`: ```json { "cancellations": [ { "order_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "status": "cancelled" } ] } ``` ### Response fields | Field | Type | Description | |---|---|---| | `order_id` | uuid | The order this outcome relates to. | | `status` | string | The order status after cancelling, or a reason it was not cancelled, such as "Order not found" for an unknown id. | --- # Cancel an order > Cancel one resting order by order_id. Source: https://docs.stxapp.io/api/rest/orders/cancel-order/ Cancel one resting order by `order_id`. A cancel is a request, not a guarantee: a fill can land while yours is in flight. An `order_id` the account does not own returns 404; every other rejection is a 422. Requires a `read_write` key. ```http DELETE /api/v1/orders/{order_id} ``` Send it with your own demo key: [Try it](/quick-start/?op=orders__order_id_delete#try-it). :::tip[In the SDKs] - TypeScript: [`STX.cancelOrder()`](/sdks/typescript/reference/stx/#cancelorder) - Python: [`STX.cancel_order()`](/sdks/python/reference/stx/#cancel_order) - C#: [`STXOrderService.CancelOrderAsync()`](/sdks/csharp/reference/trading/#stxorderservice) ::: ## Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `order_id` | `path` | string (uuid) | **yes** | The order's UUID. | ## Request body | Field | Type | Required | Description | |---|---|---|---| | `geo_location` | string,null | no | Location code from the STX geolocation check, covering this cancel. Read only in environments that enforce geo-fencing, and ignored elsewhere. Where it is read, omitting it is a 422 (`Placing Order(s) from this IP address … is disallowed.`) unless that environment accepts the network location the request arrives from in its place, which it can refuse for an address identified as a VPN, proxy or hosting provider. Do not rely on omitting it: send the code whenever you have one. A code that is sent is always checked, so a bad one is a 422 even where omitting it would have been accepted. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `400` | A parameter was missing or invalid. | Error | | `401` | Missing, malformed or unrecognized signature, or a timestamp outside the 30-second window. Body: {"error":"Missing or invalid API key credentials"} | Error | | `403` | This API key does not have write access. The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | | `404` | No such record on this account. | Error | | `422` | The cancel was refused, for example by the location check or because the order can no longer be cancelled. The body's `error` says why. | Error | ## Example Request: ```bash curl --request DELETE \ --url 'https://demo.stxapp.io/api/v1/orders/{order_id}' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' \ --header 'Content-Type: application/json' \ --data '{ "geo_location": null }' ``` Response `200`: ```json { "order_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "status": "cancelled" } ``` ### Response fields | Field | Type | Description | |---|---|---| | `order_id` | uuid | The order that was cancelled. | | `status` | string | The order's status after cancelling. | --- # Get an order > One order by order_id. Source: https://docs.stxapp.io/api/rest/orders/get-order/ One order by `order_id`. An `order_id` belonging to another account returns 404, not 403: the API never confirms the existence of something the caller does not own. ```http GET /api/v1/orders/{order_id} ``` Send it with your own demo key: [Try it](/quick-start/?op=orders__id_get#try-it). :::tip[In the SDKs] - TypeScript: [`STX.order()`](/sdks/typescript/reference/stx/#order) - Python: [`STX.order()`](/sdks/python/reference/stx/#order) ::: ## Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `order_id` | `path` | string (uuid) | **yes** | The order's UUID. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `400` | A parameter was missing or invalid. | Error | | `401` | Missing, malformed or unrecognized signature, or a timestamp outside the 30-second window. Body: {"error":"Missing or invalid API key credentials"} | Error | | `403` | The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | | `404` | No such record on this account. | Error | ## Example Request: ```bash curl --request GET \ --url 'https://demo.stxapp.io/api/v1/orders/{order_id}' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' ``` Response `200`: ```json { "order": { "accepted_at": null, "action": "buy", "amount": "0.6700", "avg_price": "0.6700", "cancellation_reason": null, "client_order_id": null, "delayed_until": null, "device_id": null, "expiration": null, "expiration_time": null, "expires_at": null, "filled": "2.00", "filled_amount": "0.6700", "filled_percentage": 0, "fix_order": false, "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "inserted_at": 0, "ip_address": null, "market_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "odds_type": null, "odds_value": null, "order_type": "limit", "placed_pre_start": false, "price": "0.4200000", "quantity": "2.00", "rejection_reason": null, "status": "accepted", "time": "2026-08-25T04:42:46.242093Z", "total_value": "0.6700" } } ``` ### Response fields | Field | Type | Description | |---|---|---| | `accepted_at` | int64 | When the matching engine accepted the order. `null` while the order is still pending. UNIX microseconds. | | `action` | string | The action of the order, either `buy` or `sell`. | | `amount` | decimal | Order size in dollars, for an order entered by amount rather than by contract quantity. `null` for orders placed through this API, which always take a quantity. | | `avg_price` | decimal | Volume-weighted average fill price. `null` until the order has its first fill. In dollars. | | `cancellation_reason` | string | The cancellation reason, if the order was cancelled. | | `client_order_id` | string | Your own identifier, echoed back unchanged, or `null` if you sent none. A free-form string, not a UUID, and set by REST and FIX callers alike. | | `delayed_until` | int64 | The extended deadline for a delayed order. UNIX microseconds. | | `device_id` | string | The device from which this order was placed. | | `expiration` | string | The expiration condition for the order. | | `expiration_time` | int64 | The expiration time for time-based expiration. UNIX microseconds. | | `expires_at` | int64 | When the contracts this order trades expire, as UNIX microseconds: the market's expiration, copied onto the order when it is placed. `null` when the market has no event. Not the same as `expiration_time`. | | `filled` | decimal | Contracts filled so far. Compare with `quantity` to get remaining size. | | `filled_amount` | decimal | The portion of `amount` that has been filled. In dollars. | | `filled_percentage` | integer | The percentage of the contracts on the order that have been filled. A percentage, not money. | | `fix_order` | boolean | True if the order arrived over FIX rather than REST. | | `id` | uuid | Unique identifier for the record. | | `inserted_at` | int64 | Creation time, as UNIX microseconds. | | `ip_address` | string | The IP address from which this order was placed. | | `market_id` | uuid | The market this record relates to. | | `odds_type` | string | Legacy. The odds format (`decimal` or `american`) an order was entered in on an STX app. `null` for orders placed through this API. | | `odds_value` | string | Legacy. The odds an order was entered at on an STX app, as a string. `null` for orders placed through this API. | | `order_type` | string | The type of order, either `limit` or `market`. | | `placed_pre_start` | boolean | Whether the order was placed before the event started. | | `price` | decimal | Limit price, below the market's `max_price`; read that per market rather than assuming a ceiling. Absent for market orders. In dollars. Carries up to 7 decimal places; parse money with a variable-scale decimal type, not a fixed-width one. | | `quantity` | decimal | Order size in contracts. | | `rejection_reason` | string | The rejection reason, if the order was rejected. | | `status` | string | Order state, one of nine. See [Market and order status](/concepts/market-status/#order-statuses) for the full set and which are terminal. | | `time` | date-time | The ISO-8601 timestamp of the time the order was placed. | | `total_value` | decimal | Total premium across all fills on this order. In dollars. | --- # List orders > Orders for the authenticated account, most recent first. Source: https://docs.stxapp.io/api/rest/orders/list-orders/ Orders for the authenticated account, most recent first. ```http GET /api/v1/orders ``` Send it with your own demo key: [Try it](/quick-start/?op=orders_get#try-it). :::tip[In the SDKs] - TypeScript: [`STX.orders()`](/sdks/typescript/reference/stx/#orders) - Python: [`STX.orders()`](/sdks/python/reference/stx/#orders) - C#: [`STXOrderService.GetMyOrdersAsync()`](/sdks/csharp/reference/trading/#stxorderservice) ::: ## Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `order_ids` | `query` | string | no | Comma-separated order UUIDs. | | `client_order_ids` | `query` | string | no | Comma-separated client order ids you supplied on placement. | | `market_ids` | `query` | string | no | Comma-separated market UUIDs. | | `status` | `query` | `created` \| `requested` \| `accepted` \| `delayed` \| `open` \| `filled` \| `rejected` \| `cancelled` \| `partially_cancelled`[] | no | Order statuses, lowercase. An unknown value is a 400. Note that `open` alone does not mean "my working orders": on a `pre_open` market an order rests at `accepted`, so filter on `created,requested,accepted,delayed,open` to list everything still live. | | `limit` | `query` | integer | no | Rows per page. Defaults to 100 and is silently clamped to 200; a larger value is not an error. | | `cursor` | `query` | string | no | Cursor from the previous response. Omit for the first page. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `400` | A parameter was missing or invalid. | Error | | `401` | Missing, malformed or unrecognized signature, or a timestamp outside the 30-second window. Body: {"error":"Missing or invalid API key credentials"} | Error | | `403` | The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | ## Example Request: ```bash curl --request GET \ --url 'https://demo.stxapp.io/api/v1/orders' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' ``` Response `200`: ```json { "cursor": null, "orders": [ { "accepted_at": null, "action": "buy", "amount": "0.6700", "avg_price": "0.6700", "cancellation_reason": null, "client_order_id": null, "delayed_until": null, "device_id": null, "expiration": null, "expiration_time": null, "expires_at": null, "filled": "2.00", "filled_amount": "0.6700", "filled_percentage": 0, "fix_order": false, "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "inserted_at": 0, "ip_address": null, "market_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "odds_type": null, "odds_value": null, "order_type": "limit", "placed_pre_start": false, "price": "0.4200000", "quantity": "2.00", "rejection_reason": null, "status": "accepted", "time": "2026-08-25T04:42:46.242093Z", "total_value": "0.6700" } ] } ``` ### Response fields | Field | Type | Description | |---|---|---| | `cursor` | string | Opaque cursor for the next page. Pass the value from the previous response; omit for the first page. `null` once there are no more pages. | | `accepted_at` | int64 | When the matching engine accepted the order. `null` while the order is still pending. UNIX microseconds. | | `action` | string | The action of the order, either `buy` or `sell`. | | `amount` | decimal | Order size in dollars, for an order entered by amount rather than by contract quantity. `null` for orders placed through this API, which always take a quantity. | | `avg_price` | decimal | Volume-weighted average fill price. `null` until the order has its first fill. In dollars. | | `cancellation_reason` | string | The cancellation reason, if the order was cancelled. | | `client_order_id` | string | Your own identifier, echoed back unchanged, or `null` if you sent none. A free-form string, not a UUID, and set by REST and FIX callers alike. | | `delayed_until` | int64 | The extended deadline for a delayed order. UNIX microseconds. | | `device_id` | string | The device from which this order was placed. | | `expiration` | string | The expiration condition for the order. | | `expiration_time` | int64 | The expiration time for time-based expiration. UNIX microseconds. | | `expires_at` | int64 | When the contracts this order trades expire, as UNIX microseconds: the market's expiration, copied onto the order when it is placed. `null` when the market has no event. Not the same as `expiration_time`. | | `filled` | decimal | Contracts filled so far. Compare with `quantity` to get remaining size. | | `filled_amount` | decimal | The portion of `amount` that has been filled. In dollars. | | `filled_percentage` | integer | The percentage of the contracts on the order that have been filled. A percentage, not money. | | `fix_order` | boolean | True if the order arrived over FIX rather than REST. | | `id` | uuid | Unique identifier for the record. | | `inserted_at` | int64 | Creation time, as UNIX microseconds. | | `ip_address` | string | The IP address from which this order was placed. | | `market_id` | uuid | The market this record relates to. | | `odds_type` | string | Legacy. The odds format (`decimal` or `american`) an order was entered in on an STX app. `null` for orders placed through this API. | | `odds_value` | string | Legacy. The odds an order was entered at on an STX app, as a string. `null` for orders placed through this API. | | `order_type` | string | The type of order, either `limit` or `market`. | | `placed_pre_start` | boolean | Whether the order was placed before the event started. | | `price` | decimal | Limit price, below the market's `max_price`; read that per market rather than assuming a ceiling. Absent for market orders. In dollars. Carries up to 7 decimal places; parse money with a variable-scale decimal type, not a fixed-width one. | | `quantity` | decimal | Order size in contracts. | | `rejection_reason` | string | The rejection reason, if the order was rejected. | | `status` | string | Order state, one of nine. See [Market and order status](/concepts/market-status/#order-statuses) for the full set and which are terminal. | | `time` | date-time | The ISO-8601 timestamp of the time the order was placed. | | `total_value` | decimal | Total premium across all fills on this order. In dollars. | --- # Place an order > Places a single order. Source: https://docs.stxapp.io/api/rest/orders/place-order/ Places a single order. The body is flat: the order's fields sit at the top level, not wrapped in a `user_order` key. Requires a `read_write` key. ```http POST /api/v1/orders ``` Send it with your own demo key: [Try it](/quick-start/?op=orders_post#try-it). :::tip[In the SDKs] - TypeScript: [`STX.placeOrder()`](/sdks/typescript/reference/stx/#placeorder) - Python: [`STX.place_order()`](/sdks/python/reference/stx/#place_order) - C#: [`STXOrderService.ConfirmOrderAsync()`](/sdks/csharp/reference/trading/#stxorderservice) ::: ## Request body | Field | Type | Required | Description | |---|---|---|---| | `action` | `buy` \| `sell` | **yes** | Which side of the book the order takes. | | `cancel_on_disconnect` | boolean,null | no | Cancel this order if the `orders` channel stops heartbeating. See the cancel_on_disconnect guide. | | `client_order_id` | string,null | no | Your own reference, echoed back unchanged. A free-form string, not a UUID; FIX clients routinely send ids like `my-order-001`. | | `device_id` | string,null | no | Identifier for the device placing the order. | | `expiration` | `good_till_start` \| `good_till_time` \| `null` | no | When the order should stop resting. | | `expiration_time` | integer,null (int64) | no | The moment a `good_till_time` order expires. Required when `expiration` is `good_till_time`. UNIX microseconds. | | `geo_location` | string,null | no | Location code from the STX geolocation check, covering this order. Inside a `POST /api/v1/orders/batched` leg it is ignored; send the batch's top-level `geo_location` instead. Read only in environments that enforce geo-fencing, and ignored elsewhere. Where it is read, omitting it is a 422 (`Placing Order(s) from this IP address … is disallowed.`) unless that environment accepts the network location the request arrives from in its place, which it can refuse for an address identified as a VPN, proxy or hosting provider. Do not rely on omitting it: send the code whenever you have one. A code that is sent is always checked, so a bad one is a 422 even where omitting it would have been accepted. | | `market_id` | string (uuid) | **yes** | The market to place the order on. | | `order_type` | `limit` \| `market` | **yes** | Whether the order takes a price or rests on the book. | | `price` | string,null (decimal) | no | Limit price in dollars, as a string: `"0.42"` is 42 cents. A number is rejected outright rather than reinterpreted, because a bare `42` could mean 42 cents or 42 dollars and the wrong reading is off by 100x. Must be strictly less than the market's `max_price`; read that per market rather than assuming a fixed ceiling. Required for `limit` orders and unused by `market` orders, but validated whenever it is present: a malformed or sub-cent price is a 400 on a `market` order too, rather than being quietly dropped. An explicit `null` is accepted there, exactly as omitting the key is. Must be greater than zero and a whole number of cents: at most two decimal places, not counting trailing zeros. So "0.42" and "0.4200" are the same accepted value, and "0.001" is rejected. | | `quantity` | string | **yes** | Number of contracts, greater than zero, as a decimal string: `"10"` and `"10.00"` are read the same. A number is rejected rather than converted, the same rule `price` follows, though for a different reason: a float arrives as a binary double, so the size that rests on the book would not always be the size that was sent. More than nine decimal places is rounded. Responses always return it as a string. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `400` | A parameter was missing or invalid. | Error | | `401` | Missing, malformed or unrecognized signature, or a timestamp outside the 30-second window. Body: {"error":"Missing or invalid API key credentials"} | Error | | `403` | This API key does not have write access. The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | | `422` | The order was refused, for example a limit price at or above the market's `max_price`, insufficient funds or a failed location check. The body's `error` says why. | Error | ## Example Request: ```bash curl --request POST \ --url 'https://demo.stxapp.io/api/v1/orders' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' \ --header 'Content-Type: application/json' \ --data '{ "action": "buy", "cancel_on_disconnect": null, "client_order_id": null, "device_id": null, "expiration": null, "expiration_time": null, "geo_location": null, "market_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "order_type": "limit", "price": "0.42", "quantity": "10" }' ``` Response `200`: ```json { "order": { "accepted_at": null, "action": "buy", "amount": "0.6700", "avg_price": "0.6700", "cancellation_reason": null, "client_order_id": null, "delayed_until": null, "device_id": null, "expiration": null, "expiration_time": null, "expires_at": null, "filled": "2.00", "filled_amount": "0.6700", "filled_percentage": 0, "fix_order": false, "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "inserted_at": 0, "ip_address": null, "market_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "odds_type": null, "odds_value": null, "order_type": "limit", "placed_pre_start": false, "price": "0.4200000", "quantity": "2.00", "rejection_reason": null, "status": "accepted", "time": "2026-08-25T04:42:46.242093Z", "total_value": "0.6700" } } ``` ### Response fields | Field | Type | Description | |---|---|---| | `accepted_at` | int64 | When the matching engine accepted the order. `null` while the order is still pending. UNIX microseconds. | | `action` | string | The action of the order, either `buy` or `sell`. | | `amount` | decimal | Order size in dollars, for an order entered by amount rather than by contract quantity. `null` for orders placed through this API, which always take a quantity. | | `avg_price` | decimal | Volume-weighted average fill price. `null` until the order has its first fill. In dollars. | | `cancellation_reason` | string | The cancellation reason, if the order was cancelled. | | `client_order_id` | string | Your own identifier, echoed back unchanged, or `null` if you sent none. A free-form string, not a UUID, and set by REST and FIX callers alike. | | `delayed_until` | int64 | The extended deadline for a delayed order. UNIX microseconds. | | `device_id` | string | The device from which this order was placed. | | `expiration` | string | The expiration condition for the order. | | `expiration_time` | int64 | The expiration time for time-based expiration. UNIX microseconds. | | `expires_at` | int64 | When the contracts this order trades expire, as UNIX microseconds: the market's expiration, copied onto the order when it is placed. `null` when the market has no event. Not the same as `expiration_time`. | | `filled` | decimal | Contracts filled so far. Compare with `quantity` to get remaining size. | | `filled_amount` | decimal | The portion of `amount` that has been filled. In dollars. | | `filled_percentage` | integer | The percentage of the contracts on the order that have been filled. A percentage, not money. | | `fix_order` | boolean | True if the order arrived over FIX rather than REST. | | `id` | uuid | Unique identifier for the record. | | `inserted_at` | int64 | Creation time, as UNIX microseconds. | | `ip_address` | string | The IP address from which this order was placed. | | `market_id` | uuid | The market this record relates to. | | `odds_type` | string | Legacy. The odds format (`decimal` or `american`) an order was entered in on an STX app. `null` for orders placed through this API. | | `odds_value` | string | Legacy. The odds an order was entered at on an STX app, as a string. `null` for orders placed through this API. | | `order_type` | string | The type of order, either `limit` or `market`. | | `placed_pre_start` | boolean | Whether the order was placed before the event started. | | `price` | decimal | Limit price, below the market's `max_price`; read that per market rather than assuming a ceiling. Absent for market orders. In dollars. Carries up to 7 decimal places; parse money with a variable-scale decimal type, not a fixed-width one. | | `quantity` | decimal | Order size in contracts. | | `rejection_reason` | string | The rejection reason, if the order was rejected. | | `status` | string | Order state, one of nine. See [Market and order status](/concepts/market-status/#order-statuses) for the full set and which are terminal. | | `time` | date-time | The ISO-8601 timestamp of the time the order was placed. | | `total_value` | decimal | Total premium across all fills on this order. In dollars. | --- # Place several orders > Places up to 100 orders (configurable) in one call. Source: https://docs.stxapp.io/api/rest/orders/place-several-orders/ Places up to 100 orders (configurable) in one call. `orders` is a list, each entry shaped exactly like the single `POST /api/v1/orders` body. `batched` is a literal path segment. One geo check covers the batch; each leg is placed independently, so a rejected leg does not fail the others; the `results` array has one entry per order, in request order, each either `{order}` on success or `{errors}` on rejection. A malformed body (a bad field in any leg, empty, over the limit, or `orders` missing/not a list) is a 400 and places nothing. Requires a `read_write` key. ```http POST /api/v1/orders/batched ``` Send it with your own demo key: [Try it](/quick-start/?op=orders_batched_post#try-it). ## Request body | Field | Type | Required | Description | |---|---|---|---| | `geo_location` | string,null | no | Location code from the STX geolocation check, covering the whole batch, checked once for the request, not per leg. Read only in environments that enforce geo-fencing, and ignored elsewhere. Where it is read, omitting it is a 422 (`Placing Order(s) from this IP address … is disallowed.`) unless that environment accepts the network location the request arrives from in its place, which it can refuse for an address identified as a VPN, proxy or hosting provider. Do not rely on omitting it: send the code whenever you have one. A code that is sent is always checked, so a bad one is a 422 even where omitting it would have been accepted. | | `orders` | NewOrder[] | **yes** | The orders to place, each shaped exactly like the single `POST /api/v1/orders` body (a `NewOrder`). At most 100 (configurable) per request. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `400` | A parameter was missing or invalid. | Error | | `401` | Missing, malformed or unrecognized signature, or a timestamp outside the 30-second window. Body: {"error":"Missing or invalid API key credentials"} | Error | | `403` | This API key does not have write access. The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | | `422` | The batch was refused as a whole, for example by the location check, and nothing was placed. A leg refused on its own is reported in `results`, not as a 422. | Error | ## Example Request: ```bash curl --request POST \ --url 'https://demo.stxapp.io/api/v1/orders/batched' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' \ --header 'Content-Type: application/json' \ --data '{ "geo_location": null, "orders": [ { "action": "buy", "cancel_on_disconnect": null, "client_order_id": null, "device_id": null, "expiration": null, "expiration_time": null, "geo_location": null, "market_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "order_type": "limit", "price": "0.42", "quantity": "10" } ] }' ``` Response `200`: ```json { "results": [ "string" ] } ``` ### Response fields | Field | Type | Description | |---|---|---| | `results` | array | One entry per requested order, in request order. | --- # Portfolio > Money that has already moved: settlements, deposits, withdrawals, fees, adjustments and loyalty entries. Source: https://docs.stxapp.io/api/rest/portfolio/ Money that has already moved: settlements, deposits, withdrawals, fees, adjustments and loyalty entries. Your current balance is not here: it is a live account figure, served by `GET /api/v1/account/balance` under **Account** (the same snapshot the `balances:{user_id}` WebSocket channel pushes), while exposure broken out per market is `GET /api/v1/account/market_stats`. 6 endpoints. - [List adjustments](/api/rest/portfolio/list-adjustments/) - [List deposits](/api/rest/portfolio/list-deposits/) - [List fees](/api/rest/portfolio/list-fees/) - [List loyalty entries](/api/rest/portfolio/list-loyalty/) - [List settlements](/api/rest/portfolio/list-settlements/) - [List withdrawals](/api/rest/portfolio/list-withdrawals/) --- # List adjustments > Manual corrections applied to the account balance by an administrator, most recent first, enriched with the reason recorded against each one. Source: https://docs.stxapp.io/api/rest/portfolio/list-adjustments/ Manual corrections applied to the account balance by an administrator, most recent first, enriched with the reason recorded against each one. `amount` may be either sign. The reason is `null` for an adjustment with no associated payment record. ```http GET /api/v1/portfolio/adjustments ``` Send it with your own demo key: [Try it](/quick-start/?op=portfolio_adjustments_get#try-it). :::tip[In the SDKs] - TypeScript: [`STX.adjustments()`](/sdks/typescript/reference/stx/#adjustments) - Python: [`STX.adjustments()`](/sdks/python/reference/stx/#adjustments) ::: ## Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `limit` | `query` | integer | no | Rows per page. Defaults to 100 and is silently clamped to 200; a larger value is not an error. | | `cursor` | `query` | string | no | Cursor from the previous response. Omit for the first page. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `401` | Missing, malformed or unrecognized signature, or a timestamp outside the 30-second window. Body: {"error":"Missing or invalid API key credentials"} | Error | | `403` | The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | ## Example Request: ```bash curl --request GET \ --url 'https://demo.stxapp.io/api/v1/portfolio/adjustments' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' ``` Response `200`: ```json { "adjustments": [ { "account_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "amount": "0.6700", "event_id": null, "fee_id": null, "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "inserted_at": 0, "market_id": null, "method": null, "payment_id": null, "points": null, "reason": null, "settlement_id": null, "sub_method": null, "time": "2026-08-25T04:42:46.242093Z", "type": "deposit" } ], "cursor": null } ``` ### Response fields | Field | Type | Description | |---|---|---| | `cursor` | string | Opaque cursor for the next page. Pass the value from the previous response; omit for the first page. `null` once there are no more pages. | | `account_id` | uuid | The account the record belongs to. | | `amount` | decimal | The monetary value the entry was calculated from. In dollars. | | `event_id` | uuid | The event the market belongs to. | | `fee_id` | uuid | The fee that generated this entry, when it came from one. | | `id` | uuid | Unique identifier for the record. | | `inserted_at` | int64 | Creation time, as UNIX microseconds. | | `market_id` | uuid | The market this record relates to. | | `method` | string | The payment method the provider reports for this transaction. | | `payment_id` | uuid | The payment that generated this entry, when it came from one. | | `points` | number | Points added by this entry. Negative when points were spent. | | `reason` | string | The provider's reason or description for the transaction. | | `settlement_id` | uuid | The settlement that generated this entry, when it came from one. | | `sub_method` | string | For Interac, whether the transfer was a `Send` or a `Request`. | | `time` | date-time | When the entry was recorded. | | `type` | string | What moved the money or the points, for example a deposit, a trade fee, a referral, or a manual adjustment. Each endpoint returns only its own subset of these. | --- # List deposits > Deposit history, most recent first, enriched with the payment method and the provider's reason for each transaction. Source: https://docs.stxapp.io/api/rest/portfolio/list-deposits/ Deposit history, most recent first, enriched with the payment method and the provider's reason for each transaction. ```http GET /api/v1/portfolio/deposits ``` Send it with your own demo key: [Try it](/quick-start/?op=portfolio_deposits_get#try-it). :::tip[In the SDKs] - TypeScript: [`STX.deposits()`](/sdks/typescript/reference/stx/#deposits) - Python: [`STX.deposits()`](/sdks/python/reference/stx/#deposits) ::: ## Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `limit` | `query` | integer | no | Rows per page. Defaults to 100 and is silently clamped to 200; a larger value is not an error. | | `cursor` | `query` | string | no | Cursor from the previous response. Omit for the first page. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `401` | Missing, malformed or unrecognized signature, or a timestamp outside the 30-second window. Body: {"error":"Missing or invalid API key credentials"} | Error | | `403` | The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | ## Example Request: ```bash curl --request GET \ --url 'https://demo.stxapp.io/api/v1/portfolio/deposits' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' ``` Response `200`: ```json { "cursor": null, "deposits": [ { "account_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "amount": "0.6700", "event_id": null, "fee_id": null, "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "inserted_at": 0, "market_id": null, "method": null, "payment_id": null, "points": null, "reason": null, "settlement_id": null, "sub_method": null, "time": "2026-08-25T04:42:46.242093Z", "type": "deposit" } ] } ``` ### Response fields | Field | Type | Description | |---|---|---| | `cursor` | string | Opaque cursor for the next page. Pass the value from the previous response; omit for the first page. `null` once there are no more pages. | | `account_id` | uuid | The account the record belongs to. | | `amount` | decimal | The monetary value the entry was calculated from. In dollars. | | `event_id` | uuid | The event the market belongs to. | | `fee_id` | uuid | The fee that generated this entry, when it came from one. | | `id` | uuid | Unique identifier for the record. | | `inserted_at` | int64 | Creation time, as UNIX microseconds. | | `market_id` | uuid | The market this record relates to. | | `method` | string | The payment method the provider reports for this transaction. | | `payment_id` | uuid | The payment that generated this entry, when it came from one. | | `points` | number | Points added by this entry. Negative when points were spent. | | `reason` | string | The provider's reason or description for the transaction. | | `settlement_id` | uuid | The settlement that generated this entry, when it came from one. | | `sub_method` | string | For Interac, whether the transfer was a `Send` or a `Request`. | | `time` | date-time | When the entry was recorded. | | `type` | string | What moved the money or the points, for example a deposit, a trade fee, a referral, or a manual adjustment. Each endpoint returns only its own subset of these. | --- # List fees > Fees charged against the account, most recent first, across every fee source: per-trade, per-settlement, market-group and event. Source: https://docs.stxapp.io/api/rest/portfolio/list-fees/ Fees charged against the account, most recent first, across every fee source: per-trade, per-settlement, market-group and event. Scoped to `fee` and `fee_refund` entries. `amount` is the signed effect on the balance, so fees paid net out by summing it: a `fee` is never positive and a `fee_refund` is never negative. A `fee` of `0.0000` is a real entry meaning the fee was assessed and came to nothing: your profit across the group was not positive, or your rate is zero. Fees still pending against the balance are not included. ```http GET /api/v1/portfolio/fees ``` Send it with your own demo key: [Try it](/quick-start/?op=portfolio_fees_get#try-it). :::tip[In the SDKs] - TypeScript: [`STX.fees()`](/sdks/typescript/reference/stx/#fees) - Python: [`STX.fees()`](/sdks/python/reference/stx/#fees) ::: ## Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `limit` | `query` | integer | no | Rows per page. Defaults to 100 and is silently clamped to 200; a larger value is not an error. | | `cursor` | `query` | string | no | Cursor from the previous response. Omit for the first page. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `401` | Missing, malformed or unrecognized signature, or a timestamp outside the 30-second window. Body: {"error":"Missing or invalid API key credentials"} | Error | | `403` | The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | ## Example Request: ```bash curl --request GET \ --url 'https://demo.stxapp.io/api/v1/portfolio/fees' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' ``` Response `200`: ```json { "cursor": null, "fees": [ { "account_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "amount": "0.6700", "event_id": null, "fee_id": null, "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "inserted_at": 0, "market_id": null, "payment_id": null, "points": null, "settlement_id": null, "time": "2026-08-25T04:42:46.242093Z", "type": "deposit" } ] } ``` ### Response fields | Field | Type | Description | |---|---|---| | `cursor` | string | Opaque cursor for the next page. Pass the value from the previous response; omit for the first page. `null` once there are no more pages. | | `account_id` | uuid | The account the record belongs to. | | `amount` | decimal | The monetary value the entry was calculated from. In dollars. | | `event_id` | uuid | The event the market belongs to. | | `fee_id` | uuid | The fee that generated this entry, when it came from one. | | `id` | uuid | Unique identifier for the record. | | `inserted_at` | int64 | Creation time, as UNIX microseconds. | | `market_id` | uuid | The market this record relates to. | | `payment_id` | uuid | The payment that generated this entry, when it came from one. | | `points` | number | Points added by this entry. Negative when points were spent. | | `settlement_id` | uuid | The settlement that generated this entry, when it came from one. | | `time` | date-time | When the entry was recorded. | | `type` | string | What moved the money or the points, for example a deposit, a trade fee, a referral, or a manual adjustment. Each endpoint returns only its own subset of these. | --- # List loyalty entries > Loyalty point transactions, most recent first. Source: https://docs.stxapp.io/api/rest/portfolio/list-loyalty/ Loyalty point transactions, most recent first. Scoped to rollup and referral entries only. This endpoint returns the entries themselves, not a points balance or a tier. ```http GET /api/v1/portfolio/loyalty ``` Send it with your own demo key: [Try it](/quick-start/?op=portfolio_loyalty_get#try-it). :::tip[In the SDKs] - TypeScript: [`STX.loyalty()`](/sdks/typescript/reference/stx/#loyalty) - Python: [`STX.loyalty()`](/sdks/python/reference/stx/#loyalty) ::: ## Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `limit` | `query` | integer | no | Rows per page. Defaults to 100 and is silently clamped to 200; a larger value is not an error. | | `cursor` | `query` | string | no | Cursor from the previous response. Omit for the first page. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `401` | Missing, malformed or unrecognized signature, or a timestamp outside the 30-second window. Body: {"error":"Missing or invalid API key credentials"} | Error | | `403` | The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | ## Example Request: ```bash curl --request GET \ --url 'https://demo.stxapp.io/api/v1/portfolio/loyalty' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' ``` Response `200`: ```json { "cursor": null, "loyalty": [ { "account_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "amount": "0.6700", "event_id": null, "fee_id": null, "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "inserted_at": 0, "market_id": null, "payment_id": null, "points": null, "referee_account_id": null, "referrer_account_id": null, "settlement_id": null, "time": "2026-08-25T04:42:46.242093Z", "type": "deposit" } ] } ``` ### Response fields | Field | Type | Description | |---|---|---| | `cursor` | string | Opaque cursor for the next page. Pass the value from the previous response; omit for the first page. `null` once there are no more pages. | | `account_id` | uuid | The account the record belongs to. | | `amount` | decimal | The monetary value the entry was calculated from. In dollars. | | `event_id` | uuid | The event the market belongs to. | | `fee_id` | uuid | The fee that generated this entry, when it came from one. | | `id` | uuid | Unique identifier for the record. | | `inserted_at` | int64 | Creation time, as UNIX microseconds. | | `market_id` | uuid | The market this record relates to. | | `payment_id` | uuid | The payment that generated this entry, when it came from one. | | `points` | number | Points added by this entry. Negative when points were spent. | | `referee_account_id` | uuid | For referral entries, the account that was referred. | | `referrer_account_id` | uuid | For referral entries, the account that referred. | | `settlement_id` | uuid | The settlement that generated this entry, when it came from one. | | `time` | date-time | When the entry was recorded. | | `type` | string | What moved the money or the points, for example a deposit, a trade fee, a referral, or a manual adjustment. Each endpoint returns only its own subset of these. | --- # List settlements > Settled positions and their payouts, most recent first. Source: https://docs.stxapp.io/api/rest/portfolio/list-settlements/ Settled positions and their payouts, most recent first. ```http GET /api/v1/portfolio/settlements ``` Send it with your own demo key: [Try it](/quick-start/?op=portfolio_settlements_get#try-it). :::tip[In the SDKs] - TypeScript: [`STX.settlements()`](/sdks/typescript/reference/stx/#settlements) - Python: [`STX.settlements()`](/sdks/python/reference/stx/#settlements) - C#: [`STXSettlementService.GetMySettlementsAsync()`](/sdks/csharp/reference/account/#stxsettlementservice) ::: ## Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `market_ids` | `query` | string | no | Comma-separated market UUIDs. | | `type` | `query` | `closed_short` \| `closed_long` \| `expired_short` \| `expired_long` | no | Filter to one settlement type. | | `limit` | `query` | integer | no | Rows per page. Defaults to 100 and is silently clamped to 200; a larger value is not an error. | | `cursor` | `query` | string | no | Cursor from the previous response. Omit for the first page. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `400` | A parameter was missing or invalid. | Error | | `401` | Missing, malformed or unrecognized signature, or a timestamp outside the 30-second window. Body: {"error":"Missing or invalid API key credentials"} | Error | | `403` | The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | ## Example Request: ```bash curl --request GET \ --url 'https://demo.stxapp.io/api/v1/portfolio/settlements' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' ``` Response `200`: ```json { "cursor": null, "settlements": [ { "account_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "closing_placed_pre_start": null, "closing_price": "0.6700", "closing_trade_id": null, "closing_traded_pre_start": null, "fee": "0.6700", "gross_pnl": "0.6700", "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "inserted_at": 0, "market_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "opening_placed_pre_start": false, "opening_price": "0.6700", "opening_trade_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "opening_traded_pre_start": false, "pre_start": false, "quantity": "2.00", "realized_pnl": "0.6700", "settled_premium": "0.6700", "settled_risk": "0.6700", "time": "2026-08-25T04:42:46.242093Z", "type": "closed_short" } ] } ``` ### Response fields | Field | Type | Description | |---|---|---| | `cursor` | string | Opaque cursor for the next page. Pass the value from the previous response; omit for the first page. `null` once there are no more pages. | | `account_id` | uuid | The account the record belongs to. | | `closing_placed_pre_start` | boolean | Whether the closing trade's order was placed before the event started. | | `closing_price` | decimal | The price of the contracts when the position was closed. In dollars. | | `closing_trade_id` | uuid | The id of the trade that closed this settlement, if any. | | `closing_traded_pre_start` | boolean | Whether the closing trade was placed before the event started. | | `fee` | decimal | The fee charged for the settlement. In dollars. | | `gross_pnl` | decimal | The profit or loss associated with the position. In dollars. | | `id` | uuid | Unique identifier for the record. | | `inserted_at` | int64 | Creation time, as UNIX microseconds. | | `market_id` | uuid | The market this record relates to. | | `opening_placed_pre_start` | boolean | Whether the opening trade's order was placed before the event started. | | `opening_price` | decimal | The price of the contracts when the position was opened. In dollars. | | `opening_trade_id` | uuid | The id of the trade that opened this settlement. | | `opening_traded_pre_start` | boolean | Whether the opening trade was placed before the event started. | | `pre_start` | boolean | Whether the settlement was placed before the event started. | | `quantity` | decimal | The number of contracts that were settled. | | `realized_pnl` | decimal | The net profit or loss minus fees. In dollars. | | `settled_premium` | decimal | The amount of premium that was settled by the trade. In dollars. | | `settled_risk` | decimal | The amount of risk that was settled by the trade. In dollars. | | `time` | date-time | ISO-8601 timestamp of when the settlement was created. Same instant as `inserted_at`, which carries it as UNIX microseconds. | | `type` | string | The type of settlement. | --- # List withdrawals > Withdrawal history, most recent first, enriched with the payment method and the provider's reason for each transaction. Source: https://docs.stxapp.io/api/rest/portfolio/list-withdrawals/ Withdrawal history, most recent first, enriched with the payment method and the provider's reason for each transaction. ```http GET /api/v1/portfolio/withdrawals ``` Send it with your own demo key: [Try it](/quick-start/?op=portfolio_withdrawals_get#try-it). :::tip[In the SDKs] - TypeScript: [`STX.withdrawals()`](/sdks/typescript/reference/stx/#withdrawals) - Python: [`STX.withdrawals()`](/sdks/python/reference/stx/#withdrawals) ::: ## Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `limit` | `query` | integer | no | Rows per page. Defaults to 100 and is silently clamped to 200; a larger value is not an error. | | `cursor` | `query` | string | no | Cursor from the previous response. Omit for the first page. | ## Responses | Status | Description | Schema | |---|---|---| | `200` | Success | object | | `401` | Missing, malformed or unrecognized signature, or a timestamp outside the 30-second window. Body: {"error":"Missing or invalid API key credentials"} | Error | | `403` | The account behind this key is not active, for example it is pending approval or suspended. Body: {"error":"Your account is suspended. Contact support."}, the message naming the account's status. | Error | ## Example Request: ```bash curl --request GET \ --url 'https://demo.stxapp.io/api/v1/portfolio/withdrawals' \ --header 'X-STX-ACCESS-KEY: ' \ --header 'X-STX-ACCESS-TIMESTAMP: ' \ --header 'X-STX-ACCESS-SIGNATURE: ' ``` Response `200`: ```json { "cursor": null, "withdrawals": [ { "account_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "amount": "0.6700", "event_id": null, "fee_id": null, "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "inserted_at": 0, "market_id": null, "method": null, "payment_id": null, "points": null, "reason": null, "settlement_id": null, "sub_method": null, "time": "2026-08-25T04:42:46.242093Z", "type": "deposit" } ] } ``` ### Response fields | Field | Type | Description | |---|---|---| | `cursor` | string | Opaque cursor for the next page. Pass the value from the previous response; omit for the first page. `null` once there are no more pages. | | `account_id` | uuid | The account the record belongs to. | | `amount` | decimal | The monetary value the entry was calculated from. In dollars. | | `event_id` | uuid | The event the market belongs to. | | `fee_id` | uuid | The fee that generated this entry, when it came from one. | | `id` | uuid | Unique identifier for the record. | | `inserted_at` | int64 | Creation time, as UNIX microseconds. | | `market_id` | uuid | The market this record relates to. | | `method` | string | The payment method the provider reports for this transaction. | | `payment_id` | uuid | The payment that generated this entry, when it came from one. | | `points` | number | Points added by this entry. Negative when points were spent. | | `reason` | string | The provider's reason or description for the transaction. | | `settlement_id` | uuid | The settlement that generated this entry, when it came from one. | | `sub_method` | string | For Interac, whether the transfer was a `Send` or a `Request`. | | `time` | date-time | When the entry was recorded. | | `type` | string | What moved the money or the points, for example a deposit, a trade fee, a referral, or a manual adjustment. Each endpoint returns only its own subset of these. | --- # Changelog > Changes to the STX API, the SDKs, and these docs. Source: https://docs.stxapp.io/changelog/ What changed, and when. If you are building against the API, this is the page to check before you upgrade, and worth a look whenever something behaves differently than you expect. Entries headed with a name, such as C# SDK, are scoped to that; the rest are the API itself. There is one [feed](/changelog/rss.xml) for all of it, and each entry carries its scope as a category. Every page carries the build revision in its footer. Quote it if you report something that looks wrong, and we will know exactly what you were reading. Headings carry the documentation site's own version, written `docs-v1.2.0`. Product versions, such as the C# SDK, are named inside each entry. ## 2026-10-03 docs-v1.5.1 - **Combos (preview):** any account can request or quote, but not both on one request. Requesting, quoting, accepting, cancelling and cashing out need a `read_write` API key; a `read_only` key is refused with `insufficient_scope`. See [Combos](/concepts/combos/). ## 2026-10-03 docs-v1.5.0 **A User-Agent requirement and an early look at combos.** ### REST API - **Send a `User-Agent`.** REST requests and WebSocket handshakes without a `User-Agent` header are refused with `403`. Most HTTP libraries send one already; some WebSocket clients, such as Node's `ws`, do not unless you set it. See [Authentication](/api/authentication/). ### Combos (preview) Combos, one contract across several markets priced by request for quote, are documented as an early look so you can plan for them. They are **not available to try yet**, and the protocol may change before release. See [Combos](/concepts/combos/) and the [combo negotiation channel](/websockets/channels/combo-negotiation/). ## 2026-10-03 docs-v1.4.0 **The Python SDK is on PyPI.** ### Python SDK 0.6.0 **The Python SDK is on PyPI.** Trade on STX from Python with typed methods instead of hand-built, hand-signed requests. ```bash pip install stx-python ``` - A blocking client, `STX`, and an asyncio client, `AsyncSTX`, with a method for every REST route. - Requests and the WebSocket handshake are signed with your API key. There is no login call and no session to refresh. - Stream order books, trades and your own orders, fills, positions and balance on one connection that keeps itself alive and reconnects for you. - Walk long lists without handling pages yourself, with `iter_markets()`, `iter_orders()` and the rest. - Every response is a typed model, and money and quantities arrive as strings exactly as the API sends them, never floats. - Retries are safe: an order is never sent twice after an uncertain failure. - Pick an exchange by name, such as `region="us", env="demo"`, or set your key once in a credentials profile. Start with the [Python SDK guide](/sdks/python/). Runnable examples are in [stx-python-demo](https://github.com/stxapp/stx-python-demo). ## 2026-09-30 docs-v1.3.0 **The TypeScript SDK is on npm, two new WebSocket channels, and your positions over REST.** ### TypeScript SDK 0.6.3 **The TypeScript SDK is on npm.** Trade on STX from Node.js with typed methods instead of hand-built, hand-signed requests. ```bash npm install @stxapp/stx-typescript ``` - Read markets and your account, and place and cancel orders, with every response typed in your editor. - Stream order books, trades and your own orders, fills, balance and positions. The SDK signs the connection, keeps it alive and reconnects for you. - Keep one live, always current view of your account with `accountView()`. - Walk long lists without handling pages yourself, with `iterMarkets()`, `iterOrders()` and the rest. - Retries are safe: an order is never sent twice after an uncertain failure. - Set your key once, in environment variables or a credentials profile, and switch between the US and Ontario exchanges with one option. Start with the [TypeScript SDK guide](/sdks/typescript/). Since 0.6.0: - `balance()`, `positions()` and `placeOrders()` work on every STX exchange, with the same results everywhere. - `placeOrders()` checks the whole list before placing any order: an over-long list or a malformed order is refused with nothing placed. ### REST API - **`GET /api/v1/positions`** returns your open positions, largest first. It is the same body the `positions:{user_id}` channel sends on join, so one REST read can seed the state that channel then keeps current. See [List open positions](/api/rest/account/list-open-positions/). - **`GET /api/v1/fills` takes `order_ids`** and returns only the fills those orders produced. It combines with `market_ids` and `status`. See [List fills](/api/rest/fills/list-fills/). - **Name your client.** Send a `User-Agent` such as `acme-mm/1.4 (python/3.13)` on REST calls and the WebSocket handshake. It is optional and never rejected, and it lets us find your calls when you report something. See [Identify your client](/api/authentication/#identify-your-client). ### WebSockets - **`order_slip:{user_id}`** costs an order before you place it: risk, fee, and how much would fill at once and at which prices, pushed again whenever the book moves. Nothing is placed. It replaces `betslip:{user_id}`, which still works but sends money rounded to two decimals; moving is a topic change plus parsing money and quantities as decimal strings. See [Order slip](/websockets/channels/order-slip/). - **`events`** carries traded volume per event, one number across every market on it. No authentication. See [Events](/websockets/channels/events/). ### OAuth for apps Apps can act for STX members with their consent: the authorization code flow with PKCE, tokens limited to the scopes the member approved, and a member can disconnect at any time. See [ISV](/isv/). ## 2026-09-19 docs-v1.2.0 **C# SDK 1.6.0 and 1.6.1**, published to NuGet as [`STX.Sdk`](https://www.nuget.org/packages/STX.Sdk). Email and password authentication is unchanged and everything new here is opt-in. Two types were removed, `STXProfileService` and `STXUserProfile`; see Removed below if you use them. ``` dotnet add package STX.Sdk ``` ### C# SDK 1.6.0 **API keys.** Authenticate with an Ed25519 API key, signed per request. No login call, no token expiry, no refresh cycle. Email and password authentication continues to work exactly as it does today. ```csharp services.ConfigureSTXServices( STXEnvironment.OntarioDemo, STXApiKeyCredentials.FromPemFile(keyId, "~/.stx/ontario.pem")); ``` See [Request signing](/api/authentication/) for creating a key and choosing its scope. **Named environments.** Supply credentials, not URLs. - `STXEnvironment.OntarioDemo`, `USDemo`, `OntarioProduction` - `STXEnvironment.Custom(...)` for anything else; passing URLs directly still works - These resolve to `demo.stxapp.ca`, `demo.stxapp.io` and `stxapp.ca`, which may differ from the host your integration uses today. Check firewall and allowlist rules before switching **Identity.** `STXIdentityService.GetMeAsync()` returns your user id, account id and scope. Call it once at startup before joining any user-scoped channel: channel topics are keyed on the user id, and an API key has no login response to carry it. **.NET 10.** A `net10.0` build ships alongside `net8.0`. A .NET 8 or 9 app resolves the `net8.0` build as before; nothing needs to change. **Debug symbols** are embedded, so SDK frames in stack traces carry file names and line numbers. **Reliability** - A failed token refresh no longer stops the host process. It is reported through the session message callback instead - Login and token refresh are retried; previously they were the only calls with no retry - Retries exclude credential errors, 401, 403 and 429. HTTP 404 is retried, since that is what an ingress returns mid-deployment - Failures preserve the cause as `InnerException` instead of a bare "Request Failed" **Removed.** `STXProfileService` and `STXUserProfile`. Use `STXIdentityService` and `STXIdentity`. ### C# SDK 1.6.1 - `STXTrade.Action` no longer throws when serialised with `System.Text.Json`, which affected ASP.NET endpoints returning trade history - If you installed 1.6.0 in the days before this announcement, the identity types were named `STXViewerService` and `STXViewer` in that build. They are `STXIdentityService` and `STXIdentity` from 1.6.1 onwards; `GetMeAsync()` is unchanged ## 2026-09-10 v1.1.0 **Breaking changes to the REST API.** **Money and quantities are now strings.** Responses carry money as a dollar amount and quantities as decimals, both as strings. | | Before | Now | | --- | --- | --- | | Price | `49` | `"0.4900"` | | Quantity | `10` | `"10.00"` | To port, divide by 100 any money field you were reading or sending as a whole number of cents. These fields on `GET /api/v1/markets` were already in dollars and keep their value: `price`, `bids[].price`, `offers[].price` and `recent_trades[].price`. Parse money with a decimal type rather than a float. **Placing orders.** Send `price` in quotes to `POST /api/v1/orders`, for example `"price": "0.49"`. A price sent without quotes, such as `49` or `0.49`, is rejected with a 400. **Use `total_fee` for a trade's fee.** It is the full fee for the trade. **New WebSocket topics.** The new account topics carry the same events as before, with money and quantities as strings like REST: | Previous topic | New topic | | --- | --- | | `active_orders:{user_id}` | `orders:{user_id}` | | `active_trades:{user_id}` | `fills:{user_id}` | | `active_positions:{user_id}` | `positions:{user_id}` | | `active_settlements:{user_id}` | `settlements:{user_id}` | | `portfolio:{user_id}` | `balances:{user_id}` | Also new: - `account:{user_id}` carries all of the above on one topic. - `orderbook`, `ticker` and `trades` carry public market data for any set of markets on a single join. See [WebSocket channels](/websockets/). ### Effective 2026-09-16 **Prices have at most two decimal places.** `"0.49"` and `"0.4900"` are accepted; `"0.495"` is rejected with a 400. **`quantity` must be in quotes too.** Send `"quantity": "10"`; a quantity sent without quotes, such as `10`, is rejected with a 400. **`GET /api/v1/trades` is now `GET /api/v1/fills`.** Results are under `fills` instead of `trades`, and the row fields keep their names (`trade_id`, `trade_fee`). The old path returns 404. **New REST endpoints:** `GET /api/v1/portfolio/fees` and `GET /api/v1/portfolio/adjustments`. ## 2026-08-25 v1.0.0 First release of the STX documentation. --- # Concepts > How the STX exchange works: binary contracts, order matching, liability, settlement and the limits that apply to your account. Source: https://docs.stxapp.io/concepts/ STX is an exchange, not a sportsbook. There is no house price to take: every price on the book was put there by another participant, and you can put your own there too. That changes what you need to understand before writing code. These pages cover the model the API is built on. Read them in order if you are new to the exchange, or jump to whichever one your current question belongs to. ## Start here - [United States and Canada](/concepts/us-and-canada/): Two exchanges under different regulators. Which one you integrate with changes almost everything, and accounts do not carry across. - [How markets work](/concepts/how-markets-work/): Binary contracts that settle at $1 or $0, what a price means, and how to read a market's status. ## Trading - [Order types](/concepts/order-types/): Limit and market orders, how each fills, and what happens to the quantity that does not. - [Positions and liability](/concepts/positions/): Order liability, position liability, and why filling an order can increase your available balance. - [Settlements and payouts](/concepts/settlements/): When a settlement is created, the fields it carries, and how profit and loss is computed. - [Market symbols](/concepts/market-symbols/): How to read a symbol, and how to map STX markets onto a universe you already track without a lookup. - [Market and order status](/concepts/market-status/): The seven market statuses and the nine order statuses, which are terminal, and which transitions are possible. - [Combos (preview)](/concepts/combos/): An upcoming feature: one contract across several markets, priced by request for quote. Not available to try yet. ## Constraints on your account - [Account limits](/concepts/account-limits/): Ceilings on order liability and exposure. They surface as a rejected order rather than an error about limits. - [Rate limits](/concepts/rate-limits/): What is enforced today, and how to build so that future limits are a non-event. Once the model makes sense, [Quick Start](/quick-start/) takes you from creating an API key to a resting order and a live feed of your own fills. --- # Account limits Source: https://docs.stxapp.io/concepts/account-limits/ Every account carries limits set by STX, and users can set their own lower ones. They are managed in the STX web and iOS apps; there is no REST endpoint for them, and nothing here changes them. Two of them affect trading through the API, and both surface as a rejected order rather than as an error about limits: | Limit | Effect | | --- | --- | | `ORDER_LIABILITY` | Caps your total outstanding order liability at any moment | | `MAX_ORDER_LIABILITY_PER_ORDER` | Caps the liability on any single order | Defaults are calibrated for casual participants, not market makers. If you are sizing orders and seeing rejections you cannot otherwise explain, this is the first thing to check. Contact STX support to have the ceilings raised. --- # Fees Source: https://docs.stxapp.io/concepts/fees/ Your fee schedule is specific to your account. Read it in the web app under **My Profile → Fee Schedule**: the same menu the API key lives in. That page is the authority on your rate and on when you are charged; this page only explains how the resulting fees appear in the API. Fee amounts are dollar strings, like every other money field in the API: a minimum of four decimal places, with any further precision preserved. You will find them on `trade_fee` and `total_fee` on a fill, `fee` on a settlement, and the `potential_order_trade_fee` projection on `GET /api/v1/account/market_stats`. ## Fee history `GET /api/v1/portfolio/fees` lists the fees charged against your account, newest first. It is the one place every fee appears, whichever way your account is charged; the other fee fields above each show one fee in the context of a single fill or settlement. Each row's `amount` is the signed effect on your balance, so you total what you have paid by summing `amount` rather than by reading each row's `type`: a `fee` is never positive, and a `fee_refund` is never negative. Fees that are only *projected* (held against your available balance but not yet charged) never appear here. `GET /api/v1/account/market_stats` is where you see those. ## Telling one fee from another A row does not name what kind of fee it is. Use the references it carries: | Row carries | What it is | |---|---| | `settlement_id` | The fee for that settlement. The same fee appears as a positive `fee` on that row in `GET /portfolio/settlements`. | | `market_id` but no `settlement_id` | A fee tied to that one market: a trade fee, or a charge applied when the market settled. | | Neither, plus an `event_id` | A single charge covering a set of related markets on that event, rather than any one of them. | **Every row fills in the narrowest reference that applies and leaves the rest `null`**, so a null is the normal case, not missing data. That is why `event_id` is `null` on the first two kinds. They already name a single market, and you read that market's event from `event_id` on `GET /api/v1/markets`. `event_id` is populated only on the third kind, where no one market applies and the event is the narrowest thing there is to name. `fee_id` behaves the same way: present on trade fees and on the set-of-markets charge, `null` on a fee that came from a settlement. So a settlement fee arrives with `market_id` and `settlement_id` set and `event_id` and `fee_id` both `null`. That row is complete. One thing not to infer from `fee_id`: **it does not pair a refund with the charge it reverses.** A `fee_refund` is recorded as a fee in its own right and carries its own `fee_id`, so joining the two on it finds nothing. A refund always concerns a single market, so `market_id` and the ordering are what relate it to the charge it offsets. :::caution[A fee can legitimately be zero] A `fee` of `"0.0000"` is a real entry, not a placeholder for a missing value. It means the fee was assessed and came to nothing. You will only see it on the last row type in the table above: the one charge covering a set of markets. Because that fee is a percentage of your *total* profit across the whole set, it cannot be worked out until every market in the set has settled, and it comes to zero when that total is zero or negative, or when your rate is zero. A positive total is never charged less than `"0.0100"`. The other row types are simply absent when there is no fee to charge. ::: --- # How markets work Source: https://docs.stxapp.io/concepts/how-markets-work/ Every market on the STX exchange is a **binary outcome contract**: it either resolves yes or no. When a market resolves, every outstanding contract settles at either the market's `max_price` or at $0. The market's `description` field is the definitive statement of what constitutes a "yes" result: ```json { "description": "Contracts for this market settle into $1 if the Boston Red Sox beats the New York Yankees by at least 1.5 runs and $0 if they do not.", "market_id": "becf9bf9-e2ff-4b50-879b-46054ad5c69a", "short_title": "NYY @ BOS -1.5" } ``` **Buying** a contract means you believe the outcome described will happen. **Selling** means you believe it won't. You are always trading against other participants on the exchange, not against STX. --- ## Prices Prices are **dollar amounts sent as strings**. A price of `"0.45"` means $0.45, and an order price takes at most two decimal places. The spread between the best available bid and offer represents the current market consensus. The REST API writes every money field this way, and quantities as decimal strings: ```json { "max_price": "1.0000", "bids": [{"quantity": "566.00", "price": "0.5300"}], "offers": [{"quantity": "3636.00", "price": "0.6700"}] } ``` The order book channel, `market:`, sends those levels in dollars too but in its own abbreviated shape. `order_book_update` carries `ob.b` and `ob.o`, and each level is `{p, q, l, tc, tl}` (price, contracts, liquidity, and the cumulative contracts and liquidity through that level), with every value a **JSON number, not a string**: ```json {"ob": {"b": [{"p": 0.53, "q": 566, "l": 299.98, "tc": 566, "tl": 299.98}], "o": [{"p": 0.67, "q": 3636, "l": 2436.12, "tc": 3636, "tl": 2436.12}]}} ``` The `markets` and `market_updates` channels are different again: their `bids[].price` and `offers[].price` are in **cents** (`53`), not dollars. All three formats side by side are in [Mapping markets](/concepts/market-shape/#money-on-the-wire). Over REST a book price can go straight into an order: `"0.5300"` from `bids` is a valid `price`. Only the `markets` and `market_updates` channels need converting from cents. The `max_price` field on a market is the settlement value for a winning contract, in dollars. It is also the reference point for calculating sell-order liability (see [Understanding Positions](/concepts/positions/)). Read `max_price` from each market rather than assuming it. New markets settle at $1, so `max_price` is `"1.0000"` and a contract trades between $0.01 and $0.99, but older markets carry other values and one environment can hold both. An order priced at or above a market's `max_price` is rejected. --- ## Market Status | Status | Meaning | |--------|---------| | `scheduled` | Created; no orders accepted yet | | `pre_open` | Accepts limit orders ahead of open; no trades yet | | `open` | Orders can be placed and matched | | `closed` | The result is known; no new orders, and resting orders are cancelled | | `resulted` | Result confirmed; settlements generated | | `cancelled` | Cancelled; no new orders, and resting orders are cancelled | | `voided` | Cancellation confirmed; void settlements generated | A response can also read `suspended`, which is not a stored status: an `open` or `pre_open` market that is not trading right now. `archived` is a separate boolean field, not a status. See [Market and order status](/concepts/market-status/) for the full detail. --- ## Querying Markets Use the [`GET /api/v1/markets`](/api/rest/markets/list-markets/) query to fetch current markets. It is signed like every other `/api/v1` route, and heavily cached, so you can call it frequently; it is designed to handle high query rates. For a price display or market screener, see [List markets](/api/rest/markets/list-markets/) for the request, response and a runnable curl. The best bid and offer are enough to estimate the implied probability of the outcome. For example, on a market whose `max_price` is `"1.0000"`, a best bid of `"0.5700"` and a best offer of `"0.6000"` mean the market is trading at 57 to 60 cents, roughly a 57 to 60% probability of the "yes" outcome. --- ## Real-Time Updates Querying `GET /api/v1/markets` gives you a snapshot. To receive live price and status changes without polling, subscribe to the [`markets`](/websockets/channels/markets/) WebSocket channel, which pushes only the changed fields whenever a market is updated. --- # Leaderboard Source: https://docs.stxapp.io/concepts/leaderboard/ The leaderboard ranks members by what they did on the exchange: how much they traded and made, how many contracts and markets they settled, how often they won, their biggest win, their return and their longest winning streak. It is public inside the platform (every ranked row shows a **handle** and an **avatar**, never a name, an email or an account id), and you control whether you appear on it. :::note[Not every environment has it] The leaderboard is a feature STX turns on per environment. Where it is off, every `/api/v1/leaderboard/*` endpoint answers `404 {"error": "Leaderboard is not enabled"}`. ::: ## Metrics | `metric` | What is ranked | `value` type | | --- | --- | --- | | `volume` | Notional traded: `filled quantity × market max_price`, whatever the trade price | dollar string | | `profit` | Net profit and loss on markets settled in the period, after fees | dollar string, e.g. `"12.5000"` | | `predictions` | Contracts settled in the period | integer | | `markets` | Markets settled in the period, whether won, lost or pushed | integer | | `win_rate` | Markets won ÷ markets won or lost. Needs 10 decided markets in the period | number, `0`–`1` | | `biggest_win` | Net profit on your best single market settled in the period | dollar string | | `return` | Net profit ÷ volume. Needs $500 of volume and a profit in the period | number, `0`–`1` | | `streak` | Longest run of winning markets in a row in the period; a push does not break it | integer | ## Which boards are shown STX decides which boards are shown and how many members each lists (10, 25, 50 or 100). Asking for a hidden board is not an error: it comes back empty with `"shown": false`. The table above gives how `value` is written on each board. STX can keep some account types (for example market makers or STX's own accounts) off every board. An account that is self-excluded, in a cool-off period, suspended, banned, closed, rejected, archived or overdrawn never appears on a board or a profile page. Only a **positive** value earns a place on a board: a member with nothing settled or traded in the period is not listed, and neither is a net loss on `profit`. Ranks are **dense**: two members with the same value share a rank, and the next distinct value takes the next rank. A board holds at most the configured number of members per board, never more than **100 rows**. ## Periods Every period is a range of calendar days in **America/New_York**, ending today. A settlement at 23:30 New York time on a Sunday counts for Sunday even though it is already Monday in UTC. | `period` | Covers | Resets | | --- | --- | --- | | `daily` | Today | Midnight New York, every day | | `weekly` | Monday to today | Midnight New York on Monday | | `monthly` | The 1st to today | Midnight New York on the 1st | | `yearly` | 1 January to today | Midnight New York on 1 January | | `all` | Everything | Never | Every board response carries `next_reset_at` as a UTC timestamp: local midnight shifted to UTC, so it moves by an hour across a daylight-saving change while the board still resets at 00:00 New York. It is `null` for `all`. Boards are rebuilt on a fixed schedule (every 15 minutes by default, on the clock), not after each trade or settlement; `refreshed_at` is when the snapshot you are reading was built. A member who leaves the leaderboard is removed at once. ## Categories `category` is `all` (every sport combined) or one sport key: the lowercase sport name, as in a row's `top_sport` (for example `basketball`). A sport has a board once a market in it has settled or traded in the period. An unknown category is not an error. The board is simply empty, because a sport that is between seasons genuinely has nobody on it. ## Endpoints | Method | Path | Returns | | --- | --- | --- | | GET | `/api/v1/leaderboard?period=weekly&category=all&metric=profit&limit=50` | The top rows of one board | | GET | `/api/v1/leaderboard/me?period=weekly&category=all` | Your own standing, including a rank outside the top 100 | | GET | `/api/v1/me` | Your identity and public profile | | PATCH | `/api/v1/me/profile` | Change handle, avatar or opt-in | All of `period`, `category`, `metric` and `limit` are optional and default to `weekly`, `all`, the board STX opens on and the number of members per board. An unknown `period` or `metric` is a `400` naming the field. `limit` is clamped to the number of members per board: a board is a fixed top-N, so there is no cursor and never a next page. A board row: ```json { "rank": 1, "handle": "swift.fox12", "avatar_url": "/avatars/swift.fox12.svg", "top_sport": "basketball", "top_sport_icon_url": "/api/images/categories/standard/basketball-6b422c4a.svg", "value": "300.0000" } ``` `top_sport` is the sport the member traded most in the period; on a sport board it is that sport. Your own standing: ```json { "profit": {"rank": 2, "value": "12.5000"}, "volume": {"rank": 1, "value": "400.0000"}, "predictions": {"rank": 3, "value": 7}, "markets": {"rank": 4, "value": 12}, "win_rate_rank": {"rank": 2, "value": 0.7}, "biggest_win": null, "return": null, "streak": {"rank": 6, "value": 3}, "win_rate": 0.7, "settled_markets": 10, "opted_in": true, "period": "weekly", "category": "basketball" } ``` A metric you have nothing positive on is `null` even while you rank on another, and so is every board STX hides. The Win rate board's rank is `win_rate_rank`; `win_rate` is your plain share of decided markets. `win_rate` is the share of decided markets you won, and is `null` until at least 10 markets have been decided in the period. A market you settled at exactly zero is neither a win nor a loss. When you are unranked (opted out, or no activity in the period), every metric is `null`, `settled_markets` is `0` and `opted_in` tells you which of the two it is. ### Authentication These endpoints, and `GET /api/v1/me` / `PATCH /api/v1/me/profile`, accept **either** credential: - The signed Ed25519 headers every other `/api/v1` endpoint takes; see [request signing](/api/authentication/). A `read_only` key can read every leaderboard endpoint; `PATCH /api/v1/me/profile` needs `read_write`. - `Authorization: Bearer ` with the session token STX's own apps sign in with. A session may always update its own profile. An integration signs with its API key. With no credential, or a bad one, the answer is `401 {"error": "Unauthorized"}`. If any `X-STX-ACCESS-*` header is present the request is treated as a signed request and the signing rules apply. ## Handles and avatars Your **handle** is the name other members see. It is separate from your sign-in email and never derived from your name. Every account starts with a generated `adjective.animal1234` handle; you can pick your own: - 3 to 24 characters: lowercase letters, digits, and `.` or `_` between them. Case is folded, so `Swift.Fox12` is saved as `swift.fox12`. - Not already taken, not a reserved word (`admin`, `support`, `stx`, …) and not on the blocklist. - Changeable **once every 30 days**. `GET /api/v1/me` reports `handle_changeable_at`: `null` when you may change it now, otherwise when the window ends. Sending your current handle again is not a change. Your **avatar** is generated from three values you choose and rendered on demand. There is no upload: ```json {"style": "dots", "seed": "a1b2c3d4", "palette": "ocean"} ``` `style` is one of `dots`, `rings`, `stripes`, `grid`, `ball`, `court`, `stitch`, `target`, `candles`, `dice`; `palette` one of `ember`, `forest`, `ocean`, `grape`, `slate`, `mint`, `rose`, `gold`; `seed` any 1–32 characters, and the same three values always render the same image. Every row carries `avatar_url`, a path relative to the API host (`/avatars/{handle}.svg`) that serves the SVG publicly with a one-day cache and an `ETag`. ```bash curl -X PATCH https://demo.stxapp.io/api/v1/me/profile \ -H "X-STX-ACCESS-KEY: $KEY_ID" -H "X-STX-ACCESS-TIMESTAMP: $TS" \ -H "X-STX-ACCESS-SIGNATURE: $SIG" -H "Content-Type: application/json" \ -d '{"handle": "swift.fox12", "avatar": {"style": "rings", "seed": "deadbeef", "palette": "gold"}}' ``` The response is the same object `GET /api/v1/me` returns. A key of the wrong JSON type is a `400`; a handle that is taken, reserved, malformed or changed too soon is a `422` whose `error` names the rule. ## Joining and leaving ```json {"leaderboard_opt_in": false} ``` sent to `PATCH /api/v1/me/profile` removes you from every public board on the next snapshot. Your own `GET /api/v1/leaderboard/me` then reports every rank as `null` with `opted_in: false`; your trading is unaffected. Send `true` to join. Each change is recorded with its time. A new account starts with the jurisdiction's default. An account opened before the leaderboard existed is not listed until you join; joining also gives you a generated handle and avatar if you do not have one yet. --- # Mapping markets Source: https://docs.stxapp.io/concepts/market-shape/ How a market on the exchange corresponds to a real game and a real outcome, and which fields to key your own records on. It covers the core game markets: winners, draws, totals, handicaps, and team and event stat lines. Player props follow a different `specifier` grammar and are not covered here; see [What this page does not cover](#what-this-page-does-not-cover). For the symbol grammar see [Market symbols](/concepts/market-symbols/), and for the lifecycle values see [Market and order status](/concepts/market-status/). ## Start here: one game, twelve markets, nine books Markets do not have a "both sides" object. Each outcome is its own market with its own order book. A real NCAAF game carries twelve, and those twelve form nine sets of mutually exclusive outcomes: the moneyline pair is one set, the three handicap lines are one set, and each stat question is its own. `grouping_id` is the field that says which set a market belongs to. Every value below is prefixed by the event's id, elided here as `{event}` to keep the table readable. | `rules` | `specifier` | `grouping_id` | Settles yes if | | --- | --- | --- | --- | | `home_winner` | `null` | `{event}:moneyline:full` | Duke win | | `away_winner` | `null` | `{event}:moneyline:full` | Tulane win | | `spread` | `"-4.5"` | `{event}:spread:full` | Duke win by more than 4.5 | | `spread` | `"-7.5"` | `{event}:spread:full` | Duke win by more than 7.5 | | `spread` | `"-13.5"` | `{event}:spread:full` | Duke win by more than 13.5 | | `event_stat_line` | `"home\|FIRST_SCORE\|NA"` | `{event}:esl:full:FIRSTSCORE:HOME` | Duke score first | | `event_stat_line` | `"away\|FIRST_SCORE\|NA"` | `{event}:esl:full:FIRSTSCORE:AWAY` | Tulane score first | | `event_stat_line` | `"home\|LAST_SCORE\|NA"` | `{event}:esl:full:LASTSCORE:HOME` | Duke score last | | `event_stat_line` | `"away\|LAST_SCORE\|NA"` | `{event}:esl:full:LASTSCORE:AWAY` | Tulane score last | | `event_stat_line` | `"both\|FIRST_SCORE_TOUCHDOWN\|NA"` | `{event}:esl:full:FIRSTSCORETOUCHDOWN:BOTH` | The first score is a touchdown | | `event_stat_line` | `"both\|ANY_SCORE\|39.5"` | `{event}:esl:full:ANYSCORE:BOTH` | Either team scores 40+ | | `event_stat_line` | `"both\|OVERTIME\|NA"` | `{event}:esl:full:OVERTIME:BOTH` | The game goes to overtime | Four things to take from that list. **A moneyline is two markets**, not one market with two sides. Their books are independent, and their prices need not sum to `max_price`. They share a `grouping_id`, which is how you know they are the same question seen from both ends. **One `rules` value can appear several times on the same game**, at different specifiers: three handicap lines above, all one set. `(event_id, rules)` does not identify a market; `(event_id, rules, specifier)` does. **A shared `grouping_id` does not identify a market either**; that is the point of it. Five of the twelve markets above sit on a `grouping_id` they share with another market. **Not every family exists on every game.** That game has no totals market at all. Build your mapping from what the API returns, not from a template of what a game "should" have. If that table makes sense, the rest of this page is detail. The three fields below and the six steps after them are the whole job; everything later is reference for the moments when one of those steps is not obvious. --- ## The three fields that map a market Mapping has two levels, and conflating them is the most expensive mistake on this page. Your system almost certainly holds an *instrument* ("the Duke/Tulane handicap ladder") with several legs. Ours holds one market per leg. Three fields carry that structure, and each answers a different question. | Field | Question it answers | Unique per market? | | --- | --- | --- | | `grouping_id` | **Which set of mutually exclusive outcomes?** The book. | **No, deliberately**: siblings share it | | `rules` + `specifier` | **Which outcome within that set?** The leg. | Yes, with `event_id` | | `market_id` | **What do I send back to trade?** The handle. | Yes | A complete mapping stores all three: `grouping_id` to line our book up against your instrument, `rules` and `specifier` to line up the leg, `market_id` to act. ```json { "market_id": "0a5f9c31-6d24-4b17-9e83-c1f7a0d5b862", "event_id": "272b75e2-77d2-408b-b9f6-d7f1090646fc", "grouping_id": "272b75e2-77d2-408b-b9f6-d7f1090646fc:spread:full", "grouping_name": "Spread", "rules": "spread", "specifier": "-7.5" } ``` ### Why `grouping_id` is worth reading rather than deriving For game markets you could work the set out yourself: the family and the scope both follow from `rules`, and the strike from `specifier`. For season futures you cannot. A `division_winner` market's set is its division, which comes from the standings we hold and you do not, and the filter fields that used to carry it are empty on every settled market. One NFL season event holds 96 markets in 11 sets; nothing else in the payload separates them. `grouping_id` is the only part of the market relation that is not computable from the rest of the payload. That is the reason it exists. :::caution[`grouping_id` keys a set, not a market] Two markets sharing a `grouping_id` is normal and intended, and common: every moneyline pair, every three-way triple, and every line of a handicap or totals ladder sits on one. An integration that resolves its instrument to a `grouping_id` and then trades "the market" it found will buy the draw when it meant the home side. Resolve to the set, then pick the leg with `rules` and `specifier`. ``` grouping_id {event}:3way:regulation specifier null -> three markets: home_winner_regulation_3way, away_winner_regulation_3way, draw_regulation_3way ``` ::: :::caution[Never join on `grouping_name`] `grouping_name` is the set in words: `"Spread"`, `"AL East Division"`, `"Last Run (BOS)"`. It is for display and for a human reading a log. It is not a key. It is derived for display, so two different sets can render the same name: a player appearing under two ids, with the same statistic, is enough to produce it. For tournaments the name is the event title, which STX can rename. Map on `grouping_id`; show `grouping_name`. ::: --- ## Map a game in six steps 1. **Pull markets for the competitions you follow** with [`GET /api/v1/markets`](/api/rest/markets/list-markets/). ``` GET /api/v1/markets?competitions=NCAAF&status=open&trading=true ``` Results are ordered tradeable-first, and the response is paginated: follow `cursor` until it comes back `null`. 2. **Group by `event_id`.** Every market on one game shares it. `event_start`, `event_title` and `participants` on any of them describe the game. 3. **Group by `grouping_id`.** Each group is one set of mutually exclusive outcomes, one of your instruments. This works on an unfiltered bulk pull too: `grouping_id` carries the event id, so grouping a whole dump by it alone will not merge two fixtures' moneylines. 4. **Within a set, key each market on `(rules, specifier)`** and store its `market_id`. That pair is the leg: `rules` for winners and draws, the `home`/`away`/`both` prefix in `specifier` for stat lines, home-relative for handicaps, over for totals. `market_id` is what you send back when you trade. 5. **Read `max_price` per market** and convert prices into your own representation, minding the units table below. 6. **Subscribe for changes.** `GET /api/v1/markets` is a snapshot; the [`markets`](/websockets/channels/markets/) channel pushes only the fields that change afterwards. New markets on a game you already track arrive as `market_created`, so a live client can map them without another REST call. :::note[Where the grouping fields appear] `grouping_id` and `grouping_name` are returned by `GET /api/v1/markets` and on every `market_created` push, so a market opened after your snapshot can be filed into its book without another REST call. They are absent from `market_updated` for the ordinary reason any field is absent from a diff: neither ever changes for a market that already exists. Cache them per market on first sight. ::: --- ## Identifying a market | Field | What it is | Use it for | | --- | --- | --- | | `market_id` | UUID, fixed for the life of the market | **The key in your own store.** Every order, cancel and channel topic takes it | | `grouping_id` | Opaque string shared by every market in one set of mutually exclusive outcomes | **Lining our book up against your instrument.** Compare for equality; never parse | | `event_id` | UUID shared by every market on the game | **Grouping a game's markets.** Needs no string handling | | `rules` | What the market asks, machine-readable | Deciding how to interpret `specifier`, and which leg of a set this is | | `specifier` | The strike: the line, the handicap, the stat | The threshold the result grades against | | `grouping_name` | The set in words, e.g. `"Spread"`, `"AL East Division"` | Display and logs. **Not a key** | | `symbol` | Readable name, e.g. `STXNCAAF-26SEP051530TULNDUKE-SPREADDUKEMINUS7.5` | Logs, dashboards, anything a person reads | The mapping key is **`event_id` + `rules` + `specifier`**, which identifies exactly one market. `grouping_id` sits above it and says which markets belong together. Store `market_id` alongside whatever you match on, and you can act on the market without resolving it again. :::caution[`symbol` is a label, not an identifier] `symbol` is rebuilt from the event whenever the event changes, and **the event's start time is one of its inputs**. Every market symbol on a fixture moves when that fixture is rescheduled. ``` Osasuna vs Celta Vigo, postponed eleven days before STXLALIGA-26AUG161530OSACEL-DRAWOSACEL after STXLALIGA-26AUG271430OSACEL-DRAWOSACEL ``` Nothing about the market changed. A stored symbol will not find it again. `market_id` and `grouping_id` both survive a reschedule; `symbol` does not, so do not key on it. `title`, `description`, `max_price` and `featured` can also be edited after a market opens. In rare cases a `specifier` is rewritten too, when the upstream key for a participant changes. ::: --- ## Money on the wire The same field carries different units depending on which surface you read it from. This is the single most common source of mapping bugs. | Field | REST `/api/v1/markets` | `markets` / `market_updates` channels | | --- | --- | --- | | `max_price` | `"1.0000"`, dollar string | `100`, cents | | `last_traded_price` | `"0.5500"`, dollar string | `55`, cents | | `price` | `"0.6000"`, dollar string | `60`, cents | | `bids[].price`, `offers[].price` | `"0.5300"`, dollar string | `53`, cents | | `recent_trades[].price` | `"0.5500"`, dollar string | `55`, cents | Reading the same market both ways returns `"0.5300"` over REST and `53` on the `markets` channel for the identical price level. :::caution[The order book channel is a third format] `market:` is not the column above. Its `order_book_update` sends book levels in **dollars as JSON numbers** (`0.53`), under abbreviated keys: `ob.b` and `ob.o`, each level `{p, q, l, tc, tl}`. So one price level reads `"0.5300"` over REST, `53` on `markets`, and `0.53` on `market:`. Check which surface you are holding before converting anything. ::: :::note[Order prices use the REST format] `POST /api/v1/orders` takes `price` as a dollar string with at most two decimal places, so a REST book price such as `"0.5300"` can be sent as it is. A price taken from the `markets` channel is in cents and has to be divided by 100 first. ::: Two further details on the REST decimal strings: - **Parse them as decimals.** Money has at least four decimal places and some fields carry more, so never compare them as strings. - **`price_change24h` is a percentage**, rounded to a whole number, not a price delta. --- ## One market is one outcome Every market is a contract on a single named outcome. It settles at the market's `max_price` if that outcome happens and at `0` if it does not. **Buying** means you expect it to happen; **selling** means you expect it not to. You always trade against other participants, never against the exchange. The outcome is named by two fields together: `rules` says what kind of question the market asks, and `specifier` gives the strike. Everything else on the payload (titles, questions, descriptions) is prose derived from those two. :::caution[Settlement is not always all-or-nothing] Two results settle between the extremes. `push` applies to a `home_winner` or `away_winner` market whose game ends level: neither side won, so it settles at `max_price / 2`. `settled` means the market was resolved at a price strictly between `0` and `max_price`. Branch on `result` rather than assuming a winning contract is always worth `max_price` and a losing one always `0`. ::: ### Which side a market takes | Family | Sides on the exchange | One `grouping_id` covers | | --- | --- | --- | | Winner, 2-way | Two markets: `home_winner` and `away_winner` | Both | | Winner, 3-way | Three markets: `home_winner_regulation_3way`, `away_winner_regulation_3way`, `draw_regulation_3way` | All three | | Totals | **One** market per line, and it is the **over**. Sell it to be short the over | The whole ladder | | Handicap | One market per line, always stated **from the home team's side** | The whole ladder | | Team/event stat | One market per stat, and per team where the stat is team-scoped | One stat on one side | To pair `home_winner` with `away_winner`, match on `grouping_id`: both sides of a moneyline carry the same value, and so do all three legs of a three-way and every line of a ladder. :::note[Two sides of one question can be two sets] For `event_stat_line`, a set is one statistic on one side. `"home|FIRST_SCORE|NA"` and `"away|FIRST_SCORE|NA"` therefore have **different** `grouping_id` values, even though at a glance they look like the two halves of "who scores first". They are independent yes/no contracts and are priced as such. If you model that question as a single two-way instrument, pair the two sets yourself on the statistic. ::: --- ## `rules`: what the market asks These are the values this page covers. Scoped variants are listed separately under [Scope](#scope-markets-on-part-of-a-game). | `rules` | Question | Event types | | --- | --- | --- | | `home_winner` | Does the home team win? | baseball, basketball, football, hockey, soccer, cricket | | `away_winner` | Does the away team win? | baseball, basketball, football, hockey, soccer, cricket | | `home_winner_regulation_3way` | Home win in regulation, draw excluded | soccer, cricket | | `away_winner_regulation_3way` | Away win in regulation, draw excluded | soccer, cricket | | `draw_regulation_3way` | Does the game end level in regulation? | baseball, soccer, cricket | | `home_winner_regulation_2way` | Home win in regulation, 2-way | soccer, cricket | | `away_winner_regulation_2way` | Away win in regulation, 2-way | soccer, cricket | | `home_winner_regulation` | Home win in regulation | cricket | | `away_winner_regulation` | Away win in regulation | cricket | | `over_under` | Do both teams combine for more than the line? | baseball, basketball, football, hockey, soccer, cricket, tennis | | `spread` | Does the home team beat the handicap? | baseball, basketball, football, hockey, soccer, tennis | | `event_stat_line` | A game-level or team-level stat question | baseball, basketball, football, hockey, soccer | | `participant_stat_line` | A team total for one named stat | baseball, basketball, football, hockey, soccer | The event types listed are the ones each rule is defined for. Which of those markets actually open is a per-environment choice, so a sport can support a rule without any live markets in it at a given moment. :::caution[`rules` is not unique across sports] `over_under` on a baseball game and `over_under` on a soccer game are graded by different logic, and their period units differ. Always read `rules` together with `event_type`. New rules appear from time to time. Match the values you recognize and fall through gracefully on one you do not, rather than treating this as a closed set. The same holds for `grouping_id`: treat it as opaque, so a family you have not seen before still groups correctly. ::: --- ## `specifier`: the strike | `rules` | `specifier` format | Example | | --- | --- | --- | | All winner and draw rules | `null` | `null` | | `over_under` | A half number, the line | `"54.5"` | | `spread` | A signed half number, **from the home team's side** | `"-7.5"` | | `participant_stat_line` | `{home\|away}\|{STAT}\|{line}` | `"home\|POINTS\|57.5"` | | `event_stat_line` | `{home\|away\|both}\|{STAT}\|{line}` | `"both\|HITS\|16.5"`, `"home\|FIRST_SCORE\|NA"` | Lines are always half numbers, so a totals or handicap market cannot end level and there are no pushes on them. ### Handicaps are stated from the home side `spread` with `"-7.5"` is *home team wins by more than 7.5*; with `"7.5"` it is *home team wins, or loses by less than 7.5*. The away team never gets its own handicap market; to be on the away side of the line, sell the home market. Every line of the ladder shares one `grouping_id`. ### Stat lines: three fields, always Split a stat-line specifier on `|`. It always has three fields: 1. the side: `home`, `away`, or `both` for a statistic about the game itself 2. the statistic code 3. the line, or `NA` where the statistic states no threshold For countable stats the line is a real over/under threshold: ``` "both|HITS|16.5" Will both teams combine for more than 16.5 hits? "both|ANY_SCORE|39.5" Will either team score 40+ points? "home|POINTS|57.5" Will the home team have more than 57.5 total points? ``` For yes/no stats there is no threshold, and the line field reads `NA`: ``` "both|OVERTIME|NA" Will the game go to overtime? "home|FIRST_SCORE|NA" Will the home team score first? "both|FIRST_SCORE_TOUCHDOWN|NA" Will the first score be a touchdown? ``` `NA` is the signal, so you do not need to know which statistics are countable to render a market correctly: a numeric third field means an over/under, and `NA` means a proposition. Fall back to the market's `question` for display. The set of stat codes in use is a per-environment setting rather than a fixed part of the API, so treat any list you build as the set in use today and handle an unrecognized code by skipping the market rather than failing. --- ## Scope: markets on part of a game A market that settles over part of a game carries a scope suffix on `rules`. No suffix means the full game, which is most markets. | Suffix | Window | | --- | --- | | *(none)* | Full game | | `_f2`, `_f5` | The first 2 or 5 periods | | `_r1`, `_r2` | Period 1, period 2 | ``` over_under full game over_under_r1 first period only home_winner_f5 first five periods spread_f2 first two periods ``` The scope is also a segment of `grouping_id`, so two scopes of one rule never share a set: ``` {event}:total:full over_under {event}:total:r1 over_under_r1 ``` :::caution[A scope code is not a fixed duration] The period *unit* depends on the sport: `_r1` is the first inning in baseball and the first half in soccer. Read the suffix against `event_type`, never on its own, and note that `grouping_id` carries the raw code (`r1`), not the period, so it does not settle this for you either. `grouping_name` does spell the period out (`"Spread - 1st Inning"` against `"Spread - 1st Quarter"`), which is useful for display, but it is not a key. ::: Two scopes of one rule are two independent markets with separate books. `over_under` and `over_under_r1` on the same game are unrelated instruments. --- ## Naming the side: which text to trust Every market carries several human-readable strings, and they are not interchangeable. | Field | For a `home_winner` market | | --- | --- | | `title` | `"NCAAF - Week 1 TULN @ DUKE"` | | `short_title` | `"TULN @ DUKE"` | | `group_title` | `"Duke"` | | `position` | `"Duke Blue Devils"` | | `grouping_name` | `"Moneyline"` | | `question` | `"Will the Duke Blue Devils defeat the Tulane Green Wave?"` | | `description` | `"Contracts for this market settle into $1 if the Duke Blue Devils beats the Tulane Green Wave and settle into $0 if they do not."` | :::caution[`title` and `short_title` do not identify a winner market] On `home_winner` and `away_winner` markets for the same game, `title` and `short_title` are **identical**: both describe the fixture, not the side. The away market above also reads `"TULN @ DUKE"`. Keying a winner market on either field silently merges the two sides of a moneyline into one record. `group_title` and `position` name the side. `description` is the definitive statement of what settles the contract. `grouping_name` names the *set*, so it is identical on both sides by design; it is the one string on this list that is meant to be shared. ::: Handicap and totals markets do include the line in `short_title` (`"TULN @ DUKE -7.5"`, `"BC @ CIN OU 54.5"`), which is why the problem is easy to miss until a moneyline reaches your book. `position` is also unreliable on stat-line markets, where it can be the bare stat code (`"FIRST_SCORE"`); for those, `grouping_name` reads as the statistic and its side: `"Last Run (BOS)"`. ### `participants` describes the fixture, not the market `participants` is the game's two teams (`role` of `away` and `home`, away first), and it is **the same on every market of that game**. It does not tell you which side a market settles on. Read that from `rules` for winner markets, and from the side prefix in `specifier` for stat lines. ```json "participants": [ {"name": "Tulane Green Wave", "role": "away", "short_name": "Green Wave", "abbreviation": "TULN"}, {"name": "Duke Blue Devils", "role": "home", "short_name": "Blue Devils", "abbreviation": "DUKE"} ] ``` --- ## `max_price` is a per-market field `max_price` is the settlement value of one winning contract, in dollars, and the ceiling on order prices. An order must price **strictly below** it. Read it from each market. It is not a per-region constant: a single environment can carry markets at several different values at the same time, and one competition's markets can differ from another's. A client that hardcodes a value will have orders rejected, or will misprice settlement value and sell-side liability. ```json {"symbol": "STXNCAAF-26SEP051530TULNDUKE-GAMEDUKE", "max_price": "1.0000"} ``` `max_price` also sets the reference point for sell-order liability; see [Understanding positions](/concepts/positions/). --- ## The order book on a market `bids` and `offers` on a market carry the **top seven price levels** per side, each a `{price, quantity}` object, with quantity aggregated across all resting orders at that price. They are a summary for display and mapping, not a feed to trade from: for the full aggregated book on one market, join the [order book channel](/websockets/order-book/). **Both arrays are sorted by price descending.** For bids that puts the best price first; for offers it puts the best price **last**. ```json "bids": [ {"price": "0.4900", "quantity": "120.00"}, {"price": "0.4700", "quantity": "40.00"}, {"price": "0.4500", "quantity": "75.00"} ], "offers": [ {"price": "0.5700", "quantity": "60.00"}, {"price": "0.5400", "quantity": "25.00"}, {"price": "0.5100", "quantity": "90.00"} ] ``` Best bid is `bids[0]` at `0.49`; best offer is `offers[len - 1]` at `0.51`. Taking `offers[0]` gives you the *worst* offer in the summary, up to seven levels away from the touch: a mistake that reads as a wide spread rather than as an error. Best bid and best offer are enough to compute the implied probability of the outcome: a market with a best bid of `0.49` and a best offer of `0.51` on a `max_price` of `"1.0000"` is trading around a 50% chance. Each market in a set has its own book. A set is a set of related contracts, not a combined book. --- ## What this page does not cover You will see these markets in the API. Identify them by `rules` and handle them deliberately rather than letting them fall into your game-market mapping. | `rules` | What it is | | --- | --- | | `player_stat_line` | Player props. `specifier` is `{player}\|{player_id}\|{STAT}\|{line}`: four fields, and the player's team is not in the payload | | `division_winner`, `conference_winner`, `superbowl_champion`, and similar | Season futures. `specifier` is a team abbreviation, and the market is not tied to a single game | | `race_winner` | A field of participants rather than a fixture | | `ad_hoc_rule` | Hand-created markets with no machine-readable settlement definition; `description` is the only statement of what settles | | `combo_rule` | Combination markets built from other markets' legs; `participants` is empty | The identity rules on this page hold for all of them: `market_id` is the key, `event_id` groups a game, `grouping_id` groups a set, and `rules` plus `specifier` name the outcome. Only the `specifier` grammar and the settlement source differ. `grouping_id` matters most on the season futures, which is the one shape where `event_id` is not enough. A single NFL season event carries 96 markets in 11 sets (the Super Bowl, both conferences and all eight divisions), and grouping that event by `event_id` pools them into one. `grouping_name` reads as the set: `"AFC South Division"`, `"American League"`, `"World Series"`. ```json { "rules": "division_winner", "specifier": "JAX", "grouping_id": "201e3810-064d-4ff3-8246-5b9be6dfef0f:division_winner:full:AFCSOUTHDIVISION", "grouping_name": "AFC South Division" } ``` --- # Market and order status Source: https://docs.stxapp.io/concepts/market-status/ A market has one of **seven** stored statuses. These are the values `?status=` accepts, lowercase, comma-separated, e.g. `?status=open,pre_open`. | Status | Meaning | | --- | --- | | `scheduled` | Created and ready to pre-open. No orders accepted yet. | | `pre_open` | Accepts limit orders ahead of open. No trades, and no market orders. | | `open` | Open for all orders and trades. | | `closed` | The result is known. No new orders; pending orders are cancelled. Awaiting confirmation by STX, or a delayed feed. | | `resulted` | Result confirmed. Settlements generated for all trades. | | `cancelled` | Cancelled. No new orders; pending orders are cancelled. Awaiting confirmation. | | `voided` | Cancellation confirmed. Void settlements generated. | An uppercase value returns `400`. Omit the parameter entirely and you get every non-archived market, not just the open ones. ## `suspended` is not one of them A response's `status` field can read **`suspended`**, which is not in the list above and cannot be filtered on. It is derived at render time: ```text trading == false AND status in (open, pre_open) -> suspended ``` So a market returned by `?status=open` can report `"status": "suspended"`. The filter is not wrong, that market genuinely *is* open, it simply is not taking trades at this moment. The two fields answer different questions: | Field | Answers | | --- | --- | | `status` | Where the market is in its lifecycle | | `trading` | Whether it is accepting orders **right now** | A market you can actually trade against has `status: open` **and** `trading: true`. Ask for exactly those with both filters: ```python tradeable = get("/api/v1/markets?status=open&trading=true")["markets"] ``` `?status=pre_open&trading=true` gets you the markets that are not matching yet but do accept resting limit orders. :::tip[Tradeable markets come back first] `GET /api/v1/markets` orders its results tradeable-first (`open` and trading, then `pre_open` and trading, then suspended markets, `scheduled`, `closed`/`cancelled`, and finally settled markets), with the soonest event first within each tier. So the first page is the tradeable one even without a filter. ::: ## Order statuses An order has one of **nine** statuses. They are a different set from the market ones above. In practice you mostly see the last five, because the first four are normally transient. | Status | Meaning | | --- | --- | | `created` | Stored, but not yet checked against your limits or available balance. | | `requested` | Passed the account checks and handed to the market. | | `accepted` | The market accepted it; not yet run against the book. | | `delayed` | Accepted, but held before further processing, see the in-play delay on the market. | | `open` | Resting on the book and available to be filled. | | `filled` | The entire quantity has been filled. | | `rejected` | Refused, either on the account checks or by the market. Read `rejection_reason`. | | `cancelled` | Cancelled with no quantity filled. | | `partially_cancelled` | Cancelled after part of the order had already traded. The trades it already created stand. | `created`, `requested`, `accepted` and `delayed` are normally transient, an order moves through them in the time it takes the request to return, so a resting order you placed will usually read `accepted` in the `POST` response and `open` by the time you query it. `filled`, `rejected`, `cancelled` and `partially_cancelled` are terminal. ### Which statuses mean the order is still live Branch on this rather than on `open` alone: | | Statuses | | --- | --- | | **Live**: working, cancellable, holding liability | `created`, `requested`, `accepted`, `delayed`, `open` | | **Done**: no longer working | `filled`, `rejected`, `cancelled`, `partially_cancelled` | :::caution[`accepted` is not transient on a `pre_open` market] A `pre_open` market takes limit orders but has no live book for them to rest on, so an order stays `accepted` until the market opens rather than moving to `open` in the next moment. That breaks the obvious reconciliation. `GET /api/v1/orders?status=open` returns an empty list while the account holds live, cancellable orders consuming balance, which reads as flat. A strategy that places its orders again on that answer doubles its position. Treat every status in the **Live** row as working, and do not use `status=open` to mean "my resting orders". ::: `partially_cancelled` is the one to handle explicitly: it means you have trades from an order you cancelled, so reconcile the fills rather than assuming a cancel left you flat. ## Events have their own statuses Do not confuse these with event status, which is a separate, shorter set: `scheduled`, `in_progress`, `completed`, `cancelled`. A market carries its event's status alongside its own as `event_status`. --- # Market symbols Source: https://docs.stxapp.io/concepts/market-symbols/ Every market carries a `symbol`: a short, readable name that says which event it belongs to and which outcome it settles on. An **event** is one scheduled game, the same thing `event_id` and `event_start` refer to. ``` STXMLB-26AUG271305COLWSH-GAMEWSH ``` That is the Washington Nationals to beat the Colorado Rockies, in the MLB game starting 27 August 2026 at 13:05. The `market_id` is a UUID and tells you nothing on its own. `STX` identifies the exchange, and every segment after it is derived from the event (the competition, the start time, the two teams, and the outcome the contract settles on), all of which you already know if you track the game at all. So the symbol is predictable rather than assigned. Given a game you already follow, you can construct the symbol you expect and match it against ours, instead of calling the API to discover which `market_id` corresponds to which game. ## Anatomy A symbol has two halves, joined by a hyphen. ``` STXMLB-26AUG271305COLWSH - GAMEWSH └────── event symbol ────┘ └ market ┘ ``` The **event symbol** identifies the event, and every market on the same game shares it. The **market segment** identifies what this particular market is asking, which part of the game it settles over, and which side of it you are buying. ``` STXMLB-26SEP032210STLLAD-TOTAL8.5 full game STXMLB-26SEP032210STLLAD-R1TOTAL0.5 first inning, same game ``` To collect every market on a game, `event_id` is the field to group on: it is a UUID and needs no string handling. See [Which identifier to use](#which-identifier-to-use). Written out in full: ``` {PREFIX}{COMPETITION}-{YYMONDDHHMM}{AWAY}{HOME}-{SCOPE}{TYPE}{SELECTION} ``` | Segment | Example | What it is | | --- | --- | --- | | Prefix | `STX` | Identifies the exchange. The only segment not derived from the event. | | Competition | `MLB` | The league or competition code. | | Date and time | `26AUG271305` | `YYMONDDHHMM`, from the event's start time. | | Away | `COL` | The away team, first. | | Home | `WSH` | The home team, second. | | Scope | `R1` | The part of the game the market settles over. **Absent for a full-game market**, which is most of them. | | Type | `GAME` | What the market asks. | | Selection | `WSH` | The outcome the contract pays on. | Away always precedes home, the usual way a matchup is written. ### The date is in US Eastern `26AUG271305` is 27 August 2026, 13:05 **US Eastern**, the exchange's clock. Not UTC, not the venue's local time, and not whatever your pricing feed publishes: a LaLiga match in Spain and an MLB game in Los Angeles are both stamped Eastern. The `event_start` field on the market is the UTC timestamp for the same moment, so convert before comparing the two, or a game near midnight Eastern will look like the wrong day. ### Scope Markets covering part of a game carry a scope code at the **front of the market segment**, before the type. A symbol with no scope code covers the full game, which is most of them. ``` STXMLB-26AUG281840LADDET-R1TOTAL0.5 MLB LAD @ DET OU 0.5, 1st inning STXEPL-26AUG281500MNCCRY-R1TOTAL2.5 EPL MNC @ CRY OU 2.5, 1st half STXMLB-26AUG281840LADDET-TOTAL8.5 MLB LAD @ DET OU 8.5, full game ``` There are two codes, `F` for **first** and `R` for **range**. | Code | Window | Example | | --- | --- | --- | | `F{n}` | Periods 1 through *n*. | `F5`, the first five innings | | `R{a}T{b}` | Periods *a* through *b*, inclusive. `T` reads as *to*. | `R1T3`, innings one to three | | `R{a}` | Period *a* alone; the shorthand for `R{a}T{a}`. | `R1`, the first inning | Both count periods from 1, so `F{n}` and `R1T{n}` are the same window but the shorthand is preferred over `R{a}T{a}`. **A scope code is not a fixed duration.** The two symbols above carry the same `R1`, but it means the first inning in baseball and the first half in soccer: the first scoring period of whatever sport the competition belongs to. Read it against the competition, never on its own. The `rules` field carries the scope as a suffix (`over_under_r1` for both markets above), so if you need to know a market's scope in code, read `rules` rather than parsing it back out of the symbol. #### Telling a scope from a type The scope code is not padded to a fixed width. **A type code never begins with `F` or `R` followed by a digit**, so a market segment is scoped if and only if it opens with that pattern. The scope runs to the end of the pattern (`F{n}`, `R{a}` or `R{a}T{b}`), and the type begins immediately after. ``` TOTAL8.5 no scope, type TOTAL, line 8.5 R1TOTAL0.5 scope R1, type TOTAL, line 0.5 R1T3TOTAL0.5 scope R1T3, type TOTAL, line 0.5 F5SPREADKCPLUS9.5 scope F5, type SPREAD, selection KCPLUS9.5 ``` The `rules` field gives the scope directly. ## Market types The type segment tells you what is being asked. | Type | Market | Selection format | | --- | --- | --- | | `GAME` | Winner | The winning team, `WSH` | | `GAMEREG` | Winner in regulation | The winning team | | `GAME2W` | Winner, two-way | The winning team | | `GAME3W` | Winner, three-way | The winning team | | `DRAW` | Draw | `{AWAY}{HOME}` | | `SPREAD` | Handicap | `{HOME}PLUS{line}` or `{HOME}MINUS{line}` | | `TOTAL` | Over/under | The line, `227.5` | | `MATCH` | Tennis match winner | The player | | `CHAMP` | Championship | The team | | `WS` | World Series | The team | | `LEAGUE` | League winner | The team | | `CONF` | Conference winner | The team | | `DIV` | Division winner | The team | New market types arrive from time to time and take a code derived from the rule that created them, so treat this as the set in use today rather than a closed list. Match the types you recognize and fall through gracefully on one you do not. No type code begins with `F` or `R` followed by a digit; see [Telling a scope from a type](#telling-a-scope-from-a-type). A spread on the home team at −7.5 reads: ``` STXNBA-26APR051900CHAMIN-SPREADMINMINUS7.5 ``` ### Player props Player markets use the stat code as the type, then the player and the line: ``` {STAT}{TEAM}{INITIAL}{LASTNAME}{JERSEY}-{LINE} ``` A points line in a Memphis at Milwaukee game: ``` STXNBA-26APR051500MEMMIL-PTSMILMTURNER3-12.5 ``` Two things to know before you parse one: - **`{TEAM}` is the home team of the game, not the player's team.** Take the player's team from `participants` rather than from the symbol. - **The line is separated by a hyphen**, so a player-prop symbol has four hyphen-separated fields where other markets have three. Match the segments you need rather than splitting on every hyphen. Stat codes vary by sport: `PTS`, `REB`, `AST` and `3PT` in basketball, `PASSYDS`, `RUSHTD` and `SACK` in football, `HR`, `RBI` and `SB` in baseball. ### Futures Season markets are not tied to a single game, so the event symbol carries the competition and date without a participants segment: ``` {PREFIX}{COMPETITION}-{YYMONDDHHMM} ``` The market segment then names the team, as in a `CHAMP` or `DIV` market. ### Combination markets Combos are built from their legs rather than from a single game: ``` STXCOMBO-{EVENT_HASH}-{MARKET_HASH} ``` Each hash is derived from the symbols of the legs it combines. The same legs always produce the same symbol, in any order, so an identical combo is recognizable as one wherever you meet it. ### Custom markets Some markets do not belong to a scheduled game: a team making the playoffs, a tournament winner. They keep the same three-segment shape, but the middle segment is a label written when the market is created rather than a start time and two teams: ``` STXMLB-PLAYOFFSTOR-TOR Toronto to make the playoffs STXPGA-TOURCHAMPIONSHIP-LA Ludvig Aberg to win the Tour Championship ``` So the prefix and competition still mean what they mean, and the last segment is still the selection. Only the middle segment is free text, which means you cannot parse a date or teams out of it. Check whether it matches `{YYMONDDHHMM}` before assuming it is a scheduled game. Use `rules` and `specifier` to see what a custom market settles on, exactly as you would for a generated one. ## Which identifier to use Every market carries several identifiers, and they do different jobs. | Field | Use it for | | --- | --- | | `market_id` | **The key in your own store**, and the value you send back to us. A UUID, fixed for the life of the market. Every order, cancel and channel topic takes it. | | `event_id` | **Grouping a book by game.** A UUID every market on the game shares, and the safest thing to group on, since it needs no string handling. | | `rules` | What kind of market this is, in machine-readable form: `home_winner`, `spread`, `player_stat_line`. Scoped markets append the scope, as in `over_under_r1`. | | `specifier` | The strike: the line, the handicap, the player. | | `participants` | Team and player identity, with abbreviation, name and role given separately. | | `symbol` | Mapping to markets you already track, matching against another venue, logs, dashboards, anything a person reads. | For mapping, the pair worth leaning on is **`rules` and `specifier`**. Between them they say exactly what a market asks and at what strike, in fields meant for code, while `event_id` and `participants` supply the event and the teams. That combination is more precise than parsing a symbol, and it does not require a team-code dictionary. Symbols are rebuilt from the event they belong to, so read the current value from the API rather than treating a stored one as a key. Keep `market_id` alongside whatever you match on and you can act on a market immediately, without resolving it again. ## Finding a market `GET /api/v1/markets` returns `symbol` on every market, alongside `market_id`, `event_id` and the participants. Filter by competition and status to narrow the set: ``` GET /api/v1/markets?competitions=MLB&status=open ``` Then match on the symbol, or read `event_id` off any market and collect every market that shares it. `event_id` is the better of the two for grouping a game's book: it is a UUID, so it needs no string handling and cannot be thrown off by a change to the symbol format. Building the event symbol and matching on its prefix (matching `STXMLB-26SEP032210STLLAD-` to collect that game) does work, and picks up scoped markets along with full-game ones. The `markets` WebSocket channel carries the symbol too, so a live client can map new markets as they appear without a REST call. The order book channel accepts a symbol in place of the id, which is the one place a symbol addresses a market directly: ```json ["1", "1", "market:STXMLB-26AUG271305COLWSH-GAMEWSH", "phx_join", {}] ``` See [WebSocket channels](/websockets/) for the frame format. --- # Order types Source: https://docs.stxapp.io/concepts/order-types/ STX supports two order types: **limit** and **market**. The type is set via the `order_type` field when calling [`POST /api/v1/orders`](/api/rest/orders/place-order/). --- ## Limit Orders A limit order specifies the worst price you are willing to accept: - **Buy limit**: sets a price ceiling. You will pay no more than your limit price per contract. The exchange fills you at the best available price up to your limit. - **Sell limit**: sets a price floor. You will receive no less than your limit price per contract. If the full quantity cannot be filled at the limit price or better, the unfilled remainder is placed in the order book and waits for a matching counterparty. It stays there until fully filled or explicitly cancelled. **Use a limit order to name your price.** If you want an order to rest on the book and have others trade against it, it must be a limit order. --- ## Market Orders A market order does not constrain the price: - **Buy market**: buys at the best available offer price, sweeping through the book until filled or no more contracts are available. - **Sell market**: sells at the best available bid, sweeping downward. Any quantity that cannot be filled immediately is **cancelled**; market orders never rest in the book. They are used to take existing liquidity, not to provide it. :::caution On a thin book, a large market order can fill across a wide price range. Always check current depth via `bids` and `offers` on the [`GET /api/v1/markets`](/api/rest/markets/list-markets/) query before placing a large market order. ::: --- ## Placing an Order See [Place an order](/api/rest/orders/place-order/) for the request, response and a runnable curl. Prices are **dollar amounts sent as strings**, with at most two decimal places: `"0.45"` means $0.45. :::note `POST /api/v1/orders` is a **request**, not a guarantee. By the time your order reaches the matching engine, market conditions may have changed. Check the `order.status` in the response and subscribe to the [`orders`](/websockets/channels/orders/) channel to receive fill and cancellation events in real time. ::: --- ## Cancelling Orders Single cancel: See [Cancel an order](/api/rest/orders/cancel-order/) for the request, response and a runnable curl. Batch cancel: See [Cancel several orders](/api/rest/orders/cancel-multiple-orders/) for the request, response and a runnable curl. Cancel all open orders on your account: See [Cancel all open orders](/api/rest/orders/cancel-all-orders/) for the request, response and a runnable curl. A cancel is also a request: by the time it reaches the engine the order may already be fully filled. The response per order tells you the actual status after the attempt. --- ## Auto-Cancel on Disconnect If your integration relies on continuous connectivity to manage order risk, you can instruct the server to cancel your open orders automatically if your WebSocket connection drops. See [`cancel_on_disconnect`](/risk-controls/#cancel-orders-on-disconnect). --- # Positions and liability Source: https://docs.stxapp.io/concepts/positions/ STX does not simply debit the cost of an order when you place it and credit the payout when the market settles. It tracks several liability categories that change in real time as orders are placed, matched and settled. --- ## Wallet Fields | Field | Description | |-------|-------------| | `available_balance` | Cash you can use to place new orders | | `buy_order_liability` | Risk tied up in unfilled buy orders | | `sell_order_liability` | Risk tied up in unfilled sell orders | | `position_premium_liability` | Risk from contracts you currently hold | Your total exposure at any moment is the sum of all liability fields. The `available_balance` decreases when you place an order and can increase as orders are filled. Subscribe to the [`balances`](/websockets/channels/balances/) channel to receive live updates whenever any of these numbers change. --- ## Order Liability When you place an order the exchange immediately reserves the maximum possible loss; this is your **order liability**. - **Buy order**: `price × quantity`. A limit buy of 10 contracts at $0.45 incurs $4.50 of order liability, reducing `available_balance` by the same. - **Sell order**: `(max_price − price) × quantity`. A limit sell of 10 contracts at $0.36 on a market with `max_price` `"1.0000"` incurs `(1.00 − 0.36) × 10` = $6.40 of order liability. --- ## Position Liability When a trade executes on your order, order liability converts to **position liability**, the maximum loss on the contracts you now hold. Importantly, this conversion often **increases your available balance**, because you filled at a better price than your limit. For example: - You place a limit buy for 10 contracts at $0.45 → order liability $4.50, and `available_balance` drops by the same. - Five fill at $0.43 → position liability for those 5 is $2.15, and order liability for the 5 still resting is $2.25. Total $4.40. - `available_balance` therefore rises by $0.10: $0.02 saved on each of the 5 contracts that filled below your limit. --- ## Closing a Position Placing an order on the opposite side of an existing position does not add new liability; it hedges the existing one. **Example:** You hold a long position of 10 contracts bought at $0.45 ($4.50 of position liability). You place a sell order for 5 contracts at $0.60. - The sell order does not change your available balance, because your total risk is unchanged. - When the sell fills, a settlement is generated: `5 × (0.60 − 0.45)` = **$0.75 gross profit**, added to available balance. - Position liability drops to $2.25 (5 remaining contracts × $0.45). - Your maximum possible loss is now `2.25 − 0.75` = **$1.50** net, because you have locked in $0.75 of profit regardless of the final result. --- ## Settlement at Expiry When a market resolves, open contracts settle at the market's `max_price` or at $0. A `push` or `settled` result pays out between the two; see [One market is one outcome](/concepts/market-shape/#one-market-is-one-outcome). - **Won**: position liability is released and you receive `max_price × quantity`. Net gain is `(max_price − avg_price) × quantity`. - **Lost**: contracts settle at $0. The position liability is released, but you receive nothing back; your maximum loss on the position was already reserved. All transactions are fully capitalized. There is no margin or leverage. --- ## Monitoring Positions in Real Time | Channel | What it delivers | |---------|-----------------| | [`balances`](/websockets/channels/balances/) | Available balance and liability totals | | [`positions`](/websockets/channels/positions/) | Per-market position summary, including unrealized P&L | | [`fills`](/websockets/channels/fills/) | Individual trade confirmations | | [`settlements`](/websockets/channels/settlements/) | Settlement records as they are created | --- # Rate limits Source: https://docs.stxapp.io/concepts/rate-limits/ There are **no published rate limits today**, and none are enforced. You will not receive a `429`. That is a statement about where the platform is, not a guarantee. Limits will arrive, and they will be announced in the [changelog](/changelog/) before they are enforced. Build as though they exist: - Keep a single socket connection per process rather than polling REST for data the [order book channel](/websockets/order-book/) already pushes you - Batch cancels with `DELETE /api/v1/orders/batched` instead of one call per order - Give your HTTP client a retry with backoff, and treat `429` as retryable even though nothing returns it yet If your integration needs a specific rate ceiling agreed in advance, raise it with STX before you go live rather than discovering it in production. --- # Settlements and payouts Source: https://docs.stxapp.io/concepts/settlements/ A **settlement** is the final money-movement event for a set of contracts. Every settlement results in a realized profit or loss and the release of the corresponding position liability back into your available balance. --- ## When Settlements Are Created Settlements are generated in two situations: 1. **You close a position by trading.** If you bought 10 contracts and then sell 5, those 5 contracts are settled against each other at the prices you paid and received. The remaining 5 stay open. 2. **A market expires.** When STX resolves a market, outstanding contracts settle at the market's `max_price` for a win, or $0 for a loss. A `push` or `settled` result pays out between the two. --- ## Settlement Fields | Field | Description | |-------|-------------| | `id` | Unique settlement ID | | `type` | `closed_long`, `closed_short`, `expired_long` or `expired_short`: whether a trade or the market's result closed the position, and which side it was | | `opening_trade_id` | The trade that opened the position | | `closing_trade_id` | The trade that closed it; `null` when the market's result closed it | | `opening_price` | Price of the opening trade, as a dollar string | | `closing_price` | Price of the closing trade, or the market's settlement price (`max_price`, `0`, or between them) on expiry | | `quantity` | Number of contracts settled, as a quantity string | | `fee` | Exchange fee charged on this settlement | | `gross_pnl` | `(closing_price − opening_price) × quantity` for a long; the sign is inverted for a short | | `realized_pnl` | `gross_pnl − fee` | | `settled_premium` | Premium settled: negative for a long, positive for a short | | `settled_risk` | Risk released by the settlement | | `market_id` | Market the settlement belongs to | | `account_id` | Account the settlement belongs to | | `time` | When the settlement was created, ISO 8601 | | `inserted_at` | The same instant, as an integer of UNIX microseconds | Money fields are dollar strings, the same format as every other REST field; see [Wire format](/websockets/channels/wire-format/). --- ## Example Settlement A member sold 100 contracts at $0.10 (opening a short) on a market whose `max_price` is `"1.0000"`, and later bought 100 contracts back at $0.20 (closing the short): a loss, because the price moved against the short. The settlement looks like: ```json { "id": "801d773e-fecc-418a-b377-155605595178", "type": "closed_short", "opening_trade_id": "17aa760d-50c0-4c1a-a5c6-fe4af4adb4bb", "closing_trade_id": "7f959093-6a30-4b48-9a59-d1e09f004340", "opening_price": "0.1000", "closing_price": "0.2000", "quantity": "100.00", "fee": "0.5000", "gross_pnl": "-10.0000", "realized_pnl": "-10.5000", "settled_premium": "10.0000", "settled_risk": "90.0000", "market_id": "687e9cdb-a391-4118-aa80-1122bb14779f", "account_id": "c1c3614c-32ae-4bea-bae4-fc3f52047790", "time": "2026-11-01T19:02:28.125460Z", "inserted_at": 1793559748125460 } ``` Gross P&L: (0.10 − 0.20) × 100 = −$10.00. After a $0.50 fee: −$10.50 net realized P&L. :::note For a **short** position the sign convention is: selling contracts at a low price and buying them back at a higher price is a **loss**; buying back at a lower price is a **profit**. For a **long** position it is the reverse. ::: --- ## Querying Settlement History Use [`GET /api/v1/portfolio/settlements`](/api/rest/portfolio/list-settlements/) to retrieve past settlements. Each one names its `opening_trade_id` and `closing_trade_id`, which you can match against your fills from [`GET /api/v1/fills`](/api/rest/fills/list-fills/). --- ## Real-Time Settlement Notifications Subscribe to the [`settlements`](/websockets/channels/settlements/) channel to receive settlement records the instant they are created. This is the most reliable way to keep a running P&L in your integration; polling settlement history will always lag behind the live state of the exchange. --- # United States and Canada > STX runs two separate exchanges under different regulators. Which one you integrate with changes almost everything. Source: https://docs.stxapp.io/concepts/us-and-canada/ STX operates **two separate exchanges**. They share an API shape, but they are different products, in different jurisdictions, under different regulators. Accounts, balances, markets and API keys do not carry across. Decide which one you are building for before you write any code, it determines your base URL, what your users can legally do, and whether you can go live at all today. | | Canada (Ontario) | United States | | --- | --- | --- | | Status | **Live** | **Launching soon.** Start integrating on `demo.stxapp.io` | | Regulator | Alcohol and Gaming Commission of Ontario | U.S. Commodity Futures Trading Commission (application pending) | | Minimum age | 19 | 18, and the age of majority where you live | | Demo host | `demo.stxapp.ca` | `demo.stxapp.io` | | Production host | `api.on.stxapp.ca` | Not announced | ## Canada STX is licensed in the province of Ontario by the Alcohol and Gaming Commission of Ontario to provide Internet sports betting services. Persons under 19 are not permitted to engage in online sports wagering in Ontario. This exchange is live and taking real money in production. If you are building something users will trade on today, this is the one. :::note[Responsible gambling] If you or someone you know has a gambling problem and wants help, call Connex Ontario at **1-866-531-2600**. ::: ## United States **STX is not yet open for trading in the United States.** XV Exchange, LLC has applied to the U.S. Commodity Futures Trading Commission for designation as a contract market. That application is pending, and no assurance can be given that it will be approved. What this means for you as an integrator: - `demo.stxapp.io` is a fully working demo environment. The API is real, the order books are real, and everything in these docs works against it. - Start integrating now. The demo runs the same API as production, so an integration built on `demo.stxapp.io` moves over by changing the host and the key. - Nothing in these docs is an offer, solicitation, or recommendation to trade. ## Which host should I use? If you are exploring the API, reading the docs, or building a client library: **`demo.stxapp.io`**. Every example in these docs uses it. If you are integrating with the live Canadian exchange: start on `demo.stxapp.ca`, then move to `api.on.stxapp.ca` once your integration is exercised end to end. Production needs its own API key, created at `stxapp.ca`; demo keys do not work there. Full list with current status: [Environments](/environments/). ## What is the same The API surface itself. Request signing, the REST endpoints, the WebSocket channels, the order model and the response shapes are identical across both. A client written against one works against the other by changing the base URL and using a key issued by that exchange. --- # Environments > Demo and production base URLs for each region. Source: https://docs.stxapp.io/environments/ export const env = (region, id) => cfg.regions.find((r) => r.id === region).environments.find((e) => e.id === id) export const host = (url) => url.replace('https://', '') Each exchange has its own hosts. The base URL is where your API requests and WebSocket connections go. {cfg.regions.map((region) => ( <>

{region.label}

{region.environments.map((env) => ( ))}
EnvironmentBase URLStatus
{env.label}
{env.note}
{env.url ? {env.url.replace('https://', '')} : -} {env.status === 'live' ? 'Available' : env.status === 'internal' ? 'Internal only' : 'Planned'}
))} ## Which one should I use? Pick the exchange you are integrating with. Accounts and keys do not carry across, so this decision comes before everything else. See [United States and Canada](/concepts/us-and-canada/) if you are not sure which. **United States**: use {host(env('us', 'integration').url)}, and register there for an account and key. Shared by every developer integrating with STX, so the order books carry real activity. No real money. US production is launching soon: start integrating on demo now. **Ontario**: use {host(env('ca', 'integration').url)}, and register there for an account and key. When you are ready for production, register at {host(env('ca', 'production').appUrl)}, create a new key there, and send your requests to {host(env('ca', 'production').url)}. :::caution Production is live money. Point at it only once your integration is exercised end to end on demo. A key belongs to one environment. **Demo keys do not work in production**: create a separate production key, or every request is rejected with a 401. ::: --- # Explore > Real STX data from your browser. See the exchange before you build against it. Source: https://docs.stxapp.io/explore/ Everything here talks to the US demo exchange from your browser. Nothing is mocked: the books are the real demo books, with other developers trading in them. The money is not real. - [Live markets](/explore/live-markets/): A streaming order book, with the raw Phoenix frames beside it. No API key required. --- # Live markets > A real STX order book, streaming in your browser. See the spread and the depth before deciding whether to build. Source: https://docs.stxapp.io/explore/live-markets/ Every `/api/v1` route requires a signed request, but you should not need a working Ed25519 implementation before you see a single price. This page streams a book straight into your browser. Below is a real order book, streaming over the same WebSocket channel your client would join. It reads from the US demo exchange, so the orders are real but the money is not. Books there are seeded for testing, which is why the spreads look more uniform than a live market's. The book itself needs no key: paste a market id or symbol and it streams. To pick from a list of open markets instead, save a demo API key in [Try it](/quick-start/#try-it) on Quick start first. The page uses it, from your browser, to make one signed `GET /api/v1/markets` call. The panel below the ladder is the wire, unedited: the same message your client will parse, including the fields this page ignores. Phoenix sends each one as a five-element array: join reference, message reference, topic, event, payload. ## Reading it The ladder shows prices in cents. A bid of `44¢` is someone offering to pay that for a contract worth $1 if the event resolves yes, and nothing if it does not, so it reads directly as "a 44% chance". On the wire, in the `ob` book, the same level is the JSON number `0.44`, in dollars. To place that order over REST you send the `price` as the dollar string `"0.44"`. The number beside each price is quantity, in contracts. The shaded bar behind a row is that level's size relative to the largest on its side. ## What this tells you Spread is the number worth looking at. It is what a taker pays to cross, and what a maker earns for standing between the two sides. Wide spreads on markets with real volume are the opportunity: the book you see is what your quotes would be competing against, and there is currently a lot of room in it. If you want the same data programmatically, this is the join the page sends. Market-wide channels accept it on an unsigned connection, as long as the handshake sends a `User-Agent` header. Your account channels need a signed one (see [WebSocket channels](/websockets/)): ```json ["1", "1", "market:", "phx_join", {}] ``` The join reply carries the whole book, `recent_trades`, `volume_24h` and `last_traded_price`. See [WebSocket channels](/websockets/) for the frame format and [the order book channel](/websockets/order-book/) for the update events. Everything else, from placing orders to your positions and fills, is signed. That is [Quick Start](/quick-start/). --- # STX Prediction API Documentation > Build on the STX Prediction Platform, an order book for binary contracts, with REST, WebSocket channels and Ed25519 request signing. Source: https://docs.stxapp.io/ ## One order, end to end Place it over REST, get the response, then watch the fill arrive on your socket. ## Core operations - [Markets and events](/api/rest/markets/): Query markets and events, filter by competition and status. - [Orders](/api/rest/orders/): Place and cancel orders; list your resting book. Re-quoting is cancel-then-place, with no atomic replace. - [WebSocket channels](/websockets/): Phoenix channels for live market data, and your own orders, fills and positions. - [Portfolio](/api/rest/portfolio/): Settlements, deposits, withdrawals and loyalty. ## Reference and tools - [Live markets](/explore/live-markets/): A real order book, streaming, with no API key. See current spreads and depth before deciding whether to build. - [Postman collection](/downloads/stx-rest-api.postman_collection.json): Every REST endpoint, with the Ed25519 signing script already wired in. - [Authentication](/api/authentication/): How to sign a request with your Ed25519 key, in five languages, with a known key and signature to check your code against. - [OAuth](/oauth/): How an app gets a member's consent: the authorization code flow, scopes, tokens and discovery. - [SDKs](/sdks/): Official TypeScript, Python and C# clients: typed calls, signed requests and live channels, ready to install. - [Examples on GitHub](https://github.com/stxapp/stx-api-examples): Signed requests, order placement and live channel watchers in Python and JavaScript. --- # ISV program > Build an app that STX members connect to their account, with scoped, revocable access they approve on STX. Source: https://docs.stxapp.io/isv/ export const demo = (id) => cfg.regions.find((r) => r.id === id).environments.find((e) => e.id === 'integration').url **An ISV (independent software vendor) builds an app that acts on behalf of other STX members, using OAuth.** Your users keep their own STX accounts and connect them to your app. They sign in on STX, approve the access you ask for, and can disconnect at any time. You never see their password, their keys or their funds, and nothing your app holds can move money. How the protocol works, from the authorization code flow to scopes, tokens and errors, is covered in the [OAuth](/oauth/) section. These pages cover what the program gives you and how to get access. ## What you get - **Scoped access.** Named slices of a member's account, such as `profile.read`, `balance.read`, `orders.read` and `orders.write`. You ask for the fewest you need, and every token is confined to them. No scope moves money. See [Scopes](/oauth/scopes/). - **Consent hosted by STX.** Sign-in, sign-up, two-factor and the consent screen all run on STX. Members see your app's name and logo, and can revoke it from **Connected apps** in their account. See [Hosted pages](/isv/hosted-pages/). - **Member and app tokens.** A member token to act for one member, a refresh token to keep that access alive, and an app token to read the market and event catalogue as your app. See [Tokens and security](/oauth/tokens-and-security/). - **The same API.** An access token is a bearer on the same [REST API](/api/rest/) and [WebSocket channels](/websockets/) a signed request uses, limited to the scopes the member approved. - **Attribution.** Every order your app places for a member is recorded as placed by your app. - **A TypeScript SDK.** `@stxapp/stx-typescript` runs the whole flow for you, from linking a member to refreshing and revoking their tokens. See [With the TypeScript SDK](/isv/typescript-sdk/). ## Access Access is by invite. STX registers your app and gives you its client credentials, the host to test against, and a member account funded with play money to connect it to. ### Get demo access To ask for an invite, contact [developer support](/support/) with your app's name and what it does. To set up your app, STX needs: - an application **name** and **logo**, shown to members on the consent screen and in Connected apps; - its **application type**, from the table below; - one or more **callback URLs** (redirect URIs), where STX returns the member after consent. They are matched exactly, byte for byte, at both the authorize and token steps, except a local callback (`localhost`, `127.0.0.1`, `[::1]`), which matches on any port. The allowed forms depend on your application type. None for a Server-to-server app. No fragment; - the **scopes** your app may ever request: member scopes, app scopes, or both. Every request is narrowed against this list; - the **IP addresses** your app calls STX from, as CIDR blocks (`203.0.113.0/24`, `2001:db8::/32`; no wider than /8 for IPv4 or /32 for IPv6). Required if your app has any write scope (`orders.write`, `terms.write`), optional otherwise. When set, token requests, REST calls and WebSocket connections from any other address are refused with `403 ip_not_allowed`. Developing locally? Add your own public IP. You receive a `client_id` and, for a Web app or Server-to-server app, a one-time `client_secret`. The secret is shown once and cannot be recovered; store it on your server. Rotating it invalidates the old one immediately. ### Application types | Type | For | Client secret | Callbacks in production | | --- | --- | --- | --- | | Web app | an app with its own server | yes | `https` only | | Browser app | a single-page app with no server | no, PKCE only | `https` only | | Mobile or desktop app | an installed app | no, PKCE only | `https` link, reverse-domain custom scheme (`com.example.app:/callback`), or `http://localhost` / `127.0.0.1` / `[::1]` on any port | | Server-to-server | your backend, no member sign-in (`client_credentials`) | yes | none | The type is fixed at registration. Outside production, every type with callbacks may also register `localhost`, `127.0.0.1` and `[::1]` callbacks (http or https) that match on any port, so you can develop locally. ## What it takes to integrate 1. **Get a demo client** through your invite (see [Get demo access](#get-demo-access)). 2. **Run the authorization code flow** to turn a member's consent into tokens. See [Authorization code flow](/oauth/authorization-flow/). 3. **Call STX for the member** with the access token as a bearer on `/api/v1` and the WebSocket, limited to the [effective scope](/oauth/scopes/#effective-scope). 4. **Keep access alive** by refreshing, and drop it cleanly when the member disconnects. See [Tokens and security](/oauth/tokens-and-security/). ## Sample app [Sideline](https://github.com/stxapp/stx-isv-demo) is a fictional app built on STX OAuth, published as a public example. It links a member's account from its backend, reads their balance, positions and orders, places and cancels orders for them, streams their account data, reads public market data on its own app token, and unlinks on request. It is built on the [TypeScript SDK](/isv/typescript-sdk/) and runs against the demo exchange with the client you are given. Try it live at {cfg.sampleApps.sideline.url.replace('https://', '')}. It runs against the US demo exchange: to link an account, sign up on {demo('us').replace('https://', '')} first. No real money is involved. ## In this section - [Hosted pages](/isv/hosted-pages/): Consent, sign-in and sign-up, and Connected apps, all hosted by STX. - [With the TypeScript SDK](/isv/typescript-sdk/): Link a member, act for them, read market data as your app, and unlink, with @stxapp/stx-typescript. - [OAuth](/oauth/): The protocol: authorization code flow, scopes, tokens, discovery and errors. --- # Hosted pages Source: https://docs.stxapp.io/isv/hosted-pages/ A member's credentials and personal details are only ever entered on STX pages. Your app sends the member to STX with an [authorize request](/oauth/authorization-flow/); STX signs them in, asks for consent, and sends them back to your `redirect_uri`. None of these pages can be framed. ## Sign-in and sign-up A member who is not signed in is sent to the STX login, with a line naming your app, and returned to the authorize request when they sign in. Two-factor, when the member has it, is part of that login. A person without an STX account signs up on `/player/register`. Sign-up ends by asking them to verify their email and log in. When they log in **in the same browser session**, STX returns them to the pending authorize request and on to consent. In a different browser the pending request is lost; start the authorize request again. ## Consent `/connect/authorize` is the consent screen the member reaches from `/oauth/authorize`. It shows your app's name and logo, the line "If you allow this, *App* will be able to:", one plain-language line per scope, then **Allow** and **Deny**. The member approves or denies the list as a whole. - **Allow** issues the authorization code and redirects to your `redirect_uri` with `code` and `state`. - **Deny** redirects with `error=access_denied` and `state`. - A member whose identity verification is still pending may still allow; the screen notes that some actions, such as placing orders, stay unavailable until verification completes. A returning member whose grant already covers the request skips this screen; see [silent re-authorization](/oauth/authorization-flow/#returning-members-silent-re-authorization). ## Connected apps Every app a member has connected appears on **Connected apps** in their STX account, with your name and logo, the scopes they granted, when, and when your app last used it. From there they can revoke your app, which revokes the grant and every token under it at once: your next call for that member returns `401`. Design for this; a member may disconnect at any time. ## Money stays with the member No scope moves money, and no hosted step lets your app initiate a deposit or withdrawal. A member funds their account themselves on STX. --- # With the TypeScript SDK > Link a member, act for them, read market data as your app, and unlink, with @stxapp/stx-typescript/oauth. Source: https://docs.stxapp.io/isv/typescript-sdk/ The [TypeScript SDK](/sdks/typescript/), `@stxapp/stx-typescript`, runs the whole OAuth flow for you on Node.js 18+ and Bun: it creates the PKCE verifier and `state`, exchanges the code, refreshes and rotates tokens, and revokes them. How the flow works, what each scope allows and how to handle tokens safely are covered in the [OAuth](/oauth/) section; this page shows the SDK calls. :::note Your app's client credentials come with demo access; see [Get demo access](/isv/#get-demo-access). ::: ```bash npm install @stxapp/stx-typescript ``` ## Set up the client Everything on this page is imported from `@stxapp/stx-typescript/oauth`. Create one `OAuthClient` per registered app, on your server: ```ts import { MemoryPendingAuthorizationStore, MemoryTokenStore, OAuthClient } from "@stxapp/stx-typescript/oauth"; const oauth = new OAuthClient({ // baseUrl defaults to STX_HOST: the demo host from your invite. clientId: process.env.STX_CLIENT_ID!, clientSecret: process.env.STX_CLIENT_SECRET, // keep it on the server redirectUri: "https://yourapp.example/stx/callback", // exactly as registered scope: "profile.read balance.read portfolio.read orders.read orders.write", }); // Sign-ins in progress, and each member's tokens. const pending = new MemoryPendingAuthorizationStore(); const tokens = new MemoryTokenStore(); ``` The in-memory stores suit one process. In production, implement `TokenStore` (`get`, `set`, `delete`) and `PendingAuthorizationStore` (`put`, `take`) over your database, and encrypt tokens at rest. If several processes share one token store, also implement `withLock`, so only one of them refreshes a member's token at a time. Scope names are also exported as constants: `MemberScopes.ORDERS_WRITE`, `AppScopes.MARKET_DATA` and so on. What each one allows is listed in [Scopes](/oauth/scopes/). ## Link a member Two routes on your server: one sends the member to STX, the other receives them back. Here with Express: ```ts // 1. Send the member to STX to sign in and approve your app. app.get("/stx/link", async (req, res) => { const { url } = await oauth.beginAuthorization(pending, { data: { userId: req.user.id } }); res.redirect(url); }); // 2. STX sends them back with a code. Exchange it and store the tokens under your own user id. app.get("/stx/callback", async (req, res) => { const done = await oauth.completeAuthorization(pending, req.url, { store: tokens, memberKey: (data) => String(data?.userId), }); res.send(`Your STX account is linked (${done.scopes.join(", ")}).`); }); ``` `beginAuthorization()` creates the PKCE verifier and `state` and keeps them in `pending`; `completeAuthorization()` checks the `state`, exchanges the code and saves the tokens. `done.scopes` is what the member granted, which can be less than you asked for. The steps it runs are described in [Authorization code flow](/oauth/authorization-flow/). If one callback URL serves several apps, split the second step: `readCallback(pending, req.url)` returns the code and the pending entry (with your `data`), and `redeemAuthorization()` on the right app's `OAuthClient` finishes it. ## Act for the member `memberClient()` returns an ordinary [`STX`](/sdks/typescript/reference/stx/) client that acts for one member. Every method works as it does with an API key, within the scopes the member granted: ```ts const member = oauth.memberClient(tokens, userId); const balance = await member.balance(); console.log(balance.available_balance); const open = await member.orders({ status: ["open"] }); console.log(`${open.length} open orders`); await member.placeOrder(marketId, "buy", "limit", { price: "0.40", quantity: "1" }); ``` The client refreshes the access token before it expires and after a `401`, and writes the new tokens to your store (STX rotates refresh tokens, so the old one stops working; see [Refreshing](/oauth/tokens-and-security/#refreshing)). Pass `{ session: { onRefresh, onGrantRevoked } }` to hear about either. The member's channels work the same way: ```ts const ws = await member .websocket({ // Called if access ends while the socket is open (revoked, or the token expired). onAuthError: (err) => console.log("link again:", err.message), }) .connect(); await ws.orders({ onMessage: (msg) => console.log(msg.event, msg.payload) }); ``` ## Market data as your app With a client secret, your app can read the market catalogue and market data channels as itself, with no member involved (an [app token](/oauth/authorization-flow/#app-tokens-client-credentials)): ```ts const api = oauth.appClient("market_data"); const page = await api.markets({ status: "open", limit: 5 }); for (const market of page.items) console.log(market.symbol, market.bids?.[0]?.price ?? "-"); ``` The app token is cached and renewed for you. ## Unlink a member ```ts const revoked = await oauth.unlink(tokens, userId); ``` `unlink()` revokes the member's grant at STX and deletes their tokens from your store. It resolves `false` if STX could not be reached; the local tokens are deleted either way. ## Errors Every OAuth error extends [`STXAuthenticationException` or `STXForbiddenException`](/sdks/typescript/reference/errors/), and they are exported from `@stxapp/stx-typescript/oauth`: | Exception | When | What to do | | --- | --- | --- | | `STXGrantRevokedException` | The member revoked your app, or the grant ended. Their tokens are already deleted from your store. | Show "Link your STX account" again. | | `STXInsufficientScopeException` | The grant lacks a scope the call needs; `err.requiredScopes` names it. | Link again, asking for that scope. | | `STXOAuthException` | The callback or token endpoint returned an error; `err.error` holds the code, such as `access_denied` when the member declines. | Depends on the code; see [Discovery and errors](/oauth/discovery-and-errors/). | | `STXInvalidTokenException` | The access token was refused even after a refresh. | Link again. | ```ts import { STXGrantRevokedException, STXInsufficientScopeException } from "@stxapp/stx-typescript/oauth"; try { await member.placeOrder(marketId, "buy", "limit", { price: "0.40", quantity: "1" }); } catch (err) { if (err instanceof STXGrantRevokedException) { res.redirect("/stx/link"); } else if (err instanceof STXInsufficientScopeException) { console.log("needs", err.requiredScopes); } else { throw err; } } ``` ## What the module exports The package ships full TypeScript declarations for `@stxapp/stx-typescript/oauth`, so your editor shows every option and its documentation. The main pieces: | Export | What it is | | --- | --- | | `OAuthClient` | One registered app. `beginAuthorization()`, `completeAuthorization()` and `redeemAuthorization()` link a member; `memberClient()` and `appClient()` return an `STX` client; `unlink()` and `revoke()` disconnect; `refreshToken()`, `clientCredentials()` and `introspect()` call the token endpoints directly. `OAuthClient.fromMetadata()` builds one from the [server metadata](/oauth/discovery-and-errors/#authorization-server-metadata). | | `TokenStore`, `MemoryTokenStore` | Where each member's token pair is kept: `get`, `set`, `delete`, and optionally `withLock`. | | `PendingAuthorizationStore`, `MemoryPendingAuthorizationStore` | Where a sign-in in progress keeps its PKCE verifier and `state`: `put` and `take`. | | `readCallback()` | Reads the code and the pending entry from a callback URL, for a callback shared by several apps. | | `MemberScopes`, `AppScopes` | The scope names as constants. | | `discoverMetadata()`, `registerClient()` | Fetch the server metadata, and register a client where [dynamic registration](/oauth/tokens-and-security/#dynamic-registration) is enabled. | | `STXOAuthException`, `STXGrantRevokedException`, `STXInvalidTokenException`, `STXInsufficientScopeException` | The errors above. | ## A complete app [Sideline](https://github.com/stxapp/stx-isv-demo) is a working app built on this module: linking and unlinking, calls and live channels for the member, and public market data as the app. --- # OAuth > How an app acts for other STX members with OAuth 2.0, how that differs from an API key, and where OAuth is enabled. Source: https://docs.stxapp.io/oauth/ export const demo = (id) => cfg.regions.find((r) => r.id === id).environments.find((e) => e.id === 'integration').url STX supports two ways to authenticate. An **API key** signs requests for your own account. **OAuth** lets an app act for other STX members: a member signs in on STX, approves the access your app asks for, and your app calls the API for them with a scoped token they can revoke at any time. STX uses standard OAuth 2.0: the authorization code flow with PKCE (Proof Key for Code Exchange). A standard OAuth client library works against it. If you are building a product for STX members, the [ISV program](/isv/) covers access, the hosted pages and the TypeScript SDK; these pages cover the protocol. ## API key or OAuth | | API key | OAuth | | --- | --- | --- | | Who acts | You, on your own account | Your app, for a member, or as itself | | Credential | Your Ed25519 key | A member's scoped access token, or an app token | | The member's secrets | You hold your own key | You never see them | | Consent | Not applicable | The member grants it once and can revoke it | | Good for | Your own trading, bots, market making | A product used by STX members; a site showing prices | Trading your own account needs only an API key; see [Authentication](/api/authentication/). ## The model - **Client**: your application, registered once with STX as one of four [application types](/isv/#application-types). It has a public `client_id` and, for a Web app or Server-to-server app, a `client_secret`. - **Member**: an STX account holder using your app. They sign in to STX directly; you never handle their credentials. - **Grant**: a member's standing consent for your client, the set of scopes they approved. One active grant per member per client. Revoking it cuts your access. - **Scope**: a named slice of access, such as `balance.read` or `orders.write`. See [Scopes](/oauth/scopes/). Deposits and withdrawals are never delegable. A **member token** (authorization code with PKCE) proves your app may act for one member within a scope. A **refresh token** gets a new one without the member. An **app token** (`client_credentials`) authenticates the app itself, with no member, for the public market and event catalogue. All are opaque strings. Hold tokens on your server where you have one. A Browser app or Mobile or desktop app keeps them in memory or the platform's secure storage, never in a URL, a log or a shipped bundle. ## The endpoints at a glance | Endpoint | Method | Auth | Purpose | | --- | --- | --- | --- | | `/oauth/authorize` | GET | member's browser session | Send the member to consent ([Authorization code flow](/oauth/authorization-flow/)) | | `/oauth/token` | POST | `client_secret_basic`, or `client_id` for a Browser app or Mobile or desktop app | Code exchange, refresh, client credentials | | `/oauth/revoke` | POST | same | Revoke a token pair ([Revoking](/oauth/tokens-and-security/#revoking)) | | `/oauth/introspect` | POST | `client_secret_basic` | Check one of your own tokens ([Checking a token](/oauth/tokens-and-security/#checking-a-token)) | | `/oauth/register` | POST | none | Dynamic client registration, off by default ([Dynamic registration](/oauth/tokens-and-security/#dynamic-registration)) | | `/.well-known/oauth-authorization-server` | GET | none | Server metadata, RFC 8414 ([Discovery](/oauth/discovery-and-errors/#discovery)) | | `/.well-known/oauth-protected-resource` | GET | none | Resource metadata, RFC 9728 ([Discovery](/oauth/discovery-and-errors/#discovery)) | ## Calling the API with a token An access token is a bearer on the same [REST API](/api/rest/) a signed request uses, sent as `Authorization: Bearer ` on `/api/v1`. On the [WebSocket](/websockets/), send it in the handshake header `x-stx-oauth-token`. Every call is limited to the scopes the member approved; see [effective scope](/oauth/scopes/#effective-scope). The [rate limit guidance](/concepts/rate-limits/) applies to calls made with an OAuth token as it does to signed requests. ## In this section - [Authorization code flow](/oauth/authorization-flow/): The steps from redirect to a call made as the member, silent re-authorization, and app tokens. - [Scopes](/oauth/scopes/): Every scope, the routes and channels it reaches, and how a request is narrowed. - [Tokens and security](/oauth/tokens-and-security/): Lifetimes, refresh and rotation, revoking, introspection, dynamic registration, and a checklist for production. - [Discovery and errors](/oauth/discovery-and-errors/): The metadata documents, and every error the OAuth endpoints and the API return. --- # OAuth 2.0 authorization code flow Source: https://docs.stxapp.io/oauth/authorization-flow/ STX uses the OAuth 2.0 authorization code flow with PKCE (Proof Key for Code Exchange). The member signs in to STX, approves your scopes once, and your backend exchanges a short-lived code for tokens. A Web app exchanges the code with its `client_secret`, which must stay on your server. A Browser app or Mobile or desktop app has no secret: it sends its `client_id` in the form and proves itself with the PKCE verifier alone. See [application types](/isv/#application-types). The endpoints are listed in the [overview](/oauth/#the-endpoints-at-a-glance). Discover them from the server metadata rather than hard-coding them; see [Discovery](/oauth/discovery-and-errors/#discovery). `$STX` in the examples below is the base URL STX gives you with your invite. ## The flow, step by step ### 1. Create a PKCE verifier and challenge Generate a high-entropy `code_verifier`, 43 to 128 characters. The challenge is its base64url SHA-256. Keep the verifier on your server, keyed to this pending sign-in. Only `S256` is accepted. ```bash code_verifier=$(openssl rand -base64 64 | tr -d '=+/\n' | cut -c1-64) code_challenge=$(printf '%s' "$code_verifier" \ | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=') state=$(openssl rand -hex 16) ``` ### 2. Redirect the member to `/oauth/authorize` ``` GET $STX/oauth/authorize ?response_type=code &client_id= &redirect_uri= &scope=profile.read%20balance.read%20orders.read &state= &code_challenge= &code_challenge_method=S256 ``` `state` is yours: store it against the member's session and verify it on return. `scope` is narrowed to your client's allow-list; see [how a request is narrowed](/oauth/scopes/#how-a-request-is-narrowed). A member who is not signed in is sent to the STX login, including any two-factor step, and returned to this request when they sign in. A person without an account signs up on `/player/register`, then logs in in the same browser session and is returned to consent. The member then approves the scopes on the consent screen; see [Hosted pages](/isv/hosted-pages/). ### 3. Receive the code at your redirect URI On **Allow**, STX redirects the browser to your `redirect_uri`: ``` ?code=stxapp_code_…&state= ``` Verify `state`. On **Deny** you get `?error=access_denied&state=…`. A malformed request (bad `response_type`, missing PKCE, missing or unusable `scope`) also comes back to your `redirect_uri` with an RFC 6749 `error`; see [authorize errors](/oauth/discovery-and-errors/#authorize-errors). An unknown `client_id`, or a `redirect_uri` that does not match one on file, is shown an error page on STX and never redirected, so the flow cannot be turned into an open redirector. The code is single use and expires in 60 seconds; exchange it immediately. ### 4. Exchange the code for tokens From your server, POST to `/oauth/token` with HTTP Basic authentication (`client_secret_basic`): username `client_id`, password `client_secret`. ```bash curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$STX/oauth/token" \ -d grant_type=authorization_code \ -d code="$code" \ -d redirect_uri=http://localhost:8787/callback \ -d code_verifier="$code_verifier" ``` STX checks the code, the redirect URI and the PKCE verifier, then returns: ```json { "access_token": "stxapp_at_…", "refresh_token": "stxapp_rt_…", "token_type": "bearer", "expires_in": 3600, "scope": "profile.read balance.read orders.read" } ``` Store both tokens against the member, on your server if you have one. Presenting the same code twice revokes the member's grant to your app. ### 5. Call STX for the member Send the access token as a bearer on `/api/v1`: ```bash curl -s "$STX/api/v1/account/balance" -H "Authorization: Bearer $ACCESS_TOKEN" ``` On the WebSocket, send it in the handshake header `x-stx-oauth-token`. A call succeeds only within the [effective scope](/oauth/scopes/#effective-scope). Outside it you get `403 insufficient_scope`; an expired or revoked token gets `401`; a call from an address outside your app's [IP allowlist](/isv/#get-demo-access) gets `403 ip_not_allowed`. All three are described under [API errors](/oauth/discovery-and-errors/#api-errors). ## Returning members: silent re-authorization A member consents once. When a member whose active grant already covers the requested scopes returns, `/oauth/authorize` issues a code **without** showing the consent screen. For a background reconnection with no interaction at all, add `prompt=none`: - an active grant covering the request returns a `code`; - a member not signed in to STX returns `?error=login_required`; - a member without a covering grant returns `?error=consent_required`. All three arrive at your `redirect_uri` with `state` echoed, and none shows a screen. ## App tokens: client credentials An app token authenticates **the application itself**, with no member and no consent, for the market and event catalogue. Only an app with a client secret (Web app or Server-to-server) can mint one. ```bash curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$STX/oauth/token" \ -d grant_type=client_credentials \ -d scope=market_data ``` ```json { "access_token": "stxapp_at_…", "token_type": "bearer", "expires_in": 3600, "scope": "market_data" } ``` Omit `scope` to get every app scope your client is allow-listed for. A member scope, or an app scope your client is not allow-listed for, is `invalid_scope`. There is no refresh token: mint a new one when it expires. A member token reads the same catalogue, so you only need this with no member involved; see [app scopes](/oauth/scopes/#app-scopes). Building on Node.js or Bun? The TypeScript SDK wraps these requests; see [With the TypeScript SDK](/isv/typescript-sdk/). --- # Discovery and errors > The OAuth metadata documents STX publishes, and the errors the OAuth endpoints and the API return. Source: https://docs.stxapp.io/oauth/discovery-and-errors/ `$STX` in the examples is the base URL STX gives you with your invite. ## Discovery STX publishes two metadata documents. Neither needs authentication. Both are built on the host you fetch them from, so read the endpoints from them rather than hard-coding them. ### Authorization server metadata `GET /.well-known/oauth-authorization-server` (RFC 8414) lists the endpoints and what the server supports: ```bash curl -s "$STX/.well-known/oauth-authorization-server" ``` ```json { "issuer": "https://", "authorization_endpoint": "https:///oauth/authorize", "token_endpoint": "https:///oauth/token", "revocation_endpoint": "https:///oauth/revoke", "introspection_endpoint": "https:///oauth/introspect", "scopes_supported": ["profile.read", "balance.read", "portfolio.read", "orders.read", "transfers.read", "orders.write", "terms.write", "market_data", "events"], "response_types_supported": ["code"], "grant_types_supported": ["authorization_code", "refresh_token", "client_credentials"], "code_challenge_methods_supported": ["S256"], "token_endpoint_auth_methods_supported": ["client_secret_basic", "none"] } ``` `scopes_supported` holds both the member scopes and the app scopes; see [Scopes](/oauth/scopes/). `registration_endpoint` is added only while [dynamic registration](/oauth/tokens-and-security/#dynamic-registration) is enabled. ### Protected resource metadata `GET /.well-known/oauth-protected-resource` (RFC 9728) describes the API as an OAuth resource and names the server that issues its tokens. A client that receives a `401` from the API finds it through the `resource_metadata` in the challenge (see [API errors](#api-errors)). ```json { "resource": "https://", "authorization_servers": ["https://"], "scopes_supported": ["profile.read", "balance.read", "portfolio.read", "orders.read", "transfers.read", "orders.write", "terms.write", "market_data", "events"], "bearer_methods_supported": ["header"], "resource_name": "STX exchange API" } ``` The exchange is its own authorization server, so `resource` and the single `authorization_servers` entry are the same host. `bearer_methods_supported` is `header`: send the token in the `Authorization` header, never in a query string or form body. ## Authorize errors Errors in an authorize request come back to your `redirect_uri` as `?error=&state=`, with no screen shown to the member unless noted. | `error` | When | | --- | --- | | `access_denied` | The member chose **Deny** on the consent screen. | | `unsupported_response_type` | `response_type` is not `code`. | | `invalid_request` | PKCE is missing (`code_challenge` with `code_challenge_method=S256` is required), or `scope` is missing. | | `invalid_scope` | `scope` names an unknown scope, or nothing is left after narrowing to your client's allow-list; see [how a request is narrowed](/oauth/scopes/#how-a-request-is-narrowed). | | `login_required` | `prompt=none` and the member is not signed in to STX. | | `consent_required` | `prompt=none` and the member has no active grant covering the request. | | `server_error` | STX could not issue the code after the member allowed; start the authorize request again. | An unknown or suspended `client_id`, or a `redirect_uri` that does not exactly match one registered for the client, is never redirected. The member sees an error page on STX instead, so the flow cannot be turned into an open redirector. ## Token endpoint errors `/oauth/token`, `/oauth/revoke` and `/oauth/introspect` answer errors in the RFC 6749 shape: ```json {"error": "invalid_grant", "error_description": "the refresh token has already been used"} ``` | `error` | Status | When | | --- | --- | --- | | `invalid_request` | `400` | A required parameter is missing: `grant_type`, or for its grant `code`, `redirect_uri`, `code_verifier` or `refresh_token`; `token` on revoke. | | `invalid_client` | `401` | Client authentication failed or was not sent. The response carries `WWW-Authenticate: Basic`. | | `invalid_grant` | `400` | The code or refresh token is invalid, expired or already used, or `redirect_uri` does not match the authorize request. `error_description` says which. | | `unsupported_grant_type` | `400` | `grant_type` is not `authorization_code`, `refresh_token` or `client_credentials`. | | `invalid_scope` | `400` | On `client_credentials`: the requested scope is unknown, not an app scope, or not allowed for your client. | | `ip_not_allowed` | `403` | Your app has an IP allowlist and this request came from another address. Checked after client authentication, for every grant type. | | `temporarily_unavailable` | `503` | The request was not judged. The response carries `Retry-After`; retry with the same code or token. | A Web app or Server-to-server app authenticates with `client_secret_basic` only; a secret sent in the form body is not accepted. How to recover from a failed refresh is covered under [rotation and reuse detection](/oauth/tokens-and-security/#rotation-and-reuse-detection). `/oauth/revoke` answers `200` for a known, unknown or another client's token alike, and `/oauth/introspect` answers `{"active": false}` for any token that is not a live access token of yours, so neither reveals whether a token was valid. ## Registration errors `POST /oauth/register` uses the same `{"error", "error_description"}` shape. | Status | `error` | When | | --- | --- | --- | | `400` | `invalid_redirect_uri` | `redirect_uris` is missing or empty where callbacks are needed, or a callback is not `https`, a reverse-domain custom scheme or `http://localhost` / `127.0.0.1` / `[::1]` (any port) for an installed app, or carries a fragment. | | `400` | `invalid_client_metadata` | Any other field is missing or not allowed, including a write scope (`orders.write`, `terms.write`), which needs an IP allowlist only STX sets; see the [field table](/oauth/tokens-and-security/#dynamic-registration). | | `429` | `rate_limited` | Too many registrations from your address this hour. The response carries `Retry-After`. | | `404` | | Dynamic registration is off. | ## API errors These are the errors a call made with a token gets from `/api/v1` and the WebSocket. A missing, expired or revoked token is `401`, pointing at the [protected resource metadata](#protected-resource-metadata): ``` HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer error="invalid_token", resource_metadata="https:///.well-known/oauth-protected-resource" {"error": "Missing or invalid OAuth access token"} ``` A call outside the [effective scope](/oauth/scopes/#effective-scope) is `403` with a JSON body and an RFC 6750 challenge naming the scope that would cover it: ``` HTTP/1.1 403 Forbidden WWW-Authenticate: Bearer error="insufficient_scope", scope="orders.write" {"error": "insufficient scope for this operation", "scope": "orders.write"} ``` Send the member back through `/oauth/authorize` asking for that scope; the grant only widens, so nothing already granted is lost. A route no scope covers gets the same `403` with no `scope`. A path that is not a route at all is `404`. A call from an address outside your app's IP allowlist is `403`: ``` HTTP/1.1 403 Forbidden {"error": "ip_not_allowed", "error_description": "this app does not allow requests from this IP address"} ``` On the WebSocket, a join the token does not cover is refused with `{"reason": "insufficient_scope"}`, and a handshake from an address outside the allowlist is refused with `403` and the header `x-stx-error: 5301 - ip_not_allowed`. Handle the HTTP cases distinctly: `401` means refresh, then send the member back through consent if the refresh fails; `403 insufficient_scope` means ask for more scope; `403 ip_not_allowed` means call from an address on your allowlist. --- # Scopes Source: https://docs.stxapp.io/oauth/scopes/ A scope is a named slice of a member's account that they grant your app. You request scopes on the [authorize](/oauth/authorization-flow/) request, the member approves them, and every token your app holds is confined to them. Ask for the least you need: members approve a short list faster than a long one. There are two disjoint vocabularies. **Member scopes** travel on a member token, through consent. **App scopes** travel on an app token (`client_credentials`), with no member. Neither ever satisfies the other. ## Member scopes Named `resource.action`, where the action is `read` or `write`. This is the complete list. Paths are under `/api/v1`; `{user_id}` is the member's id from `GET /api/v1/me`. | Scope | Lets your app | REST | Channels | | --- | --- | --- | --- | | `profile.read` | See who the member is: user and account ids, public handle and avatar. No name or other personal details | `GET /me` | | | `balance.read` | See their cash balance | `GET /account/balance` | `balances:{user_id}` | | `portfolio.read` | See their open positions, settlement history, and loyalty, fee and adjustment ledgers | `GET /positions`, `GET /account/market_stats`, `GET /portfolio/settlements`, `GET /portfolio/loyalty`, `GET /portfolio/fees`, `GET /portfolio/adjustments` | `positions:{user_id}`, `settlements:{user_id}` | | `orders.read` | See their orders and fills | `GET /orders`, `GET /orders/{id}`, `GET /fills` | `orders:{user_id}`, `fills:{user_id}` | | `transfers.read` | See their deposit and withdrawal history | `GET /portfolio/deposits`, `GET /portfolio/withdrawals` | | | `orders.write` | Place and cancel orders for them | `POST /orders`, `POST /orders/batched`, `DELETE /orders/{id}`, `DELETE /orders/batched`, `DELETE /orders/all` | joining `orders:{user_id}` with `cancel_on_disconnect` | | `terms.write` | Accept the terms and conditions for them | `POST /tnc/accept` | | `GET /fills` takes an `order_ids` filter (comma-separated UUIDs of the member's own orders) alongside `market_ids`, both under `orders.read`. An id belonging to another account matches nothing rather than erroring. A member channel needs the matching scope **and** the member's own `user_id` in the topic. Any other account channel (`account:`, `portfolio:`, `user_info:`, `active_*`, and the rest) is refused to an OAuth token whatever its scopes. ## App scopes | Scope | REST | Channels | | --- | --- | --- | | `market_data` | `GET /markets` | `market:`, `orderbook`, `ticker`, `trades`, `market_stats`, `markets`, `market_info`, `market_updates` | | `events` | `GET /events` | `events` | An app token is refused by every member endpoint and member channel. ### Member tokens read the catalogue too `GET /api/v1/markets` and `GET /api/v1/events` are the market and event catalogue. A **member** token reads both whatever its scopes, and joins the public market-data channels above without any scope. An app token is needed only to read the catalogue with no member involved. ## No scope moves money There is deliberately no scope that initiates, approves or cancels a deposit or a withdrawal. `transfers.read` is read-only history. A member funds their account on STX, never through your app, which keeps your app out of money transmission. ## Effective scope The scope a request actually has is ``` token scopes ∩ grant scopes ∩ client.allowed_scopes ``` evaluated on every call. A member revoking the grant, or STX narrowing your client's allowed scopes, takes effect on the next request without waiting for the token to expire. For an app token the grant term drops out: `token scopes ∩ client.allowed_scopes`. ## How a request is narrowed - The authorize request's `scope` is intersected with the scopes your client is allow-listed for. Scopes outside the allow-list are dropped, and the consent screen shows what remains. - An unknown scope, or nothing left after narrowing, fails with `invalid_scope`. A request with no `scope` fails with `invalid_request`. Both come back to your `redirect_uri`. - The member allows or denies the list as a whole. ## The grant only widens A member's grant records every scope they have approved for your app. Approving more later widens it; a later request for fewer does not shrink it. An access token carries the scopes of the request that minted it and is re-checked against the grant on every call, so a wider consent later never widens a token already issued. Consent narrows only when the member revokes it, from [Connected apps](/isv/hosted-pages/#connected-apps). ## `orders.write` and verification A member can consent before their identity verification is complete. The grant is issued, and the consent screen tells them that placing orders stays unavailable until verification completes. --- # Tokens and security Source: https://docs.stxapp.io/oauth/tokens-and-security/ The token lifecycle and the security properties you can rely on. Read it before you go to production. `$STX` in the examples is the base URL STX gives you with your invite. ## The tokens STX issues opaque tokens. They are not JWTs, carry no readable claims, and their meaning lives only in STX. Treat them as strings. | Token | Prefix | Lifetime | Use | | --- | --- | --- | --- | | Access token | `stxapp_at_` | `expires_in`, 3600 seconds | Bearer on every call | | Refresh token | `stxapp_rt_` | Expires after 14 days unused, and capped (see [Refresh lifetime](#refresh-lifetime)) | Get a new pair, no member involved | | Authorization code | `stxapp_code_` | 60 seconds, single use | Exchanged once for the first pair | Every STX credential has the form `stxapp__` followed by 43 base62 characters and a 6-character checksum: `stxapp_at_` access token, `stxapp_rt_` refresh token, `stxapp_code_` authorization code, `stxapp_cs_` client secret, `stxapp_client_` client id. The fixed shape lets secret scanners spot a leaked credential. Store the whole string. An app token is a `stxapp_at_` access token from `client_credentials`, with no refresh token and no member. Hold tokens on your server where you have one, encrypted at rest, per member. A Browser app or Mobile or desktop app keeps them in memory or the platform's secure storage, never in a URL, a log or a shipped bundle. ## Refreshing ```bash curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$STX/oauth/token" \ -d grant_type=refresh_token \ -d refresh_token="$REFRESH_TOKEN" ``` You receive a new access token and a new refresh token; the old refresh token is retired. **Always store the new refresh token.** ### Rotation and reuse detection A retired refresh token presented again is treated as stolen: STX revokes the whole grant. Both the thief and your app lose access, and the member must reconnect. A 60-second grace window lets an ordinary retry of the same refresh return the same successor instead of tripping this. Never refresh with a token you have already rotated past. ### Refresh lifetime A refresh token not used for 14 days expires. Every refresh chain also ends at a fixed cap counted from the member's approval: 90 days for a Web app or Server-to-server app, 30 days for a Browser app or Mobile or desktop app. Refreshing never extends it. Past either limit a refresh returns `400 invalid_grant`; send the member through the authorization flow again to re-approve. ### When a refresh fails A failed refresh is `400` with `error: "invalid_grant"` and an `error_description` saying why (reused, expired, or invalid); see [token endpoint errors](/oauth/discovery-and-errors/#token-endpoint-errors). Treat it as terminal for that member: clear the stored pair and send them back through consent. A `503` with `temporarily_unavailable` means the refresh was not judged; retry with the same token. ## Revoking To disconnect a member from your side (sign-out, account deletion): ```bash curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$STX/oauth/revoke" -d token="$ACCESS_TOKEN" ``` - Revoking either token of a pair kills both. Other pairs under the same grant (another device of yours) are untouched. - The answer is `200` for a known, unknown, or another client's token alike, so it never reveals whether a token was valid. `503` means retry. - A Browser app or Mobile or desktop app revokes with `client_id` in the form instead of a secret. A member can also revoke your app from [Connected apps](/isv/hosted-pages/#connected-apps) in their STX account. That revokes the whole grant and every token under it; your next call for that member returns `401`. If STX suspends, closes, bans or otherwise restricts a member's account, your tokens for that member stop working at once and the grant is revoked; the member must approve your app again once reinstated. A temporary cool-off (the `cool_off` account status) only pauses access until it lifts. When a token is revoked, rotated or expires, STX closes any WebSocket opened with it. The member's own sessions are unaffected. ## Checking a token `POST /oauth/introspect` with `token=` answers `{"active": true, "scope": "…", "client_id": "…", "token_type": "bearer"}` for a live access token of your own, and `{"active": false}` for anything else, including another client's token. It requires `client_secret_basic`. The reported `scope` is the effective scope. Use it for debugging, not the hot path; trust the `401` and `403` from the API. ## Apps without a client secret A Browser app or Mobile or desktop app (see [application types](/isv/#application-types)) has `token_endpoint_auth_method: "none"`. It has no secret, sends no `Authorization` header to `/oauth/token` or `/oauth/revoke`, and identifies itself with a `client_id` form field; its proof is the PKCE verifier. It can redeem codes and refresh, and cannot use `client_credentials` or introspection. ## Dynamic registration **Dynamic client registration** (RFC 7591) is `POST /oauth/register`, unauthenticated, JSON body. It is **off by default**: while off it answers `404` and the metadata has no `registration_endpoint`. | Field | | | --- | --- | | `client_name` | required | | `redirect_uris` | `https`, or a reverse-domain custom scheme or `http://localhost` / `127.0.0.1` / `[::1]` (any port) for an installed app; none needed for `client_credentials` only. No fragment | | `scope` | optional; space-separated known scopes, member or app | | `token_endpoint_auth_method` | `client_secret_basic` or `none`. If omitted: `none` when any callback is local or a custom scheme, otherwise `client_secret_basic`. The response echoes the method chosen | | `grant_types` | subset of `authorization_code refresh_token client_credentials`, default `authorization_code refresh_token`; `refresh_token` needs `authorization_code`; `client_credentials` needs a secret | | `response_types` | `["code"]` | | `client_uri`, `logo_uri` | optional absolute `https` URLs | | `software_id`, `software_version` | optional strings | Without `scope`, the client is allow-listed for the read-only default `profile.read balance.read portfolio.read orders.read`, plus the app scopes when `grant_types` includes `client_credentials`. It cannot request a write scope (`orders.write`, `terms.write`): those need an IP allowlist, which only STX sets. Register with read scopes and ask STX to add the write scope. A request that names one fails with `400 invalid_client_metadata`. `201` returns `client_id`, `client_id_issued_at`, the stored metadata and, for an app with a secret, a one-time `client_secret` with `client_secret_expires_at: 0`. Errors are listed under [registration errors](/oauth/discovery-and-errors/#registration-errors). ## Security properties you can rely on - **PKCE is mandatory**, `S256` only. An intercepted code is useless without the verifier your server holds. - **Codes are single use.** Presenting a code twice revokes the member's grant to your app. - **Callbacks are matched exactly**, byte for byte, at both the authorize and token steps, except a local callback (`localhost`, `127.0.0.1`, `[::1]`), which matches on any port. An unknown client or callback gets an error page on STX and is never redirected. - **An IP allowlist guards writes.** When your app has an IP allowlist, token requests, REST calls and WebSocket connections from any other address are refused with `403 ip_not_allowed`. Any write scope requires one. - **Tokens are bound to your client.** Refresh, revoke and introspect check that the token belongs to the authenticating client. - **Scope is re-checked on every call** against the member's current grant and your client's current allow-list; see [effective scope](/oauth/scopes/#effective-scope). - **Suspending a client cuts its tokens.** A client STX suspends or revokes stops resolving on its members' live tokens and its app tokens at once. - **Two-factor stays with STX.** It is enforced at STX sign-in; your app only receives tokens after the member has authenticated. - **No scope moves money.** See [Scopes](/oauth/scopes/#no-scope-moves-money). ## A checklist for production - Store `client_secret`, access and refresh tokens server-side and encrypted where you have a server. - Verify `state` on every redirect back; keep the PKCE verifier per pending sign-in. - Refresh proactively; store the rotated refresh token every time; never present one twice. - Handle `401` (refresh, then reconnect) and `403` (ask for the named scope) distinctly; see [API errors](/oauth/discovery-and-errors/#api-errors). - Handle `403 ip_not_allowed` separately from `403 insufficient_scope`: call from an address on your allowlist. - Revoke on sign-out, and expect members to revoke you. - Request the fewest scopes that do the job. --- # Integrate with STX > Pick your environment, register and create an API key, sign your first request, and connect over WebSocket. Source: https://docs.stxapp.io/quick-start/ export const US = ENV.regions.find((r) => r.id === 'us').environments.find((e) => e.id === 'integration') export const CA = ENV.regions.find((r) => r.id === 'ca').environments.find((e) => e.id === 'integration') export const CA_PROD = ENV.regions.find((r) => r.id === 'ca').environments.find((e) => e.id === 'production') Go from nothing to a resting order on the book, with a socket streaming your own fills as they happen. By the end you will have: - an API key you created yourself, and a signed request the exchange accepts - your `user_id`, which your account channel topics are keyed on - an order placed and cancelled against a live market - a WebSocket connection pushing your orders, fills and positions as they happen Everything here runs against a demo environment: a real-time order book with other developers trading in it, and no real money at risk. You pick which one in the next step, and the interactive panels below follow that choice. ## Authentication ### 1. The environment STX runs two exchanges, one in the US and one in Ontario. Pick the one you are integrating with. They sit under different regulators and accounts do not carry across, so a key from one will not work on the other. See [United States and Canada](/concepts/us-and-canada/) if you are not sure which. | Exchange | Demo | Production | | --- | --- | --- | | **United States** | {US.url} | not yet open for trading | | **Canada (Ontario)** | {CA.url} | {CA_PROD.url} | These are the API hosts your requests go to. Demo environments carry no real money and are shared by every developer, so the order books have genuine activity in them. :::caution[Demo keys do not work in production] A key belongs to the environment it was created in. When you move to production, register there and create a new key; a demo key is rejected with a 401. ::: ### 2. Register and create an API key Keys are self-service. Register on the web app for the exchange you are integrating with, then mint a key from your profile. | Exchange | Register at | | --- | --- | | United States | {US.appUrl.replace('https://', '')} | | Ontario | {CA.appUrl.replace('https://', '')} | | Ontario production | {CA_PROD.appUrl.replace('https://', '')} | Accounts do not carry across exchanges. Register on the one you are targeting, and see [United States and Canada](/concepts/us-and-canada/) if you are not sure which. 1. Register, and **verify your email address** 2. Open the profile icon, top right → **My Profile** → **API Keys** 3. **Create key**, and give it a nickname 4. Choose **Read only** or **Read/Write**. Read/Write is required to place or cancel orders; this is the value `GET /api/v1/me` returns as `scope` 5. Optionally paste your own **Ed25519 public key**. Leave it blank and STX generates the keypair for you 6. **Create** You get a **key ID** and, unless you supplied your own public key, a **private key PEM**: ```text Key ID: a1b2c3d4e5f60718293a4b5c6d7e8f90 Private key: -----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIH... -----END PRIVATE KEY----- ``` :::caution[The private key is shown once] It cannot be re-downloaded. If you lose it, delete that key and create a new one. :::
Optional: bring your own key, so the private half never leaves your infrastructure Leaving the public key field blank is fine. STX generates the pair and shows you the private key once. If you would rather it never existed on someone else's server, generate the pair yourself and paste the **public** half into step 5: ```bash title="Generate an Ed25519 keypair" openssl genpkey -algorithm ed25519 -out stx.pem openssl pkey -in stx.pem -pubout # paste this into the dialog ``` Worth doing for a production trading system; unnecessary while you are trying the API out.
One key authenticates both surfaces: APIs and the WebSocket channels. ### 3. Deposit Nothing to do. Demo accounts are credited with test funds automatically when you register, so you have a balance to trade against from the moment your key works. There is no deposit step and no funding request. The money is not real and neither is the risk: demo environments settle in test currency and are wiped independently of production. Read your available balance and exposure with [`GET /api/v1/account/balance`](/api/rest/account/get-account-balance/), or stream them as they change on the [`balances`](/websockets/channels/balances/) socket channel. Placing an order reserves liability against that balance. If an order is rejected for insufficient funds on an account you have just created, check that you registered on the demo host in the table above rather than a production one. Run the balance down and you can ask for a top-up in **#dev** on [Discord](https://discord.gg/yF9eVzPzNZ), which is also where to bring anything else that comes up while you integrate. You will be talking to the engineers who build the exchange. ## REST ### 4. Send a request Enter your demo key ID and private key below, pick any endpoint and send it for real. You see the signed headers, a copyable curl command and the actual response. Signing happens in your browser with WebCrypto, and your private key is never transmitted. It is saved in this browser's local storage so the other widgets on this site can use it, until you clear it. Only demo hosts are offered, so use a demo key. ## WebSockets ### 5. Connect over WebSocket The same key authenticates the WebSocket, but the handshake signs differently from a REST call: always `GET`, and against the socket path only. ``` message = timestamp_ms + "GET" + "/socket/websocket" ``` Note the path excludes the query string, even though you connect to `wss:///socket/websocket?vsn=2.0.0` on whichever exchange you chose. Send a `User-Agent` header on the WebSocket handshake. A handshake without one is refused with `403`. Browsers always send one; some WebSocket libraries, such as Node's `ws`, do not unless you set it. Your `user_id` comes only from `GET /api/v1/me`. There is no other way to look it up. :::tip[Handshake accepted, join refused] A bad handshake does not fail where you would expect it to. The connection opens, and the failure surfaces later when you join an account channel, as `{"status":"error","reason":"unauthorized"}`. That almost always means the header prefix rather than the key: the socket needs `X-STX-ACCESS-*`, and the REST names without the prefix are invisible to it. See [Authentication](/api/authentication/). ::: Connect for real, right now. This joins the live market feed from your browser. A browser cannot add signed headers to a WebSocket handshake, so this connection is unsigned, which market-wide channels accept: ### Account channels Your own channels, such as `orders`, `fills`, `positions` and `balances`, need a signed connection, and a browser cannot sign a WebSocket handshake. Run one from your machine instead: - [stx-api-examples](https://github.com/stxapp/stx-api-examples): `watch_channel.py` and `watch_channel.mjs` join any channel with your API key, for example `--topic 'orders:'`, and look up your `user_id` for you. - The [SDKs](/sdks/) sign the connection and keep it alive for you. See [WebSocket channels](/websockets/) for the full channel list and frame format, or watch it live on [`cancel_on_disconnect`](/risk-controls/). ## Next
- [Postman collection](/downloads/stx-rest-api.postman_collection.json): every REST endpoint, with the signing script wired in - [REST API reference](/api/rest/): every endpoint, request and response - [WebSocket channels](/websockets/): every channel, with its topic and payloads - [Runnable examples on GitHub](https://github.com/stxapp/stx-api-examples): Python quickstart and a live watcher - [Rate limits](/concepts/rate-limits/) - [Fees](/concepts/fees/) - [Account limits](/concepts/account-limits/)
--- # Risk controls Source: https://docs.stxapp.io/risk-controls/ Orders rest on the book whether or not you are watching them. These controls decide what happens to yours when you are not the one deciding: when your connection drops, when the game starts, when a time you picked passes, and when play goes live. Most you set yourself; the in-play delay the exchange applies to you. None of them are on by default. An order placed with no expiration and no `cancel_on_disconnect` rests on the book until you cancel it or the market settles, whether or not your process is still running. ## Order expiration `expiration` is an optional field on `POST /api/v1/orders`. | `expiration` | The order expires | | --- | --- | | *omitted* | Never. It rests until you cancel it or the market resolves. This is the default. | | `good_till_start` | When the event starts. | | `good_till_time` | At `expiration_time`. | `expiration_time` is **Unix microseconds**, not milliseconds or seconds, and is required when `expiration` is `good_till_time`. Microseconds is unusual enough that a value in milliseconds looks plausible and puts the expiry in 1970, which expires the order immediately. `good_till_start` is the one to reach for if you place orders pre-game and do not want to trade in-play. It is keyed on the **event**, not the market, so every market on one game expires together. ## Cancel orders on disconnect If your process dies, your host sleeps or the network drops, the orders you left on the book keep trading without you. The `cancel_on_disconnect` flag has the exchange pull them when it stops hearing from your client, so a connection failure does not leave orders working that you can no longer manage. Unlike the others, you arm it over the WebSocket rather than on the order alone, and it takes **both** halves to work. ### Arming it on the channel Enabled when you join, not per request: ```json ["1", "1", "orders:{user_id}", "phx_join", { "cancel_on_disconnect": true, "ping_timeout": 5000 }] ``` | Parameter | Notes | | --- | --- | | `cancel_on_disconnect` | Boolean. Defaults to `false`. | | `ping_timeout` | Milliseconds. **Must be an integer**; a non-integer fails the join with `{"ping_timeout": "Must be an integer"}`. Clamped to **5000–20000**; values outside the range are silently pulled to the nearest bound. | Once joined, send a `ping` on the channel before each `ping_timeout` elapses. The server replies `{"ping": "pong", "ttl": }` and resets the clock. Ping at roughly 60% of the window so a single slow round trip does not cost you the connection. This timer is not the socket's own keep-alive. Phoenix closes an idle socket after 60 seconds; `ping_timeout` is a separate, much shorter deadline that only governs cancellation. ### Opting an order in ```json POST /api/v1/orders { "market_id": "855faa81-7115-4f4f-b492-389fbd8c1ed8", "order_type": "limit", "action": "buy", "price": "0.46", "quantity": "25", "cancel_on_disconnect": true } ``` The body is flat. Wrapping the order in `user_order`, as some older examples do, returns `400 market_id is required`, which reads like the field is missing when the real problem is the wrapper. See [Place an order](/api/rest/orders/place-order/). :::caution[The two flags are not the same flag] The one on the channel join arms the mechanism. The one on the order decides whether that order is included. Arming the channel and then placing orders without the field cancels nothing, and setting it on orders without ever joining an armed channel cancels nothing either. ::: The server echoes back the `ping_timeout` it actually applied, which is not always the one you asked for: ```json {"cancel_on_disconnect": true, "ping_timeout": 15000} ``` Read the value from that reply rather than assuming your request was honored. :::caution[One heartbeat is not enough] There are **two independent timers**, and the socket heartbeat only resets one of them. | | Resets it | Deadline | Consequence of missing it | | --- | --- | --- | --- | | Socket keep-alive | `heartbeat` on the `phoenix` topic | 60s | The connection closes | | `cancel_on_disconnect` | `ping` on the `orders` topic | **5–20s**, whatever you asked for at join | **Your flagged orders are cancelled** | A 30-second heartbeat keeps the socket up and still misses the `ping` deadline, so your book is cancelled on a connection that never dropped. Send the channel `ping` inside your configured `ping_timeout`, on a timer rather than in response to traffic. ::: ### What happens when you stop pinging 1. The deadline passes and the server closes the `orders` channel: your client receives a `phx_error` event on that topic. The socket itself stays open. 2. A **20-second grace period** starts. Reconnect inside it and your orders are untouched, which is what makes a brief network blip safe. 3. If the grace expires, every order whose own `cancel_on_disconnect` is `true` is cancelled. Orders without the flag stay on the book. An order flagged after your last `ping` is still covered: it is cancelled when that `ping_timeout` expires. The same applies to orders placed once the channel is already considered dead, so placing new orders does not stop a cancellation that is already running. ## Cancelling in bulk The manual equivalent, for when you are shutting down deliberately rather than failing: | | | | --- | --- | | `DELETE /api/v1/orders/all` | Everything resting on your account. | | `DELETE /api/v1/orders/batched` | A named list. The body takes `orders`, an array of objects each carrying an `order_id`, not a flat array of ids. | A cancel is a request, not a guarantee. An order can fill in the moment between you sending the cancel and the exchange processing it, so reconcile against `fills` rather than assuming a cancel left you flat. ## The in-play delay This one you do not set. While an event is `in_progress`, incoming orders on its markets are held in a queue before reaching the book. It exists so a participant with a faster feed than the exchange cannot pick off prices that have not caught up with play yet. An order sitting in that queue reads as [`delayed`](/concepts/market-status/): accepted, holding liability, not yet working. Whether it applies to a given order depends on: - **The event must be `in_progress`.** There is no delay pre-game. - **The market sets the duration.** Read `in_play_delay_sec` from `GET /api/v1/markets`; it is also pushed on the `market_updates` channel. The value is configured per sport, so it differs between a football market and a cricket one. Read the field rather than assuming a number. - **The account must be subject to it.** It can be switched off per account, in which case orders are never delayed regardless of the market's value. Because the duration is per market and the exemption is per account, `in_play_delay_sec` on a market is the ceiling, not a promise. If you need to know whether your own orders are being delayed, watch how long they sit at `delayed` rather than computing it. --- # SDKs > Official client libraries for the STX API. Source: https://docs.stxapp.io/sdks/ The REST and WebSocket APIs are the contract, and every SDK is a convenience on top of them. Nothing here does anything you could not do with an HTTP client and a WebSocket, so an SDK is worth taking when it saves you work and worth skipping when it does not. ## Available now - [TypeScript SDK](/sdks/typescript/): @stxapp/stx-typescript on npm. Node 18+, typed client, WebSocket channels with a live account view, API-key and OAuth authentication. - [Python SDK](/sdks/python/): stx-python on PyPI. Python 3.9+, sync and async clients, WebSocket channels, API-key authentication. - [C# SDK](/sdks/csharp/): STX.Sdk on NuGet. .NET 8 and .NET 10, typed services and channel wrappers, API-key authentication. --- # C# SDK > Official .NET SDK for the STX trading API. Source: https://docs.stxapp.io/sdks/csharp/ :::note **.NET 8 and .NET 10.** Published to NuGet as [**STX.Sdk**](https://www.nuget.org/packages/STX.Sdk). MIT-licensed. A .NET 8 or .NET 9 app resolves the `net8.0` build; a .NET 10 app resolves `net10.0`. ::: `STX.Sdk` is the official .NET SDK for the STX trading exchange. It wraps the exchange HTTP API and Phoenix WebSocket channels as a set of services you register into the .NET dependency-injection container and resolve anywhere in your app. One package, one `ConfigureSTXServices` call, dozens of typed service + channel clients. - **Services** for everything the exchange exposes: login, markets, events, orders, trades, settlements, profile, terms-and-conditions, geolocation. - **Channel wrappers** for real-time: portfolio balance, active orders/trades, positions, market updates, user info. - **Automatic session management**: optional background services keep the JWT refreshed and the geolocation token current. ```bash dotnet add package STX.Sdk ``` ```csharp // Register once in Program.cs / Startup.cs services.ConfigureSTXServices( STXEnvironment.OntarioDemo, STXApiKeyCredentials.FromPemFile(keyId, "~/.stx/ontario.pem")); // Resolve and use anywhere var markets = await marketService.GetMarketInfosWithCountAsync(); ``` Primarily targeted at **market makers** writing low-latency .NET trading bots, but the surface is generic, so any integrator building a .NET app against STX can use it. ## Units The SDK speaks **integer cents**: `price: 10` is 10¢, and `Price`, `Fee` and every other amount come back as integers in cents. If you also read the REST and WebSocket documentation you will see prices written as dollar strings (`"0.10"`). Same prices, different surface. Pick one surface per component and the conversion never comes up; mix them and convert at the boundary. ## Where to go next - [Quickstart](/sdks/csharp/quickstart/): Install, authenticate, and make your first call in under five minutes. - [Installation](/sdks/csharp/installation/): NuGet, supported runtimes, dependencies. - [Authentication](/sdks/csharp/authentication/): API keys, Ed25519 signing, scopes. - [Configuration](/sdks/csharp/configuration/): DI registration, endpoint URIs per environment. - [Market data](/sdks/csharp/markets/): Query markets, events, sports, and competitions. - [Trading](/sdks/csharp/trading/): Place and cancel orders; fetch order/trade history. - [WebSockets](/sdks/csharp/websockets/): Phoenix channels for real-time orders, trades, balance, markets. - [Errors & retries](/sdks/csharp/errors-and-retries/): Exceptions you'll encounter and the built-in Polly retry. ## Example apps Two reference apps live in one MIT-licensed repository: - **`console/`**: CLI trading bot showing authentication, market subscriptions, and order placement from a `BackgroundService`. - **`webapi/`**: ASP.NET Core 8 service exposing the SDK over HTTP, with Swagger UI. Both run against the public demo environment out of the box: ```bash git clone https://github.com/stxapp/stx-csharp-demo.git ``` --- # Authentication > API-key signing, email/password login, 2FA, token refresh, and background session keep-alive. Source: https://docs.stxapp.io/sdks/csharp/authentication/ The SDK supports two ways to authenticate, and you pick one when you register it. | | When to use it | |---|---| | **API key** (recommended) | Programmatic access: bots, services, anything unattended | | **Email and password** | Apps acting on behalf of a person signing in, including 2FA flows | API keys are the better fit for automated callers: there is no login call, no token to expire, and no refresh cycle, so a restart or an API deployment cannot leave a client without a session. ## API-key authentication Create a key under **Account → API Keys**, which gives you a key ID and an Ed25519 private key. Register the SDK with both: ```csharp using STX.Sdk; using STX.Sdk.Auth; using STX.Sdk.Settings; services.ConfigureSTXServices( STXEnvironment.OntarioProduction, STXApiKeyCredentials.FromPemFile(keyId, "~/.stx/ontario.pem")); ``` `FromPemFile` reads the key from disk so it never has to pass through an environment variable, shell history, or a process listing. If you already hold the PEM in memory, the constructor takes it directly: ```csharp new STXApiKeyCredentials(keyId, pemContents); ``` There is nothing else to call. Every request is signed automatically with three headers: | Header | | |---|---| | `X-STX-ACCESS-KEY` | Your key ID | | `X-STX-ACCESS-TIMESTAMP` | Unix time in milliseconds | | `X-STX-ACCESS-SIGNATURE` | Base64 Ed25519 signature over the timestamp, method, and path | :::caution The server rejects any request whose timestamp is more than 30 seconds from its own clock. Keep the host clock on NTP. ::: ### Knowing who you are There is no login response under API-key auth, so the user id that WebSocket channels need has to be fetched once: ```csharp var identity = await serviceProvider.GetRequiredService().GetMeAsync(); Console.WriteLine($"{identity.UserId} / account {identity.AccountId} / scope {identity.Scope}"); ``` Do this at startup, before subscribing to any user-scoped channel. ### Scopes A key is issued as either `read_only` or `read_write`. A `read_only` key can query markets, orders, trades, and settlements, but placing or cancelling an order needs `read_write`. `STXIdentity.Scope` reports which one you hold. ### Bringing your own cryptography `STXApiKeyCredentials` also accepts a signing delegate, for callers who keep private keys in an HSM or a KMS rather than on disk: ```csharp new STXApiKeyCredentials(keyId, message => myHsm.SignEd25519(message)); ``` ## Existing integrations: email and password :::note Email and password authentication is supported for integrations that already use it. It is **not recommended for new work**, and it is not documented here. Use an API key instead. ::: The path still exists and keeps working: `STXLoginService.LoginAsync` acquires a JWT, `STXSessionBackgroundService` refreshes it, and everything else in the SDK behaves the same once authenticated. Nothing about it changed in 1.6.0. It is the weaker option for a programmatic caller. A token expires, so a restart or an API deployment can leave a client without a session, and the credentials have to sit in memory for the background service to re-login. An API key has none of those properties. ### Moving to an API key 1. Create a key under **Account → API Keys** and choose a scope. `read_only` covers market, order, trade and settlement queries; placing or cancelling orders needs `read_write`. 2. Swap the registration to the API-key overload shown above. Nothing else in your code changes. 3. Replace the user id you read from the login response with one `GetMeAsync()` call at startup, since there is no login response to read. 4. Drop the `keepSessionAlive` handling. There is no token to keep alive. ## Accept terms & conditions Until the current terms are accepted, authenticated calls are rejected. :::caution **`CheckTermsAndConditionsAsync` does not only check.** If the current terms are already in effect, it **accepts them on the user's behalf** and returns the result of that acceptance. Do not call it as a read-only test before asking a user for consent: by the time it returns, consent has been recorded. ::: To show the terms and accept only after the user has agreed, use the two explicit calls and do not call `CheckTermsAndConditionsAsync` at all: ```csharp var tnc = serviceProvider.GetRequiredService(); // Read-only: fetches the current terms without accepting anything. var current = await tnc.GetTermsAndConditionsAsync(); // Display current.Version to the user. Only once they have agreed in your UI: await tnc.AcceptTermsAndConditionsAsync(current.Version); ``` `AcceptTermsAndConditionsAsync` also takes no argument, in which case the server accepts the version currently in effect. Pass the version you actually displayed where you can, so what was accepted is what the user saw. ## Logout There is no server-side logout call. With an API key there is nothing to end: requests are signed individually and no session is held. To revoke access, delete the key under **Account → API Keys**. Signatures made with it stop being accepted immediately. On the legacy email/password path the session lives in `STXUserStorage`, which is a singleton. Stop the host to end it, and stop any channels you started with `StopAsync`. ## Errors | Exception | Cause | |---|---| | `STXRequestFailedException` | Signature rejected. Check the host clock: the server allows 30 seconds of skew | | `STXWrongCredentialsException` | Bad email/password, on the legacy path | | `STXSessionExpiredException` | Token and refresh token both expired, on the legacy path | | `STXGeoComplyException` | Geo-compliance check failed; user is outside a permitted jurisdiction | See [Errors & retries](/sdks/csharp/errors-and-retries/) for the full list and recovery patterns. --- # Configuration > Register STX.Sdk services and pick the right endpoints per environment. Source: https://docs.stxapp.io/sdks/csharp/configuration/ One call wires up every service, channel, and background worker the SDK provides. ## Register services Name the environment and supply credentials. You do not need to know any URLs: ```csharp using STX.Sdk; using STX.Sdk.Auth; using STX.Sdk.Settings; using Microsoft.Extensions.DependencyInjection; services.ConfigureSTXServices( STXEnvironment.OntarioDemo, STXApiKeyCredentials.FromPemFile(keyId, "~/.stx/ontario.pem")); ``` The same overload exists without credentials, for email and password: ```csharp services.ConfigureSTXServices(STXEnvironment.OntarioDemo); ``` `ConfigureSTXServices` adds: - GraphQL HTTP client + typed services (`STXLoginService`, `STXMarketService`, `STXOrderService`, …) - Phoenix channel classes + wrappers (`STXPortfolioChannel`, `STXActiveOrdersChannel`, …) - User storage (`STXUserStorage`) as a singleton - Two hosted background services (`STXSessionBackgroundService`, `STXGeoLocationBackgroundService`) that the hosting framework starts automatically ## Named environments | | Jurisdiction | Real money | |---|---|---| | `STXEnvironment.OntarioDemo` | Ontario | No | | `STXEnvironment.OntarioProduction` | Ontario | **Yes** | | `STXEnvironment.USDemo` | United States | No | Each one carries both the GraphQL endpoint and the WebSocket endpoint, so there is nothing to assemble by hand and no `{0}` placeholder to remember. Demo environments are shared by every integrator, so their order books carry real activity. :::caution `OntarioProduction` is live money. Point at it only once your integration is exercised end to end against a demo environment. ::: ### Anything else For an endpoint not listed above, supply both URLs yourself: ```csharp services.ConfigureSTXServices( STXEnvironment.Custom( graphQLUri: "https://your-host/api/graphql", channelsUri: "wss://your-host/socket/websocket?token={0}&vsn=2.0.0"), credentials); ``` :::note `{0}` in a custom `channelsUri` is a format placeholder. Under email and password it is filled with the session token; under API-key authentication there is no token, so it is left empty and the handshake is signed instead. An empty `token=` in the URL is expected in that case, not a failure. The named environments already include the placeholder. ::: Select the environment from configuration rather than code, so a deploy can swap it without recompiling: ```csharp var environment = builder.Configuration["STX:Environment"] switch { "ontario-production" => STXEnvironment.OntarioProduction, "ontario-demo" => STXEnvironment.OntarioDemo, "us-demo" => STXEnvironment.USDemo, _ => throw new InvalidOperationException("Set STX:Environment") }; services.ConfigureSTXServices(environment, credentials); ``` ### Supplying URLs directly The original overload still works and is unchanged: ```csharp services.ConfigureSTXServices( _ => builder.Configuration["STX:GraphQLUri"]!, _ => builder.Configuration["STX:ChannelsUri"]!); ``` `appsettings.json`: ```json { "STX": { "GraphQLUri": "https://demo.stxapp.ca/api/graphql", "ChannelsUri": "wss://demo.stxapp.ca/socket/websocket?token={0}&vsn=2.0.0" } } ``` Override per environment with `appsettings.Production.json` or env vars (`STX__GraphQLUri=…`). ## Dynamic endpoint resolution If the endpoints depend on something inside DI (a tenant selector, feature flag, etc.), the overload that takes `Func` gives you a callback: ```csharp services.ConfigureSTXServices( getGraphQLUri: sp => sp.GetRequiredService().GraphQLUri, getChannelsUri: sp => sp.GetRequiredService().ChannelsUri); ``` ## Resolving services Register once, resolve anywhere: ```csharp public class OrderPlacer { private readonly STXOrderService _orders; private readonly STXMarketService _markets; public OrderPlacer(STXOrderService orders, STXMarketService markets) { _orders = orders; _markets = markets; } public async Task PlaceAsync() { /* ... */ } } ``` Services are registered as `Transient`, so they are safe to resolve per operation. Channel classes are `Singleton` so state (the active websocket, subscribed topics) is shared across the app. ## Logging The SDK uses `Microsoft.Extensions.Logging`. Hook up the provider you prefer: ```csharp services.AddLogging(builder => builder .SetMinimumLevel(LogLevel.Information) .AddConsole()); ``` The SDK logs at `Debug` for every GraphQL request/response body, `Information` for connection lifecycle, and `Warning`/`Error` for transient + fatal issues. ## GeoComply / GeoLocation `ConfigureSTXServices` registers `STX.GeoComply` and `STX.GeoLocation` internally. For trading bots running server-side, the default behavior is usually what you want: the geolocation token is fetched lazily and cached. If you're embedding the SDK in a client app (desktop, mobile), integrate with GeoComply's SDK per their docs. --- # Errors & retries > Exceptions STX.Sdk throws, what they mean, and how the SDK retries transient failures. Source: https://docs.stxapp.io/sdks/csharp/errors-and-retries/ Every public service method throws a typed exception for the error conditions you care about. Network blips are auto-retried via Polly before surfacing. ## Exceptions you'll see | Exception | When | Recovery | |---|---|---| | `STXWrongCredentialsException` | Email or password rejected at login. Legacy path only | Prompt for fresh credentials | | `STXSessionExpiredException` | JWT and refresh token both expired. Legacy path only | Re-authenticate | | `STXTokenExpiredException` | JWT expired but refresh token is still valid | Call `STXTokenService.RefreshTokenAsync`, or rely on `keepSessionAlive: true` | | `STXCancelOnDisconnectNotEnabledException` | `ConfirmOrderAsync(cancelOnDisconnect: true)` called before joining `STXActiveOrdersChannel` | Join the channel first, or pass `cancelOnDisconnect: false` | | `STXGeoComplyException` | Geo-compliance check failed | User is outside a permitted jurisdiction; surface to your UI | | `GraphQLHttpRequestException` | Non-200 HTTP response from the GraphQL endpoint | Usually transient; retries handled internally (see below) | All STX-prefixed exceptions live in `STX.Sdk.Exceptions`. ## Example: handle auth failures ```csharp try { var me = await identity.GetMeAsync(); } catch (STXRequestFailedException ex) { // A rejected signature lands here. The usual cause is clock skew: the server // allows 30 seconds, so check NTP before suspecting the key. _logger.LogError(ex, "Could not authenticate"); } catch (STXGeoComplyException ex) { // User-facing: "Trading isn't available in your region." _logger.LogWarning(ex, "Geo block"); } ``` ## Example: refresh on expired token (legacy path) Only relevant to email and password. API keys have no token to expire, so none of this is needed. When not using `keepSessionAlive: true`, wrap trading calls with a token-refresh retry: ```csharp async Task WithSession(Func> call) { try { return await call(); } catch (STXTokenExpiredException) { await _tokens.RefreshTokenAsync(); return await call(); } catch (STXSessionExpiredException) { await _login.LoginAsync(_email, _password); return await call(); } } var order = await WithSession(() => _orders.ConfirmOrderAsync(price, qty, marketId, action, type)); ``` ## Transient retries (automatic) Retry is applied per call site, not by the transport, so it covers **some** methods and not others. As of 1.6.0 it covers every order mutation, all of `STXMarketService`, `STXIdentityService`, terms-and-conditions acceptance, and login and token refresh, which previously had no retry at all. These still call the GraphQL client directly and are **not** retried: | Not retried | |---| | `STXOrderService.GetMyOrdersAsync` | | `STXTradeService.GetMyTradesAsync`, `GetMyTradesForOrderAsync` | | `STXSettlementService.GetMySettlementsAsync` | | `STXEventService.GetEventInfosAsync` | | `STXTermsAndConditionsService.GetTermsAndConditionsAsync` | | `STXGeoFencingLicenseService` | If a brief outage during one of those matters to you, wrap it yourself. Everywhere else, don't add your own retry on top: double-retry amplifies an outage rather than absorbing it. Where it does apply the policy is `RequestNumberOfRetry = 3`, meaning **3 retries after the initial call, so 4 requests in total**, with exponential backoff plus jitter: roughly 200ms, 400ms and 800ms, so about 1.4s of sleep on top of the per-attempt HTTP timeout. Size any outer deadline or circuit breaker against that, not against a single request. The policy retries anything that could plausibly succeed on a second attempt: | Retried | Not retried | |---|---| | `HttpRequestException` and other network failures | `STXWrongCredentialsException` | | 5xx from the GraphQL endpoint | `STXUnauthorizedException`, `STXSessionExpiredException`, `STXTokenExpiredException` | | **404**, which is what an ingress returns mid-deployment | `STXBadQueryObjectException`, `STXBadArgumentException`, `ArgumentException` | | Timeouts (`TaskCanceledException` wrapping `TimeoutException`) | **429**, and genuine cancellation | Credential errors are excluded deliberately, including a bare HTTP 401 or 403. Repeating a rejected credential cannot succeed, and on the legacy background re-login path it actively causes harm by driving the account towards a lockout. If a failure persists past the built-in retries, `STXRequestFailedException` surfaces with the original exception as its `InnerException`, so the underlying cause (an HTTP status, a DNS failure, a timeout) is still available: ```csharp catch (STXRequestFailedException ex) when (ex.InnerException is GraphQLHttpRequestException http) { _logger.LogWarning("GraphQL returned {Status}", http.StatusCode); } ``` ## Rate limits The exchange rate-limits by IP + account. Bursty order placement can return an HTTP 429. The SDK does **not** retry 429s (retrying is what the limiter is pushing back against). If you see them, lower your request rate or batch via `ConfirmOrdersAsync`. ## Debugging Enable `Debug` logging to see full GraphQL request/response bodies: ```csharp services.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Debug).AddConsole()); ``` Every call is logged with its operation name, variables, and elapsed time. That's usually enough to tell whether an error was transient or a schema mismatch. --- # Installation > Install STX.Sdk from NuGet, required .NET version, and dependencies. Source: https://docs.stxapp.io/sdks/csharp/installation/ ## From NuGet ```bash dotnet add package STX.Sdk ``` Or via Visual Studio's NuGet Package Manager: search for `STX.Sdk`. Package page: [nuget.org/packages/STX.Sdk](https://www.nuget.org/packages/STX.Sdk). ## Supported runtime | | | |---|---| | **Target frameworks** | `net8.0`, `net10.0` | | **Minimum .NET SDK** | 8.0 | | **Platforms** | Windows, macOS, Linux (any platform with .NET 8 or later) | Older versions of the package target `net7.0`. New projects should start on 8. ## Dependencies `STX.Sdk` pulls in a small set of battle-tested packages: | Package | Purpose | |---|---| | `GraphQL.Client` + `GraphQL.Client.Serializer.Newtonsoft` | Typed GraphQL transport | | `Microsoft.Extensions.Hosting.Abstractions` | DI + `BackgroundService` base classes | | `Microsoft.Extensions.Http` | `HttpClient` factory | | `Microsoft.Extensions.Logging.Abstractions` | `ILogger` | | `Microsoft.IdentityModel.Tokens`, `System.IdentityModel.Tokens.Jwt` | JWT parsing | | `Polly` | Retry + transient-fault handling for GraphQL calls | You don't need to reference any of these directly; they're transitive through `STX.Sdk`. ## Using the SDK without `Microsoft.Extensions.Hosting` The canonical pattern uses `Host.CreateDefaultBuilder(...)` because that bootstraps the DI container and runs background services (session keep-alive, geolocation). For small scripts you can roll your own `ServiceCollection`: ```csharp var services = new ServiceCollection(); services.ConfigureSTXServices(STXEnvironment.OntarioDemo); var provider = services.BuildServiceProvider(); var login = provider.GetRequiredService(); ``` Trade-off: background services (`STXSessionBackgroundService`, `STXGeoLocationBackgroundService`) won't start automatically. That is usually fine under API-key authentication, which has no session to keep alive. ## Versioning `STX.Sdk` follows semantic versioning. Breaking changes bump the major version; new services/methods bump the minor; bug fixes bump the patch. Each release is published to NuGet from a version tag. --- # Market data > Query markets, events, sports, and competitions. Source: https://docs.stxapp.io/sdks/csharp/markets/ Market data lives behind two services: - **`STXMarketService`**: individual markets and market counts - **`STXEventService`**: events (a match/game), with its markets attached ## List markets ```csharp var markets = serviceProvider.GetRequiredService(); var resp = await markets.GetMarketInfosWithCountAsync(); foreach (var m in resp.MarketInfos) { Console.WriteLine($"{m.MarketId} {m.Status} {m.Title}"); } ``` `GetMarketInfosWithCountAsync` returns `STXMarketInfosWithCountResponse`: the full market object, plus `Count` and `Cursor` for paging. For lightweight lookups by ID, use the `Short` variant: ```csharp var ids = new[] { "mkt_abc", "mkt_def" }; var short_ = await markets.GetShortMarketInfosWithCountAsync(ids); ``` ## Pagination & counts `GetMarketInfosWithCountAsync` returns one page plus `Count` (the total matching the filter *before* pagination) and `Cursor`, an opaque pointer to the next page: ```csharp var page = await markets.GetMarketInfosWithCountAsync(new STXMarketInfosFilter { Status = [STXMarketInfosStatus.OPEN], Trading = STXMarketInfosTrading.TRUE, Pagination = new STXKeysetPagination { Limit = 100 }, }); Console.WriteLine($"{page.MarketInfos.Count}/{page.Count}"); ``` Pass `Cursor` back to fetch the next page. The walk ends when the server stops returning a cursor, not when a page comes back short. A last page that exactly fills `Limit` still carries a cursor, and the request after it returns no markets and no cursor. Treat an empty cursor as the end too: sent back, it reads to the server as "no cursor" and restarts you at page one. ```csharp string cursor = null; do { var page = await markets.GetMarketInfosWithCountAsync(new STXMarketInfosFilter { Pagination = new STXKeysetPagination { Cursor = cursor, Limit = 100 }, }); foreach (var m in page.MarketInfos) { /* … */ } cursor = page.Cursor; } while (!string.IsNullOrEmpty(cursor)); ``` ## Fetch every market :::caution **A single response carries a limited number of markets, so one call is not guaranteed to return everything.** Ask for no page size and the server applies its own default; ask for one larger than the server's maximum and the request is rejected outright with `Cannot request more than N records per page`. Both the default and the maximum are per-environment configuration, so neither is a number to hard-code against. `Count` always reports the true total, so **whenever `MarketInfos.Count` is smaller than `Count`, you are looking at a partial set**. Because it is the total that grows, a call that returns everything today can start coming back short later without anything about your code changing. Walking the cursor is what makes a result complete, and the helpers below do it for you. ::: The paging helpers run that loop for you. `GetAllOpenMarketInfosAsync` fetches every market currently open: ```csharp var open = await markets.GetAllOpenMarketInfosAsync(); Console.WriteLine($"{open.Count} open markets"); ``` :::caution **`MarketIds` bypasses the rest of the filter.** When it is set the server fetches exactly those markets by id and ignores status, sports, trading and paging, so `GetAllOpenMarketInfosAsync(new STXMarketInfosFilter { MarketIds = [...] })` returns those markets whatever their status, open or not. Filter the result yourself if you combine the two. ::: :::caution `OPEN` is a **lifecycle** filter, not a literal status match: it returns markets whose `Status` is `suspended` as well as `open`, and the suspended share can be a large fraction of the result. If you need only tradeable markets, filter on `Status` afterwards: ```csharp var tradeable = open.Where(m => m.Status == STXMarketStatus.open).ToList(); ``` ::: `Status` is overridden and, as in every paged call, `Limit` and `Pagination` are ignored. The rest of the filter is honoured, so the walk can be narrowed: ```csharp var openSoccer = await markets.GetAllOpenMarketInfosAsync(new STXMarketInfosFilter { Sports = ["Soccer"], }); ``` Use `GetAllMarketInfosAsync` to walk any filter without forcing a status, and `GetMarketInfoPagesAsync` to stream page by page instead of buffering the whole result set. This is worth it for broad filters, which can run to tens of thousands of markets: ```csharp await foreach (var page in markets.GetMarketInfoPagesAsync(filter, pageSize: 500)) { foreach (var m in page) { /* … */ } } ``` :::note `Limit` and `Pagination` on the filter are ignored by the paging helpers; the `pageSize` argument sets the page. Keep it within the server's maximum page size: exceeding it fails the request rather than returning a smaller page. The filter you pass is never modified. Markets are created and change status continuously, so a walk returns each market as it was when its page was read, not an instantaneous snapshot of the whole set. ::: ### Cost, and how to cut it A walk covers every matching market, so the projection you ask for decides what it costs. Asking for the complete `STXMarketInfo` across a large market set moves a great deal more data and takes correspondingly longer than asking for a handful of fields (on a mature environment the difference is about an order of magnitude in both), and it holds every fully-populated market object in memory at once. The generic overload builds its GraphQL selection from `T`, so a type carrying only the fields you actually use is the largest saving available here: ```csharp public class MarketRow { public Guid MarketId { get; set; } public string Symbol { get; set; } public string GroupingId { get; set; } public string Title { get; set; } } var rows = await markets.GetAllMarketInfosAsync(); ``` Before reaching for the full set, check whether you need it: the bulk of a mature environment is `resulted` history. If what you want is the live board, `GetAllOpenMarketInfosAsync()` returns a far smaller set for a fraction of the cost. ## Filter `STXMarketInfosFilter` combines the common filter fields: | Field | Type | | |---|---|---| | `Status` | `IEnumerable` | One or more statuses, e.g. `[STXMarketInfosStatus.OPEN]` | | `Trading` | `STXMarketInfosTrading?` | `TRUE` / `FALSE` | | `Sports` | `IEnumerable` | Sport filter (`["Basketball"]`, `["Baseball"]`, …) | | `Competitions` | `IEnumerable` | Competition filter (`["NBA"]`, `["MLB"]`, …) | | `EventIds` | `IEnumerable` | Only markets for the given events | | `MarketIds` | `IEnumerable` | Only the given markets | | `Pagination` | `STXKeysetPagination` | Cursor and page size (see above) | | `Limit` | `int?` | Caps a single response; not a substitute for paging | | `KeywordRegex` | `string` | Regex match on market keywords | | `Featured` | `STXFeatured?` | Featured markets | | `SortBy` | `STXMarketInfosSortBy` | Field and direction | | `Stat` | `STXPslStats?` | Player-stat markets | :::note These are collections, not single values: `Sports`, not `Sport`. There is no `Offset`: paging is cursor-based through `Pagination`. ::: ## Sports & competitions ```csharp var catalog = await markets.GetSportAndCompetitionsAsync(); foreach (var sport in catalog) { Console.WriteLine($"{sport.Sport}"); foreach (var c in sport.Competitions) Console.WriteLine($" {c}"); } ``` ## Events `STXEventService` bundles an event with its markets: one call instead of one-markets-per-event: ```csharp var events = serviceProvider.GetRequiredService(); var resp = await events.GetEventInfosAsync(new STXEventInfosFilter { Sport = "basketball", EventStatusFilter = new[] { STXEventStatus.scheduled, STXEventStatus.live, }, }); foreach (var ev in resp.EventInfos) { Console.WriteLine($"{ev.EventId} {ev.Title} ({ev.MarketInfos.Count} markets)"); } ``` ## Real-time updates For live price ticks on open markets, subscribe via the `STXMarketChannel` websocket wrapper (see [WebSockets → Market updates](/sdks/csharp/websockets#market-data)). REST polling is fine for snapshots; channels are required for latency-sensitive use cases. ## See also - [Reference → Market data](/sdks/csharp/reference/market-data/): full method signatures - `STX.Sdk.Enums`: all filter values, listed by your IDE's completion --- # Quickstart > Install STX.Sdk, authenticate, and make your first call in under five minutes. Source: https://docs.stxapp.io/sdks/csharp/quickstart/ Shortest path from zero to your first authenticated call against an STX demo environment. :::note Use a **demo** environment for anything you're building. `OntarioProduction` trades with real balances, so exercise your code path against `OntarioDemo` first. ::: ## Prerequisites - .NET 8 SDK or later (`dotnet --version`) - An API key created under **Account → API Keys**, which gives you a key ID and an Ed25519 private key. Save the key to a file, for example `~/.stx/ontario.pem` ## Walkthrough #### Create a project and install the SDK ```bash dotnet new console -n StxQuickstart cd StxQuickstart dotnet add package STX.Sdk dotnet add package Microsoft.Extensions.Hosting ``` Verify the install: ```bash dotnet list package | grep STX.Sdk ``` #### Register STX services Replace `Program.cs` with: ```csharp Program.cs using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using STX.Sdk; using STX.Sdk.Auth; using STX.Sdk.Services; using STX.Sdk.Settings; var host = Host.CreateDefaultBuilder(args) .ConfigureServices(services => { services.ConfigureSTXServices( STXEnvironment.OntarioDemo, STXApiKeyCredentials.FromPemFile( Environment.GetEnvironmentVariable("STX_KEY_ID")!, Environment.GetEnvironmentVariable("STX_KEY_PATH")!)); }) .Build(); var identity = host.Services.GetRequiredService(); var market = host.Services.GetRequiredService(); // No login call. One call to learn who the key belongs to, which channels need later. var me = await identity.GetMeAsync(); Console.WriteLine($"Authenticated as {me.UserId} (scope {me.Scope})"); var resp = await market.GetMarketInfosWithCountAsync(); Console.WriteLine($"Got {resp.MarketInfos.Count} of {resp.Count} markets"); foreach (var m in resp.MarketInfos.Take(5)) { Console.WriteLine($" {m.MarketId} {m.Status,-10} {m.Title}"); } ``` #### Point at your key and run ```bash macOS / Linux export STX_KEY_ID="your-key-id" export STX_KEY_PATH="$HOME/.stx/ontario.pem" dotnet run ``` ```powershell Windows (PowerShell) $env:STX_KEY_ID = "your-key-id" $env:STX_KEY_PATH = "$HOME\.stx\ontario.pem" dotnet run ``` You should see something like: ``` Authenticated as 4f2c... (scope read_write) Got 50 of 217 markets mkt_8f3... open Who wins Game 4 of the NBA Finals? mkt_a12... open LAL @ BOS moneyline mkt_c94... open Total points over/under 214.5 mkt_d70... open First team to 20 points mkt_e55... open Race to 10 rebounds ``` The first number is the page you were served, the second is the total matching the filter. If the signature is rejected, check the host clock: the server allows 30 seconds of skew. If you see a `HttpRequestException`, the host isn't reachable from your network. If placing an order fails but queries work, your key is probably `read_only`. #### Place your first order After the market fetch, pick a market and place a tiny limit order: ```csharp using STX.Sdk.Enums; var orderService = host.Services.GetRequiredService(); var targetMarket = resp.MarketInfos.First(m => m.Status == STXMarketStatus.open); var order = await orderService.ConfirmOrderAsync( price: 10, // cents quantity: 1, marketId: targetMarket.MarketId, action: STXOrderAction.buy, orderType: STXOrderType.limit, cancelOnDisconnect: false); // true requires the active orders channel joined Console.WriteLine($"Placed order {order.Id} at {order.Price}c × {order.Quantity}"); await orderService.CancelOrderAsync(order.Id); Console.WriteLine($"Cancelled {order.Id}"); ``` Run again. The program should print the placed + cancelled order IDs. ## Next steps - [Authentication](/sdks/csharp/authentication/): API keys, signing, scopes. - [Configuration](/sdks/csharp/configuration/): Per-environment endpoints, DI tips. - [Trading](/sdks/csharp/trading/): Cancel-all, batch orders, cancel-on-disconnect. - [WebSockets](/sdks/csharp/websockets/): Subscribe to portfolio, orders, trades, market updates. --- # Reference > Full method signatures, grouped by concern. Source: https://docs.stxapp.io/sdks/csharp/reference/ Every public service `STX.Sdk` registers, with method signatures and typical arguments. Use the category pages for task-oriented browsing; the canonical XML doc comments and full type definitions ship inside the [NuGet package](https://www.nuget.org/packages/STX.Sdk), so your IDE shows them on hover and on go-to-definition. - [Market data](/sdks/csharp/reference/market-data/): STXMarketService, STXEventService: list, filter, and fetch markets + events. - [Trading](/sdks/csharp/reference/trading/): STXOrderService, STXTradeService: place/cancel orders, query fills. - [Account](/sdks/csharp/reference/account/): STXIdentityService, STXSettlementService, STXTermsAndConditionsService. - [Auth (legacy)](/sdks/csharp/reference/auth-2fa/): STXLoginService, STXTokenService. Legacy email/password path. ## All services at a glance | Service | Purpose | Registered as | |---|---|---| | `STXLoginService` | `LoginAsync`. Legacy email/password path | Transient | | `STXTokenService` | `LoginAsync`, `RefreshTokenAsync`. Legacy email/password path | Transient | | `STXSessionBackgroundService` | Auto-refresh JWT in background. Not used under API-key auth, which has no token to refresh | Singleton (hosted) | | `STXIdentityService` | `GetMeAsync`: identity behind the current credentials | Transient | | `STXTermsAndConditionsService` | Get / check / accept T&Cs | Transient | | `STXMarketService` | Market catalog + pagination | Transient | | `STXEventService` | Events with attached markets | Transient | | `STXOrderService` | Place, cancel, query orders | Transient | | `STXTradeService` | Fill history | Transient | | `STXSettlementService` | Settlement history | Transient | | `STXGeoLocationService` | Current geo token | Transient | | `STXGeoLocationBackgroundService` | Keep geo token fresh | Singleton (hosted) | | `STXGeoFencingLicenseService` | GeoComply license | Transient | ## All channels at a glance | Channel | Topic | Payload | |---|---|---| | `STXPortfolioChannel` | `portfolio:{user_id}` | Balance, escrow, P&L summary | | `STXActiveOrdersChannel` | `active_orders:{user_id}` | Order state updates | | `STXActiveTradesChannel` | `active_trades:{user_id}` | Fills | | `STXActiveSettlementsChannelWrapper` | `active_settlements:{user_id}` | Settlements | | `STXPositionsChannel` | `active_positions:{user_id}` | Position changes | | `STXUserInfoChannel` | `user_info:{user_id}` | Profile/account changes | | `STXMarketChannel` | `markets`, `market_info`, `market_updates` | Prices, market state | --- # Account > Identity, settlements, and terms-and-conditions services. Source: https://docs.stxapp.io/sdks/csharp/reference/account/ ## `STXIdentityService` ```csharp public class STXIdentityService { Task GetMeAsync(); } ``` Returns the identity behind the current credentials. Call it once after startup when authenticating with an API key: there is no login response to read the user id from, and the WebSocket channels need it to build their topic. ### `STXIdentity` | | | |---|---| | `UserId`, `AccountId` | Identifiers. `UserId` is what channel topics are keyed on | | `FirstName`, `LastName` | Display name | | `KeyId` | The API key that authenticated the call, when using API-key auth | | `Method` | How the caller authenticated | | `Scope` | The key's scope, `read_only` or `read_write` | :::note **Migrating from 1.5.x.** `STXProfileService` and `STXUserProfile` were removed. Use `STXIdentityService` and `STXIdentity`, which return the identifiers callers actually needed and work under both authentication methods. If you picked up 1.6.0 before it was announced, these were briefly called `STXViewerService` and `STXViewer`. They were renamed in 1.6.1 to match the naming used elsewhere in the API. `GetMeAsync()` is unchanged. ::: ## `STXSettlementService` ```csharp public class STXSettlementService { Task GetMySettlementsAsync( IEnumerable settlementTypes, STXRollingWindowIncrement rollingWindowIncrement = STXRollingWindowIncrement.DAYS, int rollingWindowValue = 1, int page = 1, int pageSize = 100); } ``` `settlementTypes` is required. The rolling window bounds how far back to look: `rollingWindowIncrement` and `rollingWindowValue` together mean "the last N days", "the last N weeks", and so on. ### Enums | Enum | Values | |---|---| | `STXSettlementType` | `CLOSED_LONG`, `CLOSED_SHORT`, `EXPIRED_LONG`, `EXPIRED_SHORT` | | `STXTradeSettlementType` | `SETTLED`, `UNSETTLED` | | `STXRollingWindowIncrement` | `SECONDS`, `MINUTES`, `HOURS`, `DAYS`, `MONTHS` | A settlement describes how a position closed, not whether it won: `CLOSED_*` means the position was closed by an opposing trade, `EXPIRED_*` that it ran to market expiry. ## `STXTermsAndConditionsService` ```csharp public class STXTermsAndConditionsService { // NOT read-only. If the current terms are in effect, accepts them on the // user's behalf and returns the result of that acceptance. Task CheckTermsAndConditionsAsync(); // Accept a version. Omit it to accept the one currently in effect. Task AcceptTermsAndConditionsAsync(string version = null); // Read-only. Fetches the current terms without accepting anything. Task GetTermsAndConditionsAsync(); } ``` :::caution `CheckTermsAndConditionsAsync` accepts the current terms on the user's behalf when they are in effect. It is not a read-only check. To gate acceptance on user consent, call `GetTermsAndConditionsAsync`, display the terms, and call `AcceptTermsAndConditionsAsync` only after the user agrees. ::: Task-oriented examples: [Authentication](/sdks/csharp/authentication/). --- # Auth (legacy) > Email and password services, kept for integrations that already use them. Source: https://docs.stxapp.io/sdks/csharp/reference/auth-2fa/ :::note This page covers the **legacy** email and password path. It is supported for integrations that already use it, and is not recommended for new work. New integrations should use [API-key authentication](/sdks/csharp/authentication/), which has no login call, no token to expire, and no refresh cycle. ::: ## `STXLoginService` ```csharp public class STXLoginService { Task LoginAsync( string email, string password, bool checkTermsAndConditions = false, bool keepSessionAlive = false, string deviceId = "C#SDK"); } ``` ## `STXTokenService` ```csharp public class STXTokenService { Task LoginAsync( string email, string password, bool keepSessionAlive, string deviceId = "C#SDK"); Task RefreshTokenAsync(); STXTokens Tokens { get; } } ``` ### `STXUserDataCollection` | | | |---|---| | `Token` | Bearer token attached to subsequent GraphQL calls | | `RefreshToken` | Used by `RefreshTokenAsync` | | `UserId`, `UserUid`, `SessionId` | Identifiers. `UserId` is what channel topics are keyed on | | `CurrentLoginAt` | Login timestamp | | `PromptTncAcceptance` | `true` if the current terms still need accepting | | `DeviceId`, `AllowMultipleLogins` | Session context | The token is cached in `STXUserStorage` and attached automatically. You never pass it by hand. ## Keeping the session alive `keepSessionAlive: true` lets `STXSessionBackgroundService` refresh the token before it expires. It requires a real host, because background services only run under one: ```csharp var host = Host.CreateDefaultBuilder(args) .ConfigureServices(services => services.ConfigureSTXServices(STXEnvironment.OntarioDemo)) .Build(); ``` The credentials stay in memory in `STXUserStorage` so the background service can re-login if the refresh token itself expires. A failed refresh is reported through `SetSessionMessageAction` rather than thrown, so it cannot stop your host. ## Two-factor authentication Not available through this SDK. The SDK has no method for completing a 2FA challenge, so an account with 2FA enabled cannot finish logging in through it. API keys are the supported path for programmatic access and are unaffected by 2FA. ## Migrating See [Existing integrations](/sdks/csharp/authentication/) for the steps. In short: swap the registration to the API-key overload, replace the user id from the login response with one `GetMeAsync()` call, and drop the `keepSessionAlive` handling. --- # Market data > STXMarketService + STXEventService method signatures. Source: https://docs.stxapp.io/sdks/csharp/reference/market-data/ ## `STXMarketService` ```csharp public class STXMarketService { // Full market info Task> GetMarketInfosWithCountAsync(STXMarketInfosFilter filter = null); // Generic variant: specify your own response type Task> GetMarketInfosWithCountAsync(STXMarketInfosFilter filter = null); // Short market info (lightweight: id, status, title only) Task> GetShortMarketInfosWithCountAsync(IEnumerable marketIds); Task> GetShortMarketInfosWithCountWithLimitAsync(IEnumerable marketIds); // Deprecated. Same data without the total count; kept for existing callers. [Obsolete] Task> GetMarketInfosAsync(STXMarketInfosFilter filter = null); [Obsolete] Task> GetMarketInfosAsync(STXMarketInfosFilter filter = null); [Obsolete] Task> GetShortMarketInfosAsync(IEnumerable marketIds); [Obsolete] Task> GetShortMarketInfosWithLimitAsync(IEnumerable marketIds); // Walk every page (see "Market data" guide) Task> GetAllMarketInfosAsync(STXMarketInfosFilter filter = null, int pageSize = 500, CancellationToken cancellationToken = default); Task> GetAllMarketInfosAsync(STXMarketInfosFilter filter = null, int pageSize = 500, CancellationToken cancellationToken = default); Task> GetAllOpenMarketInfosAsync(STXMarketInfosFilter filter = null, int pageSize = 500, CancellationToken cancellationToken = default); Task> GetAllOpenMarketInfosAsync(STXMarketInfosFilter filter = null, int pageSize = 500, CancellationToken cancellationToken = default); // Stream page by page instead of buffering the whole result set IAsyncEnumerable> GetMarketInfoPagesAsync(STXMarketInfosFilter filter = null, int pageSize = 500, CancellationToken cancellationToken = default); IAsyncEnumerable> GetOpenMarketInfoPagesAsync(STXMarketInfosFilter filter = null, int pageSize = 500, CancellationToken cancellationToken = default); // Sports / competitions catalog Task> GetSportAndCompetitionsAsync(); } ``` The paging helpers ignore `Limit` and `Pagination` on the filter and never modify it. `pageSize` must stay within the server's maximum page size (exceeding it fails the request rather than returning a smaller page), and `MarketIds`, when set, makes the server ignore status, sports, trading and paging. They stop when the server stops returning a cursor (null or empty), throw `STXBadArgumentException` on a non-positive `pageSize`, and `STXPaginationStalledException` if the server ever repeats a cursor. `cancellationToken` is observed between pages. ### `STXMarketInfosFilter` ```csharp public class STXMarketInfosFilter { public IEnumerable Status { get; set; } public STXMarketInfosTrading? Trading { get; set; } public IEnumerable Sports { get; set; } public IEnumerable Competitions { get; set; } public IEnumerable EventIds { get; set; } public IEnumerable MarketIds { get; set; } public STXKeysetPagination Pagination { get; set; } // Cursor + Limit public int? Limit { get; set; } // caps one response public string KeywordRegex { get; set; } public STXFeatured? Featured { get; set; } public STXMarketFilter? FilterBy { get; set; } public STXMarketInfoFilters Filters { get; set; } public STXMarketFilterInput MarketFilter { get; set; } public STXPoweredByInput PoweredBy { get; set; } public STXMarketInfosSortBy SortBy { get; set; } public STXPslStats? Stat { get; set; } } ``` Enums for `Status` and `Trading` live in `STX.Sdk.Enums`. ### `STXMarketInfo` Key fields: | | | |---|---| | `MarketId` | Unique ID | | `EventId` | Parent event | | `Status` | `open` / `closed` / `settled` / … | | `Sport`, `Competition`, `Title` | Descriptive | | `Rules` | Market type: `PLAYER_STAT_LINE`, `SPREAD`, `OVER_UNDER`, … | | `Specifier` | Line value (e.g. `7.5`, or `"Player Name\|id\|STAT\|value"`) | | `MaxPrice` | Settlement max (cents) | | `Participants` | Teams/players involved | | `GroupingId` | Identity of the set of mutually exclusive outcomes this market prices against. Opaque: compare for equality, never parse. Stable for the life of the market and present in every status, so it is the field to map market data on; unlike `Symbol` it does not move when a fixture is rescheduled. | | `GroupingName` | The grouping in words (`"AL East Division"`, `"Spread - 1st Quarter"`). Descriptive, not stable: display it, join on `GroupingId` | ## `STXEventService` ```csharp public class STXEventService { Task GetEventInfosAsync(STXEventInfosFilter filter = null); } ``` ### `STXEventInfosFilter` ```csharp public class STXEventInfosFilter { public string Sport { get; set; } public string Competition { get; set; } public STXEventStatus[] EventStatusFilter { get; set; } public DateTime? EventStartFrom { get; set; } public DateTime? EventStartTo { get; set; } public int? Limit { get; set; } } ``` ### `STXEventInfo` Bundles the event plus its markets: ```csharp public class STXEventInfo { public string EventId { get; set; } public string Title { get; set; } public string Sport { get; set; } public string Competition { get; set; } public STXEventStatus Status { get; set; } public DateTime EventStart { get; set; } public List MarketInfos { get; set; } } ``` See [Markets](/sdks/csharp/markets/) for task-oriented examples. --- # Trading > STXOrderService + STXTradeService + relevant channels. Source: https://docs.stxapp.io/sdks/csharp/reference/trading/ ## `STXOrderService` ```csharp public class STXOrderService { // Place a single order Task ConfirmOrderAsync( int price, int quantity, string marketId, STXOrderAction action, STXOrderType orderType, string clientOrderId = null, bool cancelOnDisconnect = true); // Place many orders Task> ConfirmOrdersAsync( IEnumerable confirmOrderParams); // Cancel Task CancelOrderAsync(string orderId); Task> CancelOrdersAsync(IEnumerable orderIds); Task> CancelAllOrdersAsync(); // History Task GetMyOrdersAsync( IEnumerable statusFilter = null, IEnumerable marketIds = null, int? limit = null, int? offset = null, STXOrdersSortByField? sortBy = null, STXSortOrder? sortOrder = null); } ``` ### `STXConfirmOrderParams` For the batch variant: ```csharp public class STXConfirmOrderParams { public int Price { get; set; } public int Quantity { get; set; } public string MarketId { get; set; } public STXOrderAction Action { get; set; } public STXOrderType OrderType { get; set; } public string ClientOrderId { get; set; } public bool CancelOnDisconnect { get; set; } = true; public STXOrderExpiration? Expiration { get; set; } public long? ExpirationTime { get; set; } // UNIX microseconds for GOOD_TILL_TIME } ``` ### Enums | Enum | Values | |---|---| | `STXOrderAction` | `buy`, `sell` | | `STXOrderType` | `limit`, `market` | | `STXOrderStatus` | `created`, `requested`, `accepted`, `open`, `filled`, `cancelled`, `rejected`, `partially_cancelled`, `delayed` | | `STXOrderExpiration` | `GOOD_TILL_START`, `GOOD_TILL_TIME` | | `STXOrdersSortByField` | `insertedAt`, `price`, `quantity`, `marketId`, … | | `STXSortOrder` | `asc`, `desc` | ## `STXTradeService` ```csharp public class STXTradeService { Task GetMyTradesAsync( IEnumerable marketIds = null, int? limit = null, int? offset = null, STXTradesSortByField? sortBy = null, STXSortOrder? sortOrder = null); Task> GetMyTradesForOrderAsync(string orderId); } ``` ## Channels used by trading workflows | Channel | Events | |---|---| | `STXActiveOrdersChannel` | `SetOnReceiveAction` fires on every order state transition. `IsChannelConnectedAndUseCancelOnDisconnect` reports whether `ConfirmOrderAsync(cancelOnDisconnect: true)` is allowed. | | `STXActiveTradesChannel` | `OnTrade`: fires on every new fill | ```csharp _activeOrders.SetOnReceiveAction(update => { foreach (var order in update.Orders) { // order.Id, order.Status, order.Filled, order.AvgPrice, ... } }); await _activeOrders.StartAsync(); ``` ## Errors worth catching - `STXCancelOnDisconnectNotEnabledException`: thrown by `ConfirmOrderAsync` / `ConfirmOrdersAsync` if `cancelOnDisconnect: true` and the active orders channel isn't joined. - `STXTokenExpiredException`: refresh the token, then retry. Full list: [Errors & retries](/sdks/csharp/errors-and-retries/). Task-oriented examples: [Trading](/sdks/csharp/trading/). --- # Settlements > Query settlement history for your account. Source: https://docs.stxapp.io/sdks/csharp/settlements/ A settlement is the final payout when a position closes. Query them with `STXSettlementService`. ## Fetch settlement history ```csharp using STX.Sdk.Enums; using STX.Sdk.Services; var settlements = serviceProvider.GetRequiredService(); var history = await settlements.GetMySettlementsAsync( settlementTypes: new[] { STXSettlementType.CLOSED_LONG, STXSettlementType.CLOSED_SHORT }, rollingWindowIncrement: STXRollingWindowIncrement.DAYS, rollingWindowValue: 7, page: 1, pageSize: 50); Console.WriteLine($"{history.Settlements.Count} of {history.TotalCount} settlements"); foreach (var s in history.Settlements) { Console.WriteLine($"{s.Id} {s.MarketId} {s.Type} P&L: {s.RealizedPnl}c"); } ``` `settlementTypes` is required. The rolling window bounds how far back to look, so the call above means "the last 7 days". ### Settlement types | | | |---|---| | `CLOSED_LONG` | A long position closed by an opposing trade | | `CLOSED_SHORT` | A short position closed by an opposing trade | | `EXPIRED_LONG` | A long position held to market expiry | | `EXPIRED_SHORT` | A short position held to market expiry | The type describes **how** the position closed, not whether it made money. Use `RealizedPnl` for that. ### `STXSettlement` | | | |---|---| | `Id`, `MarketId`, `AccountId` | Identifiers | | `Type` | One of the four above | | `RealizedPnl`, `GrossPnl`, `Fee` | Amounts in cents | | `Quantity` | Contracts settled | | `OpeningPrice`, `ClosingPrice` | Prices in cents | | `OpeningTradeId`, `ClosingTradeId` | The trades that opened and closed the position | | `InsertedAt`, `InsertedAtIso` | Unix time, and the same value as a `DateTime` | ## Real-time Subscribe for settlements as they land instead of polling: ```csharp var channel = serviceProvider.GetRequiredService(); channel.SetOnReceiveAction(s => _logger.LogInformation("Settled {MarketId}: {Type} {Pnl}c", s.MarketId, s.Type, s.RealizedPnl)); await channel.StartAsync(); ``` The channel topic is keyed on the user id, so under API-key authentication call `STXIdentityService.GetMeAsync()` once at startup before connecting. See [WebSockets](/sdks/csharp/websockets/). `STXActiveSettlementsChannelWrapper` wraps the same channel with a bounded queue if you would rather drain settlements yourself than handle a callback. ## See also - [Trading](/sdks/csharp/trading/): the fills that lead up to a settlement - [WebSockets](/sdks/csharp/websockets/): live settlement stream - [Account reference](/sdks/csharp/reference/account/): full signatures --- # Trading > Place and cancel orders, inspect trade history. Source: https://docs.stxapp.io/sdks/csharp/trading/ Orders and trades live on two services: - **`STXOrderService`**: place, cancel, and query orders - **`STXTradeService`**: query fills (trades) ## Place an order ```csharp using STX.Sdk.Enums; using STX.Sdk.Services; var orders = serviceProvider.GetRequiredService(); var order = await orders.ConfirmOrderAsync( price: 10, // cents quantity: 1, marketId: "mkt_abc", action: STXOrderAction.buy, orderType: STXOrderType.limit, clientOrderId: "my-ref-1", // optional idempotency key cancelOnDisconnect: true); Console.WriteLine($"Order {order.Id} placed at {order.Price}c x {order.Quantity}"); ``` ### Parameters | | | |---|---| | `price` | **Integer cents.** `50` = 50¢. Ignored for `market` orders. The REST and WebSocket surfaces write this same price as the dollar string `"0.50"`. | | `quantity` | Number of contracts. | | `marketId` | From `STXMarketService.GetMarketInfosWithCountAsync`. | | `action` | `STXOrderAction.buy` / `STXOrderAction.sell` | | `orderType` | `STXOrderType.limit` / `STXOrderType.market` | | `clientOrderId` | Your idempotency key. If the call retries you won't get a double-fill. | | `cancelOnDisconnect` | If `true`, the exchange cancels this order when the websocket drops. **Requires `STXActiveOrdersChannel` to be joined** (see below). | ### Cancel-on-disconnect requires the active orders channel If you pass `cancelOnDisconnect: true` but haven't joined `STXActiveOrdersChannel`, the call throws `STXCancelOnDisconnectNotEnabledException`. For the market-maker workflow, always join the channel first (it costs nothing): ```csharp var activeOrders = serviceProvider.GetRequiredService(); await activeOrders.StartAsync(); ``` If you only want a simple place-and-leave-it order, pass `cancelOnDisconnect: false`. ## Batch orders Placing many orders at once is one round-trip instead of N: ```csharp var batch = new[] { new STXConfirmOrderParams { Price = 10, Quantity = 1, MarketId = "mkt_a", Action = STXOrderAction.buy, OrderType = STXOrderType.limit }, new STXConfirmOrderParams { Price = 85, Quantity = 2, MarketId = "mkt_b", Action = STXOrderAction.sell, OrderType = STXOrderType.limit }, }; var placed = await orders.ConfirmOrdersAsync(batch); ``` ## Cancel orders ```csharp await orders.CancelOrderAsync(orderId); // one await orders.CancelOrdersAsync(new[] { orderId1, orderId2 }); // several await orders.CancelAllOrdersAsync(); // everything open on this account ``` `CancelAllOrdersAsync` returns the list of every cancelled order, which is handy to reconcile against your own state. ## Order history ```csharp var history = await orders.GetMyOrdersAsync( statusFilter: new[] { STXOrderStatus.filled, STXOrderStatus.cancelled }, limit: 50, sortBy: STXOrdersSortByField.insertedAt, sortOrder: STXSortOrder.desc); foreach (var o in history.Orders) { Console.WriteLine($"{o.Id,-10} {o.Status,-10} {o.MarketId,-12} {o.Price}c x {o.Quantity}"); } ``` ## Trades (fills) Each fill is a trade: ```csharp var trades = serviceProvider.GetRequiredService(); // Everything var hist = await trades.GetMyTradesAsync( limit: 50, sortBy: STXTradesSortByField.time, sortOrder: STXSortOrder.desc); // Fills for a specific order var orderFills = await trades.GetMyTradesForOrderAsync(orderId); ``` ## Real-time: fills + order status Polling the REST endpoints works but adds latency. The websocket channels push updates as they happen: - **`STXActiveOrdersChannel`**: every state change on an open order (`open` → `partially_filled` → `filled` / `cancelled`) - **`STXActiveTradesChannel`**: every new trade (fill) See [WebSockets → Orders & trades](/sdks/csharp/websockets#orders-and-trades). ## A canonical market-maker loop Pulled straight from the [example console app](https://github.com/stxapp/stx-csharp-demo)'s `STXWorker.cs`: ```csharp // 1. Learn who the key belongs to, then connect channels. // Channel topics are keyed on the user id, and with an API key there is no // login response to read it from. await _identity.GetMeAsync(); await _ordersChannel.StartAsync(); await _tradesChannel.StartAsync(); await _portfolioChannel.StartAsync(); // 2. Subscribe to market updates _marketChannel.SetOnReceiveAction(OnPriceTick); await _marketChannel.StartAsync(); // 3. On each tick, quote void OnPriceTick(STXMarketInfoChannelData tick) { // Cancel stale orders and place fresh ones await _orderService.CancelAllOrdersAsync(); await _orderService.ConfirmOrdersAsync(BuildQuotes(tick)); } ``` Cancel-on-disconnect, automatic session refresh, and portfolio-balance tracking are all handled by registered services; the loop above is the whole app. ## See also - [Reference → Trading](/sdks/csharp/reference/trading/): full method signatures - [WebSockets](/sdks/csharp/websockets/): real-time order/trade/balance streams - [Errors & retries](/sdks/csharp/errors-and-retries/): common order errors --- # WebSockets > Phoenix channels for real-time orders, trades, balance, and market updates. Source: https://docs.stxapp.io/sdks/csharp/websockets/ The exchange pushes state changes over Phoenix channels. `STX.Sdk` wraps each topic in a strongly-typed class. Every channel handles: - Authenticating the connection, by token or by signing the handshake - Heartbeat and automatic reconnection - Rejoining its topic after a reconnect :::caution User-scoped channel topics are keyed on your user id. Under API-key authentication there is no login response to read it from, so call `STXIdentityService.GetMeAsync()` once at startup **before** joining any channel below. Skipping it leaves the topic pointing at an empty id and the channel never receives anything. ::: ## Available channels | Channel | Topic | What it pushes | |---|---|---| | `STXPortfolioChannel` | `portfolio:{user_id}` | Available balance, escrow, liabilities | | `STXActiveOrdersChannel` | `active_orders:{user_id}` | Order state transitions | | `STXActiveTradesChannel` | `active_trades:{user_id}` | Fills as they land | | `STXActiveSettlementsChannel` | `active_settlements:{user_id}` | Settlements | | `STXPositionsChannel` | `active_positions:{user_id}` | Position changes | | `STXUserInfoChannel` | `user_info:{user_id}` | Account updates | | `STXMarketChannel` | `market_info` | Market state and prices, broadcast rather than user-scoped | ## Connect a channel Register a callback, then start it: ```csharp var portfolio = serviceProvider.GetRequiredService(); portfolio.SetOnReceiveAction(p => Console.WriteLine($"Available: {p.AvailableBalance}c Escrow: {p.Escrow}c")); await portfolio.StartAsync(); ``` `StartAsync` opens the socket, joins the topic, and begins the heartbeat. `StopAsync` closes it. `WebSocketConnected` reports the current state, and the `SocketDisconnected` and `SocketReconnected` events fire around an automatic reconnect. ## Two ways to consume a channel **Composition**: hand the channel a callback, as above. Best when you want each message as it lands. **Inheritance**: subclass and override `OnReceive`: ```csharp public class BalanceTracker : STXPortfolioChannel { public BalanceTracker(STXUserStorage storage, STXEndpointSettings settings) : base(storage, settings) { } public override void OnReceive(STXPortfolio portfolio) { AvailableBalance = portfolio.AvailableBalance; } public long AvailableBalance { get; private set; } } ``` **Wrappers**: every channel also has a `…ChannelWrapper` registered alongside it, which buffers into a bounded queue instead of calling you back. Use it when you would rather poll than handle a callback: ```csharp var wrapper = serviceProvider.GetRequiredService(); await wrapper.StartAsync(); var latest = wrapper.LastItem; // most recent message, or null var all = wrapper.Items; // buffered messages ``` ## Orders and trades ```csharp var orders = serviceProvider.GetRequiredService(); var trades = serviceProvider.GetRequiredService(); orders.SetOnReceiveAction(o => { foreach (var order in o.Orders) Console.WriteLine($"{order.Id} {order.Status} {order.Filled}/{order.Quantity}"); }); trades.SetOnReceiveAction(t => { foreach (var trade in t.Trades) Console.WriteLine($"Filled {trade.MarketId} at {trade.Price}c"); }); await orders.StartAsync(); await trades.StartAsync(); ``` :::note `cancelOnDisconnect: true` on an order requires `STXActiveOrdersChannel` to be joined first. The channel exposes `IsChannelConnectedAndUseCancelOnDisconnect` so you can check before placing one. Placing such an order without the channel throws `STXCancelOnDisconnectNotEnabledException`. ::: ## Market data Market info is a broadcast, so it is not keyed on a user and works without `GetMeAsync()`: ```csharp var market = serviceProvider.GetRequiredService(); market.SetOnReceiveAction(m => Console.WriteLine($"{m.MarketId} {m.Status}")); await market.StartAsync(); ``` ## In a hosted service ```csharp public class STXWorker : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stop) { // Channel topics are keyed on the user id. Under API-key auth there is no // login response, so fetch it once before joining anything. await _identity.GetMeAsync(); _portfolio.SetOnReceiveAction(p => _balance = p.AvailableBalance); // long, in cents _orders.SetOnReceiveAction(HandleOrders); await _portfolio.StartAsync(); await _orders.StartAsync(); await Task.Delay(Timeout.Infinite, stop); } public override async Task StopAsync(CancellationToken stop) { await _orders.StopAsync(); await _portfolio.StopAsync(); await base.StopAsync(stop); } } ``` Channels are registered as singletons, so the same instance is shared across your app. Resolve them once and keep them. ## See also - [Trading](/sdks/csharp/trading/): placing the orders these channels report on - [Settlements](/sdks/csharp/settlements/): settlement history and its channel --- # Python SDK > The Python SDK for the STX exchange. Source: https://docs.stxapp.io/sdks/python/ `stx-python` is the Python SDK for the STX exchange. Read markets, place and cancel orders, follow your account, and stream the [WebSocket channels](/websockets/), with every request signed by your API key and every response a typed model. Response fields are described in the [API reference](/api/rest/). ```bash pip install stx-python ``` ```python from stx import STX # Reads your API key and exchange from STX_* environment variables or ~/.stx/credentials. with STX() as client: me = client.me() page = client.markets(status="open", limit=5) print(me.user_id, [m.symbol for m in page]) ``` [Authentication](/sdks/python/authentication/) shows how to give the client your API key, and [Environments](/environments/) lists the exchanges and where to get a key. There are three clients: - **`STX`**: blocking, one method per API call. - **`AsyncSTX`**: the same methods for `asyncio`. See [Async](/sdks/python/async/). - **`STXWebSocket`**: the channels you join, over one signed socket that keeps itself alive and reconnects. See [WebSockets](/sdks/python/websockets/). ## Prices and quantities are strings Amounts come back as decimal strings, exactly as the API sends them: dollars with at least four decimals (`order.price == "0.5600"`) and contract counts with at least two (`"2.00"`). Orders take strings too: `place_order(market_id, "buy", "limit", price="0.56", quantity="2")`. Passing a float raises `TypeError` before anything is sent. Use `decimal.Decimal` for arithmetic: ```python from decimal import Decimal from stx import STX with STX() as client: for market in client.markets(status="open", limit=3): spread = None if market.bids and market.offers: spread = Decimal(market.offers[0].price) - Decimal(market.bids[0].price) print(market.symbol, "pays", market.max_price, "spread", spread) ``` Prices are the same on every channel: the SDK converts the channels that send cents on the wire to dollar strings. Values that are not money stay numbers: percentages, counts, loyalty points, fee factors and timestamps. ## Where to go next - [Installation](/sdks/python/installation/): Install and import. - [Quickstart](/sdks/python/quickstart/): Add a key and make your first calls. - [Authentication](/sdks/python/authentication/): Pass your API key to the client. - [Environments](/sdks/python/environments/): Pick the exchange to connect to. - [Markets](/sdks/python/markets/): Markets, events and pagination. - [Trading](/sdks/python/trading/): Place and cancel orders. - [Portfolio](/sdks/python/portfolio/): Balance, positions, fills and history. - [WebSockets](/sdks/python/websockets/): Stream market data and your account. - [Async](/sdks/python/async/): Concurrent requests and streams with asyncio. - [Errors & retries](/sdks/python/errors-and-retries/): Typed exceptions, timeouts and what the client retries. - [API reference](/sdks/python/reference/): Every method and what it returns. - [Example scripts](https://github.com/stxapp/stx-python-demo): List markets, place and cancel an order, stream live data. Released under the MIT license. --- # Async > AsyncSTX for concurrent requests, and client calls alongside WebSocket streams. Source: https://docs.stxapp.io/sdks/python/async/ `AsyncSTX` is the SDK's implementation; `STX` runs it on a private event loop. Use `AsyncSTX` when you keep many requests in flight or combine client calls with streams. ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: me, markets, orders, fills = await asyncio.gather( client.me(), client.markets(status="open", limit=5), client.orders(limit=5), client.fills(limit=5), ) print(me.user_id, len(markets), len(orders), len(fills)) asyncio.run(main()) ``` One `AsyncSTX` holds one HTTP connection pool; share it across tasks rather than creating one per request. Close it with `async with` or `await client.close()`. ## Iterating pages ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: count = 0 async for market in client.iter_markets(status="open", limit=200): count += 1 if count >= 300: break print("saw", count, "markets") asyncio.run(main()) ``` ## Client calls and streams together ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: ids = [m.market_id for m in await client.markets(status="open", limit=3)] async with client.websocket() as ws: books = {} def on_book(msg): books[msg.payload["market_id"]] = msg.payload # replace, never merge await ws.orderbook(ids, on_message=on_book) fills = await ws.fills() await fills.wait_snapshot() await asyncio.sleep(5) print("books held:", len(books)) asyncio.run(main()) ``` ## Sync code `STX` has every `AsyncSTX` client method without `await`, and works inside a program or notebook that already runs an event loop. `client.async_client` is the underlying `AsyncSTX`, bound to `STX`'s own loop thread. WebSockets are async only. ```python from stx import STX with STX() as client: for order in client.iter_orders(limit=50): print(order.id, order.status) break ``` --- # Authentication > Pass your API key to the client: a credentials profile, environment variables, or keyword arguments. Source: https://docs.stxapp.io/sdks/python/authentication/ The client needs your API key ID and its Ed25519 private key. Creating a key, how requests are signed, and why a signature fails are covered on [Authentication](/api/authentication/). The SDK signs every request and the WebSocket handshake for you, and signs again on every retry and reconnect. There are three ways to hand the key to the client. When more than one is set, keyword arguments win over environment variables, and environment variables win over the profile. `STX`, `AsyncSTX` and `STXWebSocket` read them the same way. ## A credentials profile Add a profile to `~/.stx/credentials`, one per exchange you use. Set `region` and `env` to the exchange the key belongs to; the values are on [Environments](/sdks/python/environments/). ```ini [default] region = us env = demo key_id = your-key-id key_file = ~/.stx/demo.pem [my-demo] region = ontario env = demo key_id = another-key-id key_file = ~/.stx/my-demo.pem ``` `STX()` with no profile reads `STX_PROFILE`, then the `[default]` section. Pick any other profile by name: ```python from stx import STX with STX(profile="my-demo") as client: print(client.me().user_id) ``` The other examples in these guides call `STX()` and `AsyncSTX()` with no arguments, so they use whichever profile or environment variables you have set. A profile can also set `host` (or `base_url`), `private_key` (instead of `key_file`) and `verify_tls`. ## Environment variables ```bash export STX_REGION=us STX_ENV=demo export STX_KEY_ID=your-key-id export STX_PRIVATE_KEY=~/.stx/demo.pem # a path, or the PEM text ``` ```python from stx import STX with STX() as client: print(client.me().user_id) ``` `STX_HOST` (or `STX_BASE_URL`), `STX_PROFILE`, `STX_CREDENTIALS` (the path of the credentials file) and `STX_VERIFY_TLS` are also read. ## Keyword arguments ```python import os from stx import STX with STX( region="us", env="demo", key_id=os.environ["MY_KEY_ID"], private_key=os.environ["MY_KEY_PEM"], # a path or the PEM text ) as client: print(client.me().user_id) ``` An explicit `None` means none: `key_id=None` ignores `STX_KEY_ID` and the profile. If the key lives in an HSM or KMS, pass `signer=` instead of `private_key`: a function that takes the message bytes and returns the raw 64-byte Ed25519 signature. The key never enters the process. ```python import os from stx import STX def sign(message: bytes) -> bytes: return my_kms.sign(key="stx-trading", message=message) # your KMS client with STX(region="us", env="demo", key_id=os.environ["MY_KEY_ID"], signer=sign) as client: print(client.me().user_id) ``` ## Check it works ```python from stx import STX with STX() as client: me = client.me() print(me.user_id, me.scope) # scope: "read_only" or "read_write" ``` A bad key, or a machine clock that has drifted, raises `STXAuthenticationException`. A `read_only` key gets `STXForbiddenException` from the order methods. --- # Environments > Select the STX exchange the client connects to. Source: https://docs.stxapp.io/sdks/python/environments/ The exchanges, their hosts and which one to start with are listed on [Environments](/environments/). In the SDK, pick one with `region` and `env` (as keyword arguments, as `STX_REGION` and `STX_ENV`, or in a credentials profile): | Exchange | `region` | `env` | |---|---|---| | STX US demo | `"us"` | `"demo"` | | STX Ontario demo | `"ontario"` | `"demo"` | | STX Ontario | `"ontario"` | `"production"` | ```python from stx import STX, Environment, Region with STX(region="us", env="demo") as client: print(client.base_url, client.socket_url) # same exchange: same = STX(region=Region.US, env=Environment.DEMO) same.close() ``` `env` also accepts `"prod"` and `"live"` for production. An API key works on one exchange only, so keep one credentials profile per exchange and switch with `profile`; see [Authentication](/sdks/python/authentication/). To reach any other host, such as a local server, pass `host="http://localhost:4000"` (or set `STX_HOST`). The URL's scheme is kept for both HTTP and the socket, so `http://` talks plain HTTP and `ws://`. `verify_tls=False` (or `STX_VERIFY_TLS=false`) turns off certificate checks; use it only for a local server. --- # Errors & retries > The typed exceptions the SDK raises, timeouts, and what it retries. Source: https://docs.stxapp.io/sdks/python/errors-and-retries/ What each endpoint can return is in the [API reference](/api/rest/), and request limits are in [Rate limits](/concepts/rate-limits/). The SDK raises one exception per HTTP status, so you can catch the case you care about. ## Exceptions `str(exc)` is the API's error message, and `exc.status_code`, `exc.body`, `exc.method` and `exc.path` describe the request. | Status | Exception | Typical cause | |---|---|---| | 400 | `STXValidationException` | A bad parameter: `status has invalid value: foo` | | 401 | `STXAuthenticationException` | Bad key, bad signature, or a clock more than 30 s off | | 403 | `STXForbiddenException` | A `read_only` key on an order route | | 404 | `STXNotFoundException` | Unknown id, or one belonging to another account | | 422 | `STXRejectedException` | The exchange refused: closed market, insufficient funds, fractional quantity | | 429 | `STXRateLimitException` | Too many requests; `retry_after` in seconds when sent | | 5xx | `STXServerException` | Server failure | All of those derive from `STXAPIException`, which derives from `STXException`. Three more: - `STXTransportException`: no answer at all (connection refused, DNS, TLS, timeout) - `STXConfigException`: the client is misconfigured (unknown region, missing key, bad PEM) - `STXChannelException`: a WebSocket join or control event was refused; `.reply` holds the reason ```python from stx import STX, STXNotFoundException, STXRejectedException, STXValidationException with STX() as client: try: client.order("00000000-0000-0000-0000-000000000000") except STXNotFoundException as exc: print("404:", exc) try: client.markets(status="not-a-status") except STXValidationException as exc: print(exc.status_code, exc) market = next( m for m in client.iter_markets(status="open", trading=True, sort_by="event_start", sort_direction="desc") if m.event_status == "scheduled" ) try: client.place_order(market.market_id, "buy", "limit", quantity="1") except (STXRejectedException, STXValidationException) as exc: print(exc.status_code, exc) ``` ## What is retried | Failure | `GET`, `DELETE` | `POST` | |---|---|---| | 429 | retried, waiting `Retry-After` | retried, waiting `Retry-After` | | 5xx | retried | **not** retried | | Connection error or timeout | retried | **not** retried | | 400, 401, 403, 404, 422 | not retried | not retried | A `POST` that failed with a server error or a dropped connection may still have placed the order, so the SDK does not send it again. Use `client_order_id` and look the order up (see [Avoid duplicate orders](/sdks/python/trading#avoid-duplicate-orders)). Every attempt is signed afresh. Backoff is exponential with jitter: 0.5 s, 1 s, 2 s, capped at 30 s, over 3 attempts by default. `timeout` is seconds per HTTP request (default 30). ```python from stx import NO_RETRY, STX, RetryPolicy policy = RetryPolicy(max_attempts=5, initial_backoff=0.25, max_backoff=10) with STX(retry=policy, timeout=10.0) as client: print(len(client.markets(limit=1))) with STX(retry=NO_RETRY) as client: print(client.me().scope) ``` ## WebSocket failures The socket reconnects on its own (see [Reconnects](/sdks/python/websockets#reconnects)). A channel that the server closes or errors is rejoined; a rejoin refused as `unauthorized` closes that channel instead of retrying forever. `ReconnectPolicy(max_attempts=...)` bounds reconnects; after the last one, `run_forever()` returns and every channel's iterator ends. ## Logging The SDK logs to the `stx` logger (and `stx.ws` for the socket): requests at DEBUG, retries and reconnects at INFO and WARNING. ```python import logging from stx import STX logging.basicConfig(level=logging.INFO) logging.getLogger("stx").setLevel(logging.DEBUG) with STX() as client: client.me() ``` --- # Installation > Install stx-python and import it. Source: https://docs.stxapp.io/sdks/python/installation/ ```bash pip install stx-python ``` ```bash poetry add stx-python ``` ```bash uv add stx-python ``` Requires Python 3.9 to 3.13. The package is on PyPI as [stx-python](https://pypi.org/project/stx-python/) and imports as `stx`. ## Importing ```python from stx import STX, AsyncSTX, STXWebSocket ``` Response models are in `stx.models`, and every exception is importable from `stx`. ## Check the install ```bash python -c "import stx; print(stx.__version__)" ``` ## Versioning Pin an exact version in production (`stx-python==0.6.0`). Before 1.0.0, a minor release can change the API. Pre-releases such as `0.7.0rc1` install only when asked for: `pip install --pre stx-python`. Uninstalling the package leaves `~/.stx/` (your credentials profile and keys) in place. --- # Markets > Markets and events: filters, sorting, pagination, and the market object. Source: https://docs.stxapp.io/sdks/python/markets/ What a market is and how it moves through its statuses is covered in [Concepts](/concepts/); every field is in the [API reference](/api/rest/). This page shows the SDK calls. ## Listing markets `markets()` calls `GET /api/v1/markets` and returns one `Page` of `stx.models.Market`: ```python from stx import STX with STX() as client: page = client.markets(status=["pre_open", "open"], trading=True, limit=10) for m in page: print(m.market_id, m.status, m.symbol, m.last_traded_price) print(len(page), "markets; more:", page.has_more) ``` | Argument | Meaning | |---|---| | `market_ids`, `event_ids` | One id or a list | | `status` | One status or a list: `scheduled`, `pre_open`, `open`, `closed`, `resulted`, `cancelled`, `voided` | | `trading` | `True` for markets accepting orders now | | `sports`, `competitions` | Exact names as the market carries them, e.g. `"Baseball"`, `"MLB"` | | `sort_by`, `sort_direction` | `"event_start"`, and `"asc"` or `"desc"` | | `limit` | Page size, default 100, maximum 200 | | `cursor` | The `cursor` of the previous page | Filters combine with AND; a list matches any of its values. ## Pagination Every list endpoint pages by cursor. `page.cursor` is `None` on the last page. Pass it back for the next one, or let an `iter_` method follow it for you: ```python from itertools import islice from stx import STX with STX() as client: first = client.markets(status="open", limit=2) if first.cursor: second = client.markets(status="open", limit=2, cursor=first.cursor) print([m.symbol for m in second]) walked = list(islice(client.iter_markets(status="open", limit=50), 120)) print("walked", len(walked), "markets across pages") ``` `iter_markets`, `iter_events`, `iter_orders`, `iter_fills`, `iter_settlements`, `iter_deposits`, `iter_withdrawals`, `iter_adjustments`, `iter_fees`, `iter_loyalty` and `iter_account_market_stats` all work the same way. On `AsyncSTX` they are async iterators (`async for`). ## One market There is no single-market route; `market(market_id)` asks `markets()` for that id and raises `STXNotFoundException` when there is none: ```python from stx import STX with STX() as client: some_id = client.markets(limit=1)[0].market_id market = client.market(some_id) print(market.title) print("rules", market.rules, "max", market.max_price, "delay", market.in_play_delay_sec) ``` ## The market object Selected fields of `stx.models.Market`; every field is on the [API reference](/api/rest/). | Field | Type | Notes | |---|---|---| | `market_id`, `event_id` | `str` | UUIDs | | `symbol`, `title`, `short_title`, `question` | `str` | | | `status` | `str` | See [market status](/concepts/market-status/) | | `trading` | `bool` | Accepting orders now | | `max_price` | `str` | What a winning contract pays, e.g. `"1.0000"`. Read it per market; orders price strictly below it | | `price`, `last_traded_price` | `str` or `None` | Dollars | | `bids`, `offers` | `List[BookLevel]` | Best first; each level has `price` and `quantity` strings | | `recent_trades` | `List[RecentTrade]` | `price`, `quantity`, `timestamp`, `liquidity_taker` | | `volume24h`, `total_volume`, `open_interest` | `str` or `None` | Contracts | | `event_start` | `str` | ISO 8601 | | `in_play_delay_sec` | `int` | Queue delay while the event is in progress | Fields the server adds before the SDK knows them are kept: read them as attributes or from `model_dump()`. ## Events ```python from stx import STX with STX() as client: for event in client.events(status="scheduled", sort_by="start_time", limit=5): print(event.start_time_iso, event.sport, event.title) ``` `events()` takes `event_ids`, `sports`, `competitions`, `event_types`, `title`, `status` (`scheduled`, `in_progress`, `completed`, `cancelled`), `promoted`, `sort_by="start_time"`, `sort_direction`, `limit` and `cursor`. ## Live prices `markets()` returns the book as it stood when you asked. To follow a book, subscribe to the `orderbook` channel; for price changes across many markets, `ticker`. See [WebSockets](/sdks/python/websockets/). --- # Portfolio > Balance, positions, fills, settlements, per-market stats and account history. Source: https://docs.stxapp.io/sdks/python/portfolio/ All amounts are dollar strings and all contract counts are quantity strings, as the API sends them. Every field is described in the [API reference](/api/rest/). ## Balance `balance()` calls `GET /api/v1/account/balance`: cash, what is available, liabilities, lifetime totals and the fee schedule. It is the same object the [`balances` channel](/sdks/python/websockets#balances) pushes. ```python from stx import STX with STX() as client: b = client.balance() print("available", b.available_balance, "cash", b.account_balance) print("buy liability", b.buy_order_liability, "sell liability", b.sell_order_liability) print("fees", b.fee_schedule, b.taker_factor, b.maker_factor, "tier", b.loyalty_tier) ``` `available_balance` is rounded down to the cent and the liabilities up, so they need not reconcile to the cent; treat each as authoritative. ## Positions `positions()` calls `GET /api/v1/positions` and returns your positions (`market_ids=` narrows them), the same objects the [`positions` channel](/sdks/python/websockets#positions) sends on join. ```python from stx import STX with STX() as client: for p in client.positions(): print(p.market_id, "net", p.position, "premium", p.premium, "open risk", p.open_risk) ``` `position` is positive when long and negative when short. Positions are not marked to market: value them against the order book yourself. ## Fills A fill is one of your executions. `fills()` filters on `market_ids`, `order_ids` and `status` (`created`, `open`, `settled`, `cancelled`): ```python from stx import STX with STX() as client: for f in client.fills(limit=5): print(f.trade_id, f.order_id, f.action, f.filled, "@", f.price, "fee", f.total_fee) orders = client.orders(status="filled", limit=1) if orders.items: mine = client.fills(order_ids=[orders[0].id]) print("fills for", orders[0].id, [f.filled for f in mine]) ``` `total_fee` is the all-in fee: the trade fee plus settlement fees so far. `unrounded_trade_fee` can carry up to nine decimals, so parse with `Decimal`. ## Settlements and history ```python from stx import STX with STX() as client: for s in client.settlements(limit=5): print(s.type, s.quantity, s.opening_price, "->", s.closing_price, "pnl", s.realized_pnl) for name in ("deposits", "withdrawals", "adjustments", "fees", "loyalty"): page = getattr(client, name)(limit=3) print(name, [(t.type, t.amount) for t in page]) ``` `settlements()` takes `market_ids` and `type` (`closed_short`, `closed_long`, `expired_short`, `expired_long`). Every history method pages by cursor and has an `iter_` twin. ## Per-market statistics `account_market_stats()` returns your position, exposure and P&L broken out per market (`GET /api/v1/account/market_stats`). It is unrelated to the public `market_stats` channel, which carries prices. ```python from stx import STX with STX() as client: for row in client.account_market_stats(exclude_zero_settlements=True, limit=5): print(row.market_id, "position", row.position, "net pnl", row.total_net_pnl) ``` It filters on `market_ids`, `event_ids`, `sports`, `competitions`, `from_time` and `to_time` (Unix microseconds) and `exclude_zero_settlements`. ## Keeping it current These methods return a snapshot. To stay current, join `orders`, `fills`, `positions`, `settlements` and `balances` (or `account` for all of them) on the socket, and call `orders()` and the other methods you rely on again after a reconnect. See [WebSockets](/sdks/python/websockets/). --- # Quickstart > Install stx-python, add an API key, and make your first calls. Source: https://docs.stxapp.io/sdks/python/quickstart/ From nothing to reading markets, placing an order and streaming your account. :::note Build against a **demo** exchange. Demo balances are not real money; production is. ::: ## Prerequisites - Python 3.9 or newer (`python3 --version`) - An API key for the exchange you are building against: a key ID and an Ed25519 private key. [Environments](/environments/) lists the exchanges and where to get a key. ## Walkthrough #### Install the SDK ```bash pip install stx-python ``` #### Save your key Save the private key as a file, for example `~/.stx/demo.pem`, then create `~/.stx/credentials` with a `[default]` profile. Set `region` and `env` to the exchange the key belongs to; the values are on [Environments](/sdks/python/environments/). ```ini [default] region = us env = demo key_id = your-key-id key_file = ~/.stx/demo.pem ``` Environment variables and keyword arguments work too; see [Authentication](/sdks/python/authentication/). #### Make your first call ```python hello_stx.py from stx import STX with STX() as client: me = client.me() print("user", me.user_id, "account", me.account_id, "scope", me.scope) ``` `scope` is `read_only` or `read_write`; placing orders needs `read_write`. #### Read some markets ```python markets.py from stx import STX with STX() as client: page = client.markets(status="open", limit=5) for m in page: bid = m.bids[0].price if m.bids else "-" offer = m.offers[0].price if m.offers else "-" print(f"{m.symbol:<50} bid {bid:>8} offer {offer:>8} pays {m.max_price}") print("next cursor:", page.cursor) ``` Prices are dollar strings (`"0.4200"`). A winning contract pays the market's `max_price`. #### Place and cancel an order ```python order.py from stx import STX with STX() as client: market = next( m for m in client.iter_markets( status="open", trading=True, sort_by="event_start", sort_direction="desc" ) if m.event_status == "scheduled" ) order = client.place_order(market.market_id, "buy", "limit", price="0.01", quantity="1") print("placed", order.id, order.status, order.price, order.quantity) print("cancel", client.cancel_order(order.id).status) ``` A 1-cent buy for one contract, cancelled straight away. #### Stream your orders ```python stream.py import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: async with client.websocket() as ws: orders = await ws.orders() snapshot = await orders.wait_snapshot() print("open orders:", len(snapshot["all_orders"]["orders"])) asyncio.run(main()) ``` ## Next - [Trading](/sdks/python/trading/): batch orders, cancel many or all, expiration, cancel-on-disconnect - [WebSockets](/sdks/python/websockets/): every channel - [Errors & retries](/sdks/python/errors-and-retries/) --- # API reference > Every class and method in stx-python, generated from the source. Source: https://docs.stxapp.io/sdks/python/reference/ {/* Generated from the Python source by tools/gen_reference.py. Do not edit by hand. */} Generated from the source of `stx-python`. For a walkthrough, start with the [overview](/sdks/python/). | Page | Covers | |---|---| | [STX](/sdks/python/reference/stx/) | The blocking client: every method, its parameters and what it returns. | | [AsyncSTX](/sdks/python/reference/async-stx/) | The asyncio client: every method and its parameters. | | [STXWebSocket](/sdks/python/reference/stx-websocket/) | The WebSocket client: connecting, joining channels and reconnecting. | | [Channel](/sdks/python/reference/channel/) | A joined WebSocket channel and the messages it delivers. | | [Page and retry policies](/sdks/python/reference/helpers/) | Pagination results, batch order results, and the retry and reconnect policies. | | [Errors](/sdks/python/reference/errors/) | Every exception the SDK raises. | --- # AsyncSTX > The asyncio client: every method and its parameters. Source: https://docs.stxapp.io/sdks/python/reference/async-stx/ {/* Generated from the Python source by tools/gen_reference.py. Do not edit by hand. */} Async client for the STX exchange. Configuration comes from the arguments, then environment variables, then a profile in `~/.stx/credentials`: ```python async with AsyncSTX() as client: me = await client.me() page = await client.markets(status="open", limit=10) ``` ## Constructor | Parameter | Description | |---|---| | `region` | with `env`, picks a known host, e.g. `region="us", env="demo"`. | | `env` | the environment within `region`, e.g. `"demo"` or `"production"`. | | `host` | a hostname or URL, overriding `region`/`env`. | | `key_id` | the API key id. | | `private_key` | the key's Ed25519 private key (PEM text or a path to a PEM file). | | `signer` | instead of `private_key`, a callable `bytes -> bytes` returning the raw Ed25519 signature (for keys in an HSM or KMS). | | `profile` | section of `~/.stx/credentials` to read. | | `verify_tls` | set `False` only for a local server. | | `retry` | a `stx.RetryPolicy`; `stx.NO_RETRY` disables retries. | | `timeout` | seconds per HTTP request. | | `transport` | an `httpx.AsyncBaseTransport`, for tests. | ## Attributes | Attribute | Type | Description | |---|---|---| | `credentials` | `Optional[ApiKeyCredentials]` | | ## Methods ### `accept_terms()` ```python accept_terms(device_id: str, *, accept_terms: bool = True, accept_privacy: bool = True, accept_house_rules: Optional[bool] = None) -> str ``` `POST /api/v1/tnc/accept`: accept the current terms. Returns the server's message. | Parameter | Type | Description | |---|---|---| | `device_id` | `str` | | | `accept_terms` | `bool` | | | `accept_privacy` | `bool` | | | `accept_house_rules` | `Optional[bool]` | | ### `account_market_stats()` ```python account_market_stats(*, market_ids: StrList = None, event_ids: StrList = None, exclude_zero_settlements: Optional[bool] = None, from_time: Optional[int] = None, to_time: Optional[int] = None, sports: StrList = None, competitions: StrList = None, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.MarketStat] ``` `GET /api/v1/account/market_stats`: your exposure and P&L per market. Not the public `market_stats` channel, which carries prices. | Parameter | Type | Description | |---|---|---| | `market_ids` | `StrList` | | | `event_ids` | `StrList` | | | `exclude_zero_settlements` | `Optional[bool]` | | | `from_time` | `Optional[int]` | | | `to_time` | `Optional[int]` | | | `sports` | `StrList` | | | `competitions` | `StrList` | | | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | ### `adjustments()` ```python adjustments(*, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.PaymentTransaction] ``` `GET /api/v1/portfolio/adjustments`: manual balance adjustments. | Parameter | Type | Description | |---|---|---| | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | ### `balance()` ```python balance() -> models.Balance ``` `GET /api/v1/account/balance`: balance, liabilities and fee schedule. The same object the `balances` channel pushes. ### `cancel_all_orders()` ```python cancel_all_orders() -> List[models.Cancellation] ``` `DELETE /api/v1/orders/all`: cancel everything resting on the account. ### `cancel_order()` ```python cancel_order(order_id: str) -> models.Cancellation ``` `DELETE /api/v1/orders/{order_id}`: request a cancel. A cancel is a request: a fill already in flight can still land. | Parameter | Type | Description | |---|---|---| | `order_id` | `str` | | ### `cancel_orders()` ```python cancel_orders(order_ids: Sequence[str]) -> List[models.Cancellation] ``` `DELETE /api/v1/orders/batched`: cancel the named orders. | Parameter | Type | Description | |---|---|---| | `order_ids` | `Sequence[str]` | | ### `deposits()` ```python deposits(*, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.PaymentTransaction] ``` `GET /api/v1/portfolio/deposits`. | Parameter | Type | Description | |---|---|---| | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | ### `events()` ```python events(*, event_ids: StrList = None, sports: StrList = None, competitions: StrList = None, event_types: StrList = None, title: Optional[str] = None, status: Optional[str] = None, promoted: Optional[bool] = None, sort_by: Optional[str] = None, sort_direction: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.Event] ``` `GET /api/v1/events`: one page of events. `status` is one of `scheduled`, `in_progress`, `completed`, `cancelled`. `sort_by` is `"start_time"`. | Parameter | Type | Description | |---|---|---| | `event_ids` | `StrList` | | | `sports` | `StrList` | | | `competitions` | `StrList` | | | `event_types` | `StrList` | | | `title` | `Optional[str]` | | | `status` | `Optional[str]` | | | `promoted` | `Optional[bool]` | | | `sort_by` | `Optional[str]` | | | `sort_direction` | `Optional[str]` | | | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | ### `fees()` ```python fees(*, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.FeeTransaction] ``` `GET /api/v1/portfolio/fees`: fee and fee-refund entries. | Parameter | Type | Description | |---|---|---| | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | ### `fills()` ```python fills(*, market_ids: StrList = None, order_ids: StrList = None, status: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.Fill] ``` `GET /api/v1/fills`: one page of your executions. `order_ids` narrows to the fills of those orders. `status` is one of `created`, `open`, `settled`, `cancelled`. | Parameter | Type | Description | |---|---|---| | `market_ids` | `StrList` | | | `order_ids` | `StrList` | | | `status` | `Optional[str]` | | | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | ### `iter_account_market_stats()` ```python iter_account_market_stats(*, market_ids: StrList = None, event_ids: StrList = None, exclude_zero_settlements: Optional[bool] = None, from_time: Optional[int] = None, to_time: Optional[int] = None, sports: StrList = None, competitions: StrList = None, limit: Optional[int] = None) -> AsyncIterator[models.MarketStat] ``` Every per-market stat row, following the cursor. | Parameter | Type | Description | |---|---|---| | `market_ids` | `StrList` | | | `event_ids` | `StrList` | | | `exclude_zero_settlements` | `Optional[bool]` | | | `from_time` | `Optional[int]` | | | `to_time` | `Optional[int]` | | | `sports` | `StrList` | | | `competitions` | `StrList` | | | `limit` | `Optional[int]` | | ### `iter_adjustments()` ```python iter_adjustments(*, limit: Optional[int] = None) -> AsyncIterator[models.PaymentTransaction] ``` Every adjustment, following the cursor. | Parameter | Type | Description | |---|---|---| | `limit` | `Optional[int]` | | ### `iter_deposits()` ```python iter_deposits(*, limit: Optional[int] = None) -> AsyncIterator[models.PaymentTransaction] ``` Every deposit, following the cursor. | Parameter | Type | Description | |---|---|---| | `limit` | `Optional[int]` | | ### `iter_events()` ```python iter_events(*, event_ids: StrList = None, sports: StrList = None, competitions: StrList = None, event_types: StrList = None, title: Optional[str] = None, status: Optional[str] = None, promoted: Optional[bool] = None, sort_by: Optional[str] = None, sort_direction: Optional[str] = None, limit: Optional[int] = None) -> AsyncIterator[models.Event] ``` Every event matching the filters, following the cursor. | Parameter | Type | Description | |---|---|---| | `event_ids` | `StrList` | | | `sports` | `StrList` | | | `competitions` | `StrList` | | | `event_types` | `StrList` | | | `title` | `Optional[str]` | | | `status` | `Optional[str]` | | | `promoted` | `Optional[bool]` | | | `sort_by` | `Optional[str]` | | | `sort_direction` | `Optional[str]` | | | `limit` | `Optional[int]` | | ### `iter_fees()` ```python iter_fees(*, limit: Optional[int] = None) -> AsyncIterator[models.FeeTransaction] ``` Every fee entry, following the cursor. | Parameter | Type | Description | |---|---|---| | `limit` | `Optional[int]` | | ### `iter_fills()` ```python iter_fills(*, market_ids: StrList = None, order_ids: StrList = None, status: Optional[str] = None, limit: Optional[int] = None) -> AsyncIterator[models.Fill] ``` Every fill matching the filters, following the cursor. | Parameter | Type | Description | |---|---|---| | `market_ids` | `StrList` | | | `order_ids` | `StrList` | | | `status` | `Optional[str]` | | | `limit` | `Optional[int]` | | ### `iter_loyalty()` ```python iter_loyalty(*, limit: Optional[int] = None) -> AsyncIterator[models.Transaction] ``` Every loyalty entry, following the cursor. | Parameter | Type | Description | |---|---|---| | `limit` | `Optional[int]` | | ### `iter_markets()` ```python iter_markets(*, market_ids: StrList = None, event_ids: StrList = None, status: StrList = None, trading: Optional[bool] = None, sports: StrList = None, competitions: StrList = None, sort_by: Optional[str] = None, sort_direction: Optional[str] = None, limit: Optional[int] = None) -> AsyncIterator[models.Market] ``` Every market matching the filters, following the cursor. | Parameter | Type | Description | |---|---|---| | `market_ids` | `StrList` | | | `event_ids` | `StrList` | | | `status` | `StrList` | | | `trading` | `Optional[bool]` | | | `sports` | `StrList` | | | `competitions` | `StrList` | | | `sort_by` | `Optional[str]` | | | `sort_direction` | `Optional[str]` | | | `limit` | `Optional[int]` | | ### `iter_orders()` ```python iter_orders(*, order_ids: StrList = None, client_order_ids: StrList = None, market_ids: StrList = None, status: StrList = None, limit: Optional[int] = None) -> AsyncIterator[models.Order] ``` Every order matching the filters, following the cursor. | Parameter | Type | Description | |---|---|---| | `order_ids` | `StrList` | | | `client_order_ids` | `StrList` | | | `market_ids` | `StrList` | | | `status` | `StrList` | | | `limit` | `Optional[int]` | | ### `iter_settlements()` ```python iter_settlements(*, market_ids: StrList = None, type: Optional[str] = None, limit: Optional[int] = None) -> AsyncIterator[models.Settlement] ``` Every settlement, following the cursor. | Parameter | Type | Description | |---|---|---| | `market_ids` | `StrList` | | | `type` | `Optional[str]` | | | `limit` | `Optional[int]` | | ### `iter_withdrawals()` ```python iter_withdrawals(*, limit: Optional[int] = None) -> AsyncIterator[models.PaymentTransaction] ``` Every withdrawal, following the cursor. | Parameter | Type | Description | |---|---|---| | `limit` | `Optional[int]` | | ### `loyalty()` ```python loyalty(*, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.Transaction] ``` `GET /api/v1/portfolio/loyalty`: loyalty entries. | Parameter | Type | Description | |---|---|---| | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | ### `market()` ```python market(market_id: str) -> models.Market ``` One market by id. Raises `STXNotFoundException` if there is none. | Parameter | Type | Description | |---|---|---| | `market_id` | `str` | | ### `markets()` ```python markets(*, market_ids: StrList = None, event_ids: StrList = None, status: StrList = None, trading: Optional[bool] = None, sports: StrList = None, competitions: StrList = None, sort_by: Optional[str] = None, sort_direction: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.Market] ``` `GET /api/v1/markets`: one page of markets. Filters combine with AND; list filters match any value. `status` takes one status or a list (`["pre_open", "open"]`). `sort_by` is `"event_start"` with `sort_direction` `"asc"` or `"desc"`. `limit` defaults to 100, maximum 200. | Parameter | Type | Description | |---|---|---| | `market_ids` | `StrList` | | | `event_ids` | `StrList` | | | `status` | `StrList` | | | `trading` | `Optional[bool]` | | | `sports` | `StrList` | | | `competitions` | `StrList` | | | `sort_by` | `Optional[str]` | | | `sort_direction` | `Optional[str]` | | | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | ### `me()` ```python me() -> models.Me ``` `GET /api/v1/me`: who this key belongs to. Returns the `user_id` the account channels are keyed by, the `account_id`, and the key's `scope` (`read_only` or `read_write`). ### `order()` ```python order(order_id: str) -> models.Order ``` `GET /api/v1/orders/{order_id}`: one of your orders. | Parameter | Type | Description | |---|---|---| | `order_id` | `str` | | ### `orders()` ```python orders(*, order_ids: StrList = None, client_order_ids: StrList = None, market_ids: StrList = None, status: StrList = None, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.Order] ``` `GET /api/v1/orders`: one page of your orders, newest first. `status` takes one status or a list, e.g. `["open", "delayed"]`. | Parameter | Type | Description | |---|---|---| | `order_ids` | `StrList` | | | `client_order_ids` | `StrList` | | | `market_ids` | `StrList` | | | `status` | `StrList` | | | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | ### `place_order()` ```python place_order(market_id: str, action: str, order_type: str, *, price: Optional[Amount] = None, quantity: Amount, client_order_id: Optional[str] = None, expiration: Optional[str] = None, expiration_time: Optional[int] = None, cancel_on_disconnect: Optional[bool] = None, device_id: Optional[str] = None) -> models.Order ``` `POST /api/v1/orders`: place one order. `action` is `"buy"` or `"sell"`; `order_type` is `"limit"` or `"market"`. `price` (dollars, required for a limit order) and `quantity` (contracts) are decimal strings, e.g. `price="0.56", quantity="2"`. Floats are refused client-side. `expiration` is `"good_till_start"` or `"good_till_time"`; the latter needs `expiration_time` in Unix **microseconds**. `cancel_on_disconnect=True` opts this order into cancel-on- disconnect, which also needs the `orders` channel joined with it armed (see `stx.STXWebSocket.orders`). The exchange validates the order: a bad price step or a fractional quantity raises `STXValidationException` (400) or `STXRejectedException` (422) with the API's message. A `POST` is never retried after a 5xx or a dropped connection; pass `client_order_id` so you can look the order up if that happens. | Parameter | Type | Description | |---|---|---| | `market_id` | `str` | | | `action` | `str` | | | `order_type` | `str` | | | `price` | `Optional[Amount]` | | | `quantity` | `Amount` | | | `client_order_id` | `Optional[str]` | | | `expiration` | `Optional[str]` | | | `expiration_time` | `Optional[int]` | | | `cancel_on_disconnect` | `Optional[bool]` | | | `device_id` | `Optional[str]` | | ### `place_orders()` ```python place_orders(orders: Sequence[Mapping[str, *, Any]], geo_location: Optional[str] = None) -> List[BatchOrderResult] ``` `POST /api/v1/orders/batched`: place several orders in one call. Each order is a dict with the same fields as `place_order`: ```python await client.place_orders([ {"market_id": m, "action": "buy", "order_type": "limit", "price": "0.01", "quantity": "1"}, {"market_id": m, "action": "buy", "order_type": "limit", "price": "0.02", "quantity": "1"}, ]) ``` Returns one `stx.BatchOrderResult` per order, in order; a rejected order has `errors` instead of `order` and does not stop the others. | Parameter | Type | Description | |---|---|---| | `orders` | `Sequence[Mapping[str, Any]]` | | | `geo_location` | `Optional[str]` | | ### `positions()` ```python positions(*, market_ids: StrList = None) -> List[models.Position] ``` `GET /api/v1/positions`: your open positions. The same objects the `positions` channel sends on join. `position` is positive when long, negative when short. | Parameter | Type | Description | |---|---|---| | `market_ids` | `StrList` | | ### `settlements()` ```python settlements(*, market_ids: StrList = None, type: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.Settlement] ``` `GET /api/v1/portfolio/settlements`: settlements on your account. `type` is one of `closed_short`, `closed_long`, `expired_short`, `expired_long`. | Parameter | Type | Description | |---|---|---| | `market_ids` | `StrList` | | | `type` | `Optional[str]` | | | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | ### `user_id()` ```python user_id() -> str ``` The user id from `me`, fetched once and cached. ### `websocket()` ```python websocket(**kwargs: Any) -> STXWebSocket ``` An `stx.STXWebSocket` using this client's host and key. Account channels need your user id; the socket fetches it through this client's `me` on first use. | Parameter | Type | Description | |---|---|---| | `kwargs` | `Any` | | ### `withdrawals()` ```python withdrawals(*, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.PaymentTransaction] ``` `GET /api/v1/portfolio/withdrawals`. | Parameter | Type | Description | |---|---|---| | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | --- # Channel > A joined WebSocket channel and the messages it delivers. Source: https://docs.stxapp.io/sdks/python/reference/channel/ {/* Generated from the Python source by tools/gen_reference.py. Do not edit by hand. */} ## `Channel` One joined topic. ### Attributes | Attribute | Type | Description | |---|---|---| | `reply` | `Dict[str, Any]` | | | `snapshots` | `Dict[str, Any]` | | | `watches` | `List[str]` | | | `join_ref` | `Optional[str]` | | ### Methods #### `leave()` ```python leave() -> None ``` Leave the topic. The channel stops receiving and its iterator ends. #### `next()` ```python next(timeout: Optional[float] = None) -> ChannelMessage ``` The next message on this channel. Raises `asyncio.TimeoutError`. | Parameter | Type | Description | |---|---|---| | `timeout` | `Optional[float]` | | #### `ping()` ```python ping() -> Any ``` Channel `ping`. On `orders` with cancel-on-disconnect armed this resets the cancel deadline; on `account` and `orders` it keeps the session alive. #### `push()` ```python push(event: str, payload: Any = None, timeout: Optional[float] = None) -> Any ``` Send `event` on this channel and return the reply's `response`. Raises `STXChannelException` when the reply status is `error`. | Parameter | Type | Description | |---|---|---| | `event` | `str` | | | `payload` | `Any` | | | `timeout` | `Optional[float]` | | #### `request_series()` ```python request_series(market_ids: Sequence[str], range: str = 'all') -> Any ``` `market_stats`: fetch history at `range` (`day`, `week`, `month`, `all`) without changing the subscription. | Parameter | Type | Description | |---|---|---| | `market_ids` | `Sequence[str]` | | | `range` | `str` | | #### `select_filters()` ```python select_filters(**filters: Optional[Sequence[str]]) -> Any ``` `ticker` (`sports`, `competitions`) and `trades` (`market_ids`, `event_ids`): change filters without rejoining. | Parameter | Type | Description | |---|---|---| | `filters` | `Optional[Sequence[str]]` | | #### `select_market_ids()` ```python select_market_ids(market_ids: Optional[Sequence[str]]) -> Any ``` Change the `market_ids` filter without rejoining. On `orders`, `fills`, `positions`, `settlements` and `account` `None` clears the filter; `orderbook` and `market_stats` require at least one id. Returns the reply, whose `selected_market_ids` is what the server applied. | Parameter | Type | Description | |---|---|---| | `market_ids` | `Optional[Sequence[str]]` | | #### `select_message_types()` ```python select_message_types(message_types: Optional[Sequence[str]]) -> Any ``` `markets`: receive `market_created`, `market_updated` or both. | Parameter | Type | Description | |---|---|---| | `message_types` | `Optional[Sequence[str]]` | | #### `select_rule_filters()` ```python select_rule_filters(rule_filters: Optional[Sequence[str]]) -> Any ``` `markets`: change the `rules` filter; `None` disables it. | Parameter | Type | Description | |---|---|---| | `rule_filters` | `Optional[Sequence[str]]` | | #### `wait_snapshot()` ```python wait_snapshot(timeout: Optional[float] = 10.0) -> Dict[str, Any] ``` Wait for the state-on-join and return it as `{event: payload}`. `orders` gives `{"all_orders": {...}}`; `account` waits for all four of its snapshots. `market_stats` returns the join reply, which carries the series. Channels with no snapshot (`settlements`, `ticker`, `trades`, `orderbook`, `markets`, `market_updates`) raise `ValueError`. | Parameter | Type | Description | |---|---|---| | `timeout` | `Optional[float]` | | #### `watch()` ```python watch(market_ids: Sequence[str]) -> Any ``` `market_updates`: start receiving `created`/`updated` for these markets. Re-sent automatically after a reconnect. Returns the reply, whose `subscriptions.watches` lists what is watched. | Parameter | Type | Description | |---|---|---| | `market_ids` | `Sequence[str]` | | ## `ChannelMessage` One pushed frame. `payload` is the event's JSON object as documented for the channel, with money and quantities as strings. `channel` is the topic without the user id (`"orders"` for `orders:`). ### Attributes | Attribute | Type | Description | |---|---|---| | `topic` | `str` | | | `event` | `str` | | | `payload` | `Any` | | | `ref` | `Optional[str]` | | | `join_ref` | `Optional[str]` | | | `channel` | `str` | | | `is_snapshot` | `bool` | `True` for the state-on-join events (`all_orders`, `balances`...). | --- # Errors > Every exception the SDK raises. Source: https://docs.stxapp.io/sdks/python/reference/errors/ {/* Generated from the Python source by tools/gen_reference.py. Do not edit by hand. */} Every exception extends `STXException`. | Exception | Extends | When | |---|---|---| | [`STXException`](#stxexception) | `Exception` | Base for every error the SDK raises. | | [`STXConfigException`](#stxconfigexception) | `STXException` | The client is misconfigured: unknown region, missing key, bad PEM. | | [`STXAPIException`](#stxapiexception) | `STXException` | The API answered with an error status. Base for the classes below. | | [`STXValidationException`](#stxvalidationexception) | `STXAPIException` | 400: a parameter or body field was rejected, e.g. `price is required`. | | [`STXAuthenticationException`](#stxauthenticationexception) | `STXAPIException` | 401: the signature, key id or timestamp was not accepted. | | [`STXForbiddenException`](#stxforbiddenexception) | `STXAPIException` | 403: the key is read-only, or the account may not do this. | | [`STXNotFoundException`](#stxnotfoundexception) | `STXAPIException` | 404: the resource does not exist, or belongs to another account. | | [`STXRejectedException`](#stxrejectedexception) | `STXAPIException` | 422: the exchange refused the request, e.g. insufficient funds or a closed market. | | [`STXRateLimitException`](#stxratelimitexception) | `STXAPIException` | 429: too many requests. `retry_after` is seconds, when sent. | | [`STXServerException`](#stxserverexception) | `STXAPIException` | 5xx: the server failed. Retried by the default policy. | | [`STXTransportException`](#stxtransportexception) | `STXException` | The request never got an answer: connection, DNS, TLS or timeout. | | [`STXChannelException`](#stxchannelexception) | `STXException` | A channel join or channel message was refused by the server. | ## `STXException` Base for every error the SDK raises. ## `STXConfigException` The client is misconfigured: unknown region, missing key, bad PEM. ## `STXAPIException` The API answered with an error status. Base for the classes below. ## `STXValidationException` 400: a parameter or body field was rejected, e.g. `price is required`. ## `STXAuthenticationException` 401: the signature, key id or timestamp was not accepted. A machine clock more than 30 seconds off also lands here. ## `STXForbiddenException` 403: the key is read-only, or the account may not do this. ## `STXNotFoundException` 404: the resource does not exist, or belongs to another account. ## `STXRejectedException` 422: the exchange refused the request, e.g. insufficient funds or a closed market. ## `STXRateLimitException` 429: too many requests. `retry_after` is seconds, when sent. ## `STXServerException` 5xx: the server failed. Retried by the default policy. ## `STXTransportException` The request never got an answer: connection, DNS, TLS or timeout. ## `STXChannelException` A channel join or channel message was refused by the server. `reply` holds the server's `response` object, for example `{"reason": "unauthorized"}`. --- # Page and retry policies > Pagination results, batch order results, and the retry and reconnect policies. Source: https://docs.stxapp.io/sdks/python/reference/helpers/ {/* Generated from the Python source by tools/gen_reference.py. Do not edit by hand. */} ## `Page` A `Sequence` of models plus the `cursor` for the next page. ### Attributes | Attribute | Type | Description | |---|---|---| | `has_more` | `bool` | `True` when another page exists. | ## `BatchOrderResult` One entry of `place_orders`: the order, or why it was not placed. Results come back in the order the orders were sent. Exactly one of `order` and `errors` is set. ### Attributes | Attribute | Type | Description | |---|---|---| | `order` | `Optional[Order]` | | | `errors` | `Optional[List[str]]` | | | `ok` | `bool` | `True` when the order was placed. | ## `RetryPolicy` How many times to try a call, and how long to wait between tries. The default is 3 attempts, 0.5 s initial backoff doubling to a 30 s cap, with jitter. `RetryPolicy(max_attempts=1)` (or `stx.NO_RETRY`) turns retries off. ### Attributes | Attribute | Type | Description | |---|---|---| | `max_attempts` | `int` | | | `initial_backoff` | `float` | | | `max_backoff` | `float` | | | `jitter` | `bool` | | | `retryable_exceptions` | `Tuple[Type[STXException], ...]` | | ### Methods #### `compute_backoff()` ```python compute_backoff(attempt: int, exc: STXException) -> float ``` Seconds to wait after attempt `attempt` (1-based) failed. | Parameter | Type | Description | |---|---|---| | `attempt` | `int` | | | `exc` | `STXException` | | #### `should_retry()` ```python should_retry(exc: STXException, attempt: int, idempotent: bool) -> bool ``` Whether to try again after `exc` ended attempt number `attempt`. | Parameter | Type | Description | |---|---|---| | `exc` | `STXException` | | | `attempt` | `int` | | | `idempotent` | `bool` | | ## `ReconnectPolicy` Backoff between reconnect attempts. `max_attempts=None` never gives up. ### Attributes | Attribute | Type | Description | |---|---|---| | `initial_backoff` | `float` | | | `max_backoff` | `float` | | | `max_attempts` | `Optional[int]` | | | `jitter` | `bool` | | ### Methods #### `delay()` ```python delay(attempt: int) -> float ``` | Parameter | Type | Description | |---|---|---| | `attempt` | `int` | | --- # STX > The blocking client: every method, its parameters and what it returns. Source: https://docs.stxapp.io/sdks/python/reference/stx/ {/* Generated from the Python source by tools/gen_reference.py. Do not edit by hand. */} Blocking client for the STX exchange. Takes the same arguments as `stx.AsyncSTX` and has the same methods, without `await`: ```python with STX() as client: print(client.me().user_id) for market in client.markets(status="open", limit=5): print(market.symbol, market.last_traded_price) ``` For WebSocket channels use `stx.STXWebSocket`, which is async. ## Attributes | Attribute | Type | Description | |---|---|---| | `async_client` | `AsyncSTX` | The underlying `AsyncSTX` (bound to this client's private loop). | ## Methods ### `accept_terms()` ```python accept_terms(device_id: str, *, accept_terms: bool = True, accept_privacy: bool = True, accept_house_rules: Optional[bool] = None) -> str ``` `POST /api/v1/tnc/accept`. | Parameter | Type | Description | |---|---|---| | `device_id` | `str` | | | `accept_terms` | `bool` | | | `accept_privacy` | `bool` | | | `accept_house_rules` | `Optional[bool]` | | ### `account_market_stats()` ```python account_market_stats(*, market_ids: StrList = None, event_ids: StrList = None, exclude_zero_settlements: Optional[bool] = None, from_time: Optional[int] = None, to_time: Optional[int] = None, sports: StrList = None, competitions: StrList = None, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.MarketStat] ``` `GET /api/v1/account/market_stats`. | Parameter | Type | Description | |---|---|---| | `market_ids` | `StrList` | | | `event_ids` | `StrList` | | | `exclude_zero_settlements` | `Optional[bool]` | | | `from_time` | `Optional[int]` | | | `to_time` | `Optional[int]` | | | `sports` | `StrList` | | | `competitions` | `StrList` | | | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | ### `adjustments()` ```python adjustments(*, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.PaymentTransaction] ``` `GET /api/v1/portfolio/adjustments`. | Parameter | Type | Description | |---|---|---| | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | ### `balance()` ```python balance() -> models.Balance ``` `GET /api/v1/account/balance`. ### `cancel_all_orders()` ```python cancel_all_orders() -> List[models.Cancellation] ``` `DELETE /api/v1/orders/all`. ### `cancel_order()` ```python cancel_order(order_id: str) -> models.Cancellation ``` `DELETE /api/v1/orders/{order_id}`. | Parameter | Type | Description | |---|---|---| | `order_id` | `str` | | ### `cancel_orders()` ```python cancel_orders(order_ids: Sequence[str]) -> List[models.Cancellation] ``` `DELETE /api/v1/orders/batched`. | Parameter | Type | Description | |---|---|---| | `order_ids` | `Sequence[str]` | | ### `deposits()` ```python deposits(*, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.PaymentTransaction] ``` `GET /api/v1/portfolio/deposits`. | Parameter | Type | Description | |---|---|---| | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | ### `events()` ```python events(*, event_ids: StrList = None, sports: StrList = None, competitions: StrList = None, event_types: StrList = None, title: Optional[str] = None, status: Optional[str] = None, promoted: Optional[bool] = None, sort_by: Optional[str] = None, sort_direction: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.Event] ``` `GET /api/v1/events`. See `AsyncSTX.events`. | Parameter | Type | Description | |---|---|---| | `event_ids` | `StrList` | | | `sports` | `StrList` | | | `competitions` | `StrList` | | | `event_types` | `StrList` | | | `title` | `Optional[str]` | | | `status` | `Optional[str]` | | | `promoted` | `Optional[bool]` | | | `sort_by` | `Optional[str]` | | | `sort_direction` | `Optional[str]` | | | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | ### `fees()` ```python fees(*, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.FeeTransaction] ``` `GET /api/v1/portfolio/fees`. | Parameter | Type | Description | |---|---|---| | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | ### `fills()` ```python fills(*, market_ids: StrList = None, order_ids: StrList = None, status: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.Fill] ``` `GET /api/v1/fills`. See `AsyncSTX.fills`. | Parameter | Type | Description | |---|---|---| | `market_ids` | `StrList` | | | `order_ids` | `StrList` | | | `status` | `Optional[str]` | | | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | ### `iter_account_market_stats()` ```python iter_account_market_stats(*, market_ids: StrList = None, event_ids: StrList = None, exclude_zero_settlements: Optional[bool] = None, from_time: Optional[int] = None, to_time: Optional[int] = None, sports: StrList = None, competitions: StrList = None, limit: Optional[int] = None) -> Iterator[models.MarketStat] ``` Every per-market stat row, page by page. | Parameter | Type | Description | |---|---|---| | `market_ids` | `StrList` | | | `event_ids` | `StrList` | | | `exclude_zero_settlements` | `Optional[bool]` | | | `from_time` | `Optional[int]` | | | `to_time` | `Optional[int]` | | | `sports` | `StrList` | | | `competitions` | `StrList` | | | `limit` | `Optional[int]` | | ### `iter_adjustments()` ```python iter_adjustments(*, limit: Optional[int] = None) -> Iterator[models.PaymentTransaction] ``` Every adjustment, page by page. | Parameter | Type | Description | |---|---|---| | `limit` | `Optional[int]` | | ### `iter_deposits()` ```python iter_deposits(*, limit: Optional[int] = None) -> Iterator[models.PaymentTransaction] ``` Every deposit, page by page. | Parameter | Type | Description | |---|---|---| | `limit` | `Optional[int]` | | ### `iter_events()` ```python iter_events(*, event_ids: StrList = None, sports: StrList = None, competitions: StrList = None, event_types: StrList = None, title: Optional[str] = None, status: Optional[str] = None, promoted: Optional[bool] = None, sort_by: Optional[str] = None, sort_direction: Optional[str] = None, limit: Optional[int] = None) -> Iterator[models.Event] ``` Every matching event, page by page. | Parameter | Type | Description | |---|---|---| | `event_ids` | `StrList` | | | `sports` | `StrList` | | | `competitions` | `StrList` | | | `event_types` | `StrList` | | | `title` | `Optional[str]` | | | `status` | `Optional[str]` | | | `promoted` | `Optional[bool]` | | | `sort_by` | `Optional[str]` | | | `sort_direction` | `Optional[str]` | | | `limit` | `Optional[int]` | | ### `iter_fees()` ```python iter_fees(*, limit: Optional[int] = None) -> Iterator[models.FeeTransaction] ``` Every fee entry, page by page. | Parameter | Type | Description | |---|---|---| | `limit` | `Optional[int]` | | ### `iter_fills()` ```python iter_fills(*, market_ids: StrList = None, order_ids: StrList = None, status: Optional[str] = None, limit: Optional[int] = None) -> Iterator[models.Fill] ``` Every matching fill, page by page. | Parameter | Type | Description | |---|---|---| | `market_ids` | `StrList` | | | `order_ids` | `StrList` | | | `status` | `Optional[str]` | | | `limit` | `Optional[int]` | | ### `iter_loyalty()` ```python iter_loyalty(*, limit: Optional[int] = None) -> Iterator[models.Transaction] ``` Every loyalty entry, page by page. | Parameter | Type | Description | |---|---|---| | `limit` | `Optional[int]` | | ### `iter_markets()` ```python iter_markets(*, market_ids: StrList = None, event_ids: StrList = None, status: StrList = None, trading: Optional[bool] = None, sports: StrList = None, competitions: StrList = None, sort_by: Optional[str] = None, sort_direction: Optional[str] = None, limit: Optional[int] = None) -> Iterator[models.Market] ``` Every matching market, page by page. | Parameter | Type | Description | |---|---|---| | `market_ids` | `StrList` | | | `event_ids` | `StrList` | | | `status` | `StrList` | | | `trading` | `Optional[bool]` | | | `sports` | `StrList` | | | `competitions` | `StrList` | | | `sort_by` | `Optional[str]` | | | `sort_direction` | `Optional[str]` | | | `limit` | `Optional[int]` | | ### `iter_orders()` ```python iter_orders(*, order_ids: StrList = None, client_order_ids: StrList = None, market_ids: StrList = None, status: StrList = None, limit: Optional[int] = None) -> Iterator[models.Order] ``` Every matching order, page by page. | Parameter | Type | Description | |---|---|---| | `order_ids` | `StrList` | | | `client_order_ids` | `StrList` | | | `market_ids` | `StrList` | | | `status` | `StrList` | | | `limit` | `Optional[int]` | | ### `iter_settlements()` ```python iter_settlements(*, market_ids: StrList = None, type: Optional[str] = None, limit: Optional[int] = None) -> Iterator[models.Settlement] ``` Every settlement, page by page. | Parameter | Type | Description | |---|---|---| | `market_ids` | `StrList` | | | `type` | `Optional[str]` | | | `limit` | `Optional[int]` | | ### `iter_withdrawals()` ```python iter_withdrawals(*, limit: Optional[int] = None) -> Iterator[models.PaymentTransaction] ``` Every withdrawal, page by page. | Parameter | Type | Description | |---|---|---| | `limit` | `Optional[int]` | | ### `loyalty()` ```python loyalty(*, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.Transaction] ``` `GET /api/v1/portfolio/loyalty`. | Parameter | Type | Description | |---|---|---| | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | ### `market()` ```python market(market_id: str) -> models.Market ``` One market by id. See `AsyncSTX.market`. | Parameter | Type | Description | |---|---|---| | `market_id` | `str` | | ### `markets()` ```python markets(*, market_ids: StrList = None, event_ids: StrList = None, status: StrList = None, trading: Optional[bool] = None, sports: StrList = None, competitions: StrList = None, sort_by: Optional[str] = None, sort_direction: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.Market] ``` `GET /api/v1/markets`. See `AsyncSTX.markets`. | Parameter | Type | Description | |---|---|---| | `market_ids` | `StrList` | | | `event_ids` | `StrList` | | | `status` | `StrList` | | | `trading` | `Optional[bool]` | | | `sports` | `StrList` | | | `competitions` | `StrList` | | | `sort_by` | `Optional[str]` | | | `sort_direction` | `Optional[str]` | | | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | ### `me()` ```python me() -> models.Me ``` `GET /api/v1/me`. See `AsyncSTX.me`. ### `order()` ```python order(order_id: str) -> models.Order ``` `GET /api/v1/orders/{order_id}`. | Parameter | Type | Description | |---|---|---| | `order_id` | `str` | | ### `orders()` ```python orders(*, order_ids: StrList = None, client_order_ids: StrList = None, market_ids: StrList = None, status: StrList = None, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.Order] ``` `GET /api/v1/orders`. See `AsyncSTX.orders`. | Parameter | Type | Description | |---|---|---| | `order_ids` | `StrList` | | | `client_order_ids` | `StrList` | | | `market_ids` | `StrList` | | | `status` | `StrList` | | | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | ### `place_order()` ```python place_order(market_id: str, action: str, order_type: str, *, price: Optional[Amount] = None, quantity: Amount, client_order_id: Optional[str] = None, expiration: Optional[str] = None, expiration_time: Optional[int] = None, cancel_on_disconnect: Optional[bool] = None, device_id: Optional[str] = None) -> models.Order ``` `POST /api/v1/orders`. See `AsyncSTX.place_order`. | Parameter | Type | Description | |---|---|---| | `market_id` | `str` | | | `action` | `str` | | | `order_type` | `str` | | | `price` | `Optional[Amount]` | | | `quantity` | `Amount` | | | `client_order_id` | `Optional[str]` | | | `expiration` | `Optional[str]` | | | `expiration_time` | `Optional[int]` | | | `cancel_on_disconnect` | `Optional[bool]` | | | `device_id` | `Optional[str]` | | ### `place_orders()` ```python place_orders(orders: Sequence[Mapping[str, *, Any]], geo_location: Optional[str] = None) -> List[BatchOrderResult] ``` `POST /api/v1/orders/batched`. See `AsyncSTX.place_orders`. | Parameter | Type | Description | |---|---|---| | `orders` | `Sequence[Mapping[str, Any]]` | | | `geo_location` | `Optional[str]` | | ### `positions()` ```python positions(*, market_ids: StrList = None) -> List[models.Position] ``` `GET /api/v1/positions`. See `AsyncSTX.positions`. | Parameter | Type | Description | |---|---|---| | `market_ids` | `StrList` | | ### `settlements()` ```python settlements(*, market_ids: StrList = None, type: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.Settlement] ``` `GET /api/v1/portfolio/settlements`. | Parameter | Type | Description | |---|---|---| | `market_ids` | `StrList` | | | `type` | `Optional[str]` | | | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | ### `user_id()` ```python user_id() -> str ``` Your user id, fetched once. See `AsyncSTX.user_id`. ### `withdrawals()` ```python withdrawals(*, limit: Optional[int] = None, cursor: Optional[str] = None) -> Page[models.PaymentTransaction] ``` `GET /api/v1/portfolio/withdrawals`. | Parameter | Type | Description | |---|---|---| | `limit` | `Optional[int]` | | | `cursor` | `Optional[str]` | | --- # STXWebSocket > The WebSocket client: connecting, joining channels and reconnecting. Source: https://docs.stxapp.io/sdks/python/reference/stx-websocket/ {/* Generated from the Python source by tools/gen_reference.py. Do not edit by hand. */} Async Phoenix-channels client for the documented STX topics. Build it from an `AsyncSTX` client, which supplies the host, the key and your user id: ```python async with AsyncSTX() as client: async with client.websocket() as ws: book = await ws.orderbook([""], on_message=print) orders = await ws.orders() print(await orders.wait_snapshot()) await ws.run_forever() ``` or standalone with the same settings arguments as `AsyncSTX`. ## Constructor | Parameter | Description | |---|---| | `rest` | an `AsyncSTX` to take host, key and user id from. | | `user_id` | your user id, if you already have it (skips `GET /me`). | | `heartbeat_interval` | seconds between socket heartbeats. The server closes a socket silent for 60 s. | | `channel_ping_interval` | seconds between channel `ping` frames on every joined topic; `None` disables them. `orders` with cancel-on-disconnect pings faster, from the granted timeout. | | `reconnect` | reconnect after a drop (default `True`). | | `reconnect_policy` | backoff for reconnects. | | `on_reconnect` | called (sync or async) after every successful reconnect and rejoin: the moment to call `orders()` and anything else you show again. | | `join_timeout` | seconds to wait for a join or push reply. | | `queue_size` | messages buffered per channel for `async for`; the oldest is dropped when full. | ## Attributes | Attribute | Type | Description | |---|---|---| | `url` | `str` | | | `verify_tls` | `bool` | | | `channels` | `Dict[str, Channel]` | | | `connected` | `bool` | | ## Methods ### `account()` ```python account(*, market_ids: Optional[Sequence[str]] = None, on_message: Optional[MessageHandler] = None) -> Channel ``` `account:{user_id}`: everything the five channels above carry, on one join. Do not also join a per-type channel (you would get every message twice), and use `orders` if you need cancel-on-disconnect. | Parameter | Type | Description | |---|---|---| | `market_ids` | `Optional[Sequence[str]]` | | | `on_message` | `Optional[MessageHandler]` | | ### `balances()` ```python balances(*, account_id: Optional[str] = None, on_message: Optional[MessageHandler] = None) -> Channel ``` `balances:{user_id}`: `balances` on join, then `update` and `payment_update`. `account_id` picks one of your accounts. | Parameter | Type | Description | |---|---|---| | `account_id` | `Optional[str]` | | | `on_message` | `Optional[MessageHandler]` | | ### `connect()` ```python connect() -> None ``` Open the socket. Idempotent. ### `fills()` ```python fills(*, market_ids: Optional[Sequence[str]] = None, on_message: Optional[MessageHandler] = None) -> Channel ``` `fills:{user_id}`: `all_trades` on join, then one `trade` per execution or status change. | Parameter | Type | Description | |---|---|---| | `market_ids` | `Optional[Sequence[str]]` | | | `on_message` | `Optional[MessageHandler]` | | ### `join()` ```python join(topic: str, payload: Optional[Dict[str, Any]] = None, *, on_message: Optional[MessageHandler] = None, ping_interval: Any = _UNSET) -> Channel ``` Join `topic` with `payload` and wait for the reply. Raises `STXChannelException` with the server's reason (for example `market_ids_required` or `unauthorized`) if the join is refused. | Parameter | Type | Description | |---|---|---| | `topic` | `str` | | | `payload` | `Optional[Dict[str, Any]]` | | | `on_message` | `Optional[MessageHandler]` | | | `ping_interval` | `Any` | | ### `market_stats()` ```python market_stats(market_ids: Sequence[str], *, range: Optional[str] = None, on_message: Optional[MessageHandler] = None) -> Channel ``` `market_stats`: a price series per market. The history is in the join reply (`channel.reply["markets"]`); `market_stats` pushes changed buckets (upsert by `timestamp_us`) and `market_stats_snapshot` replaces a series. | Parameter | Type | Description | |---|---|---| | `market_ids` | `Sequence[str]` | | | `range` | `Optional[str]` | | | `on_message` | `Optional[MessageHandler]` | | ### `market_updates()` ```python market_updates(watch: Optional[Sequence[str]] = None, *, on_message: Optional[MessageHandler] = None) -> Channel ``` `market_updates`: `created` and `updated` for the markets you watch. Nothing arrives until you watch something; pass `watch=` or call `channel.watch([...])`. Prices are converted from cents to dollar strings here. | Parameter | Type | Description | |---|---|---| | `watch` | `Optional[Sequence[str]]` | | | `on_message` | `Optional[MessageHandler]` | | ### `markets()` ```python markets(*, rule_filters: Optional[Sequence[str]] = None, message_types: Optional[Sequence[str]] = None, on_message: Optional[MessageHandler] = None) -> Channel ``` `markets`: `market_created` and `market_updated` for every market. Each payload maps market id to a market object; `market_updated` carries only the changed fields. Prices arrive in cents on the wire and are converted to dollar strings here. | Parameter | Type | Description | |---|---|---| | `rule_filters` | `Optional[Sequence[str]]` | | | `message_types` | `Optional[Sequence[str]]` | | | `on_message` | `Optional[MessageHandler]` | | ### `orderbook()` ```python orderbook(market_ids: Sequence[str], *, on_message: Optional[MessageHandler] = None) -> Channel ``` `orderbook`: the aggregated book, one `book` push per market. Each push is a full snapshot of that market's book; replace what you hold rather than merging. `market_ids` is required. | Parameter | Type | Description | |---|---|---| | `market_ids` | `Sequence[str]` | | | `on_message` | `Optional[MessageHandler]` | | ### `orders()` ```python orders(*, market_ids: Optional[Sequence[str]] = None, cancel_on_disconnect: bool = False, ping_timeout: Optional[int] = None, on_message: Optional[MessageHandler] = None) -> Channel ``` `orders:{user_id}`: `all_orders` on join, then `new_open_order`. `cancel_on_disconnect=True` arms cancel-on-disconnect for orders placed with `cancel_on_disconnect=True`. `ping_timeout` is in milliseconds, clamped by the server to 5000 to 20000; the granted value is in `channel.reply["ping_timeout"]` and the SDK pings at 60% of it for as long as the channel is joined. | Parameter | Type | Description | |---|---|---| | `market_ids` | `Optional[Sequence[str]]` | | | `cancel_on_disconnect` | `bool` | | | `ping_timeout` | `Optional[int]` | | | `on_message` | `Optional[MessageHandler]` | | ### `positions()` ```python positions(*, market_ids: Optional[Sequence[str]] = None, on_message: Optional[MessageHandler] = None) -> Channel ``` `positions:{user_id}`: `all_positions` on join, then `updated_positions` deltas with only the changed positions. | Parameter | Type | Description | |---|---|---| | `market_ids` | `Optional[Sequence[str]]` | | | `on_message` | `Optional[MessageHandler]` | | ### `run_forever()` ```python run_forever() -> None ``` Block until `close` is called (or reconnects are exhausted). ### `settlements()` ```python settlements(*, market_ids: Optional[Sequence[str]] = None, on_message: Optional[MessageHandler] = None) -> Channel ``` `settlements:{user_id}`: `new_settlements` as they are recorded. No snapshot; history is `AsyncSTX.settlements()`. | Parameter | Type | Description | |---|---|---| | `market_ids` | `Optional[Sequence[str]]` | | | `on_message` | `Optional[MessageHandler]` | | ### `ticker()` ```python ticker(*, sports: Optional[Sequence[str]] = None, competitions: Optional[Sequence[str]] = None, on_message: Optional[MessageHandler] = None) -> Channel ``` `ticker`: a `ticker` push whenever a market's price, top of book, volume or open interest moves. No snapshot on join. | Parameter | Type | Description | |---|---|---| | `sports` | `Optional[Sequence[str]]` | | | `competitions` | `Optional[Sequence[str]]` | | | `on_message` | `Optional[MessageHandler]` | | ### `trades()` ```python trades(*, market_ids: Optional[Sequence[str]] = None, event_ids: Optional[Sequence[str]] = None, on_message: Optional[MessageHandler] = None) -> Channel ``` `trades`: every execution on the exchange, anonymised. `action` is the taker's side. Not your fills: see `fills`. | Parameter | Type | Description | |---|---|---| | `market_ids` | `Optional[Sequence[str]]` | | | `event_ids` | `Optional[Sequence[str]]` | | | `on_message` | `Optional[MessageHandler]` | | ### `user_id()` ```python user_id() -> str ``` Your user id, from `user_id=` or `GET /api/v1/me`. ### `user_info()` ```python user_info(*, on_message: Optional[MessageHandler] = None) -> Channel ``` `user_info:{user_id}`: `user_updated` right after joining, then on every profile change. | Parameter | Type | Description | |---|---|---| | `on_message` | `Optional[MessageHandler]` | | --- # Trading > Place single and batch orders, cancel one, many or all, and read order state. Source: https://docs.stxapp.io/sdks/python/trading/ Placing and cancelling needs a `read_write` key. The examples run on the US demo exchange with 1-cent buy orders that will not fill. How orders behave on the exchange is covered in [Order types](/concepts/order-types/) and [Risk controls](/risk-controls/); this page shows the SDK calls. The examples use a market that is open, accepting orders (`trading` is `True`), and whose event has not started: ```python from stx import STX with STX() as client: market = next( m for m in client.iter_markets( status="open", trading=True, sort_by="event_start", sort_direction="desc" ) if m.event_status == "scheduled" ) print(market.market_id, market.symbol, market.max_price) ``` ## Place an order ```python from stx import STX with STX() as client: market = next( m for m in client.iter_markets( status="open", trading=True, sort_by="event_start", sort_direction="desc" ) if m.event_status == "scheduled" ) order = client.place_order( market.market_id, "buy", "limit", price="0.01", quantity="1", client_order_id="quickstart-1" ) print(order.id, order.status, order.price, order.quantity, order.client_order_id) client.cancel_order(order.id) ``` | Argument | Meaning | |---|---| | `market_id` | The market | | `action` | `"buy"` or `"sell"` | | `order_type` | `"limit"` or `"market"` | | `price` | Dollars as a string, e.g. `"0.56"`. Required for a limit order, omitted for a market order | | `quantity` | Contracts as a string, e.g. `"2"` | | `client_order_id` | Your own id, echoed back and filterable | | `expiration`, `expiration_time` | See [Expiration](#expiration) | | `cancel_on_disconnect` | See [Cancel-on-disconnect](#cancel-on-disconnect) | | `device_id` | Optional device label | `price` and `quantity` must be strings (or `decimal.Decimal`). A float raises `TypeError` before anything is sent, because a float cannot carry an exact decimal. The exchange validates everything else: a price off the tick, a price at or above `max_price`, or a fractional quantity comes back as `STXRejectedException` (422) or `STXValidationException` (400) carrying the exchange's message: ```python from stx import STX, STXAPIException with STX() as client: market = next( m for m in client.iter_markets( status="open", trading=True, sort_by="event_start", sort_direction="desc" ) if m.event_status == "scheduled" ) try: client.place_order(market.market_id, "buy", "limit", price="0.01", quantity="1.5") except STXAPIException as exc: print(exc.status_code, exc) ``` The returned `Order` shows the state at acceptance (`accepted`, `open`, `filled`...). Follow it on the [`orders` channel](/sdks/python/websockets#orders) or with `client.order(order.id)`. ## Place several orders `place_orders()` sends one `POST /api/v1/orders/batched`. Each order is a dict with the `place_order` fields; the result has one entry per order, in order, holding either `order` or `errors`: ```python from stx import STX with STX() as client: market = next( m for m in client.iter_markets( status="open", trading=True, sort_by="event_start", sort_direction="desc" ) if m.event_status == "scheduled" ) results = client.place_orders( [ {"market_id": market.market_id, "action": "buy", "order_type": "limit", "price": "0.01", "quantity": "1"}, {"market_id": market.market_id, "action": "buy", "order_type": "limit", "price": "0.02", "quantity": "1"}, ] ) for r in results: print("placed" if r.ok else "rejected", r.order.id if r.ok else r.errors) cancelled = client.cancel_orders([r.order.id for r in results if r.ok]) print("cancelled", len(cancelled)) ``` One rejected order does not stop the others. ## Cancel ```python from stx import STX with STX() as client: market = next( m for m in client.iter_markets( status="open", trading=True, sort_by="event_start", sort_direction="desc" ) if m.event_status == "scheduled" ) a = client.place_order(market.market_id, "buy", "limit", price="0.01", quantity="1") b = client.place_order(market.market_id, "buy", "limit", price="0.01", quantity="1") c = client.place_order(market.market_id, "buy", "limit", price="0.01", quantity="1") print(client.cancel_order(a.id)) # one print(client.cancel_orders([b.id, c.id])) # several by id print(client.cancel_all_orders()) # everything left on the account ``` Each returns `Cancellation` objects with `order_id` and `status`. A cancel is a request: an order can fill between sending the cancel and the exchange processing it, so reconcile against fills rather than assuming you are flat. ## Read orders ```python from stx import STX with STX() as client: for order in client.orders(status=["open", "delayed"], limit=20): print(order.id, order.market_id, order.action, order.price, order.filled, "/", order.quantity) recent = client.orders(limit=1) if recent.items: print(client.order(recent[0].id).status) ``` `orders()` filters on `order_ids`, `client_order_ids`, `market_ids` and `status` (one or a list of `created`, `requested`, `accepted`, `delayed`, `open`, `filled`, `rejected`, `cancelled`, `partially_cancelled`). ## Avoid duplicate orders A `POST` is never retried after a server error or a dropped connection: the order may already be on the book. If that happens, look it up by `client_order_id` before placing again: ```python import uuid from stx import STX, STXServerException, STXTransportException with STX() as client: market = next( m for m in client.iter_markets( status="open", trading=True, sort_by="event_start", sort_direction="desc" ) if m.event_status == "scheduled" ) coid = str(uuid.uuid4()) try: order = client.place_order( market.market_id, "buy", "limit", price="0.01", quantity="1", client_order_id=coid ) except (STXServerException, STXTransportException): found = client.orders(client_order_ids=[coid]) order = found[0] if found.items else None print(order.id if order else "not placed") if order: client.cancel_order(order.id) ``` ## Expiration `expiration="good_till_start"` pulls the order when the event starts. `expiration="good_till_time"` pulls it at `expiration_time`, in Unix **microseconds**: ```python import time from stx import STX with STX() as client: market = next( m for m in client.iter_markets( status="open", trading=True, sort_by="event_start", sort_direction="desc" ) if m.event_status == "scheduled" ) in_one_hour = int((time.time() + 3600) * 1_000_000) order = client.place_order( market.market_id, "buy", "limit", price="0.01", quantity="1", expiration="good_till_time", expiration_time=in_one_hour, ) print(order.id, order.status) client.cancel_order(order.id) ``` ## Cancel-on-disconnect Cancel-on-disconnect takes two halves: join the `orders` channel with it armed, and place orders with `cancel_on_disconnect=True`. While the socket is up, the SDK pings the channel at 60% of the timeout the server granted, so your orders stay on the book. If your process dies, the exchange cancels them. The timeout limits and the grace period are on [Risk controls](/risk-controls/). ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: market = None async for m in client.iter_markets( status="open", trading=True, sort_by="event_start", sort_direction="desc" ): if m.event_status == "scheduled": market = m break async with client.websocket() as ws: orders = await ws.orders(cancel_on_disconnect=True, ping_timeout=5000) print("armed, server timeout (ms):", orders.reply["ping_timeout"]) order = await client.place_order( market.market_id, "buy", "limit", price="0.01", quantity="1", cancel_on_disconnect=True, ) await asyncio.sleep(8) # longer than the timeout: the pings keep it alive print((await client.order(order.id)).status) await client.cancel_order(order.id) asyncio.run(main()) ``` --- # WebSockets > Stream the order book, prices, trades, market changes and your account over one signed socket. Source: https://docs.stxapp.io/sdks/python/websockets/ The client streams the channels documented under [WebSockets](/websockets/): what each channel sends, its payloads and its limits are covered there. This page shows the SDK calls. WebSockets are async only, on `AsyncSTX`. ## Connect and join ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: async with client.websocket() as ws: ticker = await ws.ticker() print(ticker.topic, ticker.reply) asyncio.run(main()) ``` `client.websocket()` uses the client's host and key, signs the handshake with that key and sends a User-Agent header, which the exchange requires. The market data channels also accept an unsigned socket; the account channels need the signed one, so they need a key. Connecting joins nothing; each join method returns a `Channel`. `STXWebSocket()` also works on its own and takes the same settings as `AsyncSTX`. ## Receive messages Each message is a `ChannelMessage` with `topic`, `event` and `payload`. Take them with a callback, one at a time, or with `async for`: ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: ids = [m.market_id for m in await client.markets(status="open", limit=5)] async with client.websocket() as ws: # 1. A callback, sync or async. await ws.trades(market_ids=ids, on_message=lambda msg: print("trade", msg.payload)) book = await ws.orderbook(ids) # 2. One at a time, with a timeout. try: msg = await book.next(timeout=5) print(msg.event, msg.payload["market_id"]) except asyncio.TimeoutError: print("no book change in 5 s") # 3. As an async iterator (runs until the channel closes). async def consume(): async for msg in book: print("book", msg.payload["market_id"], msg.payload["bids"][:1]) task = asyncio.create_task(consume()) await asyncio.sleep(3) task.cancel() asyncio.run(main()) ``` A slow consumer never blocks the socket: each channel buffers up to `queue_size` messages (10,000 by default) and drops the oldest beyond that. ## Snapshots Account channels send your current state right after the join. `wait_snapshot()` returns it, keyed by event name: ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: async with client.websocket() as ws: positions = await ws.positions() snap = await positions.wait_snapshot(timeout=10) for p in snap["all_positions"]["positions"]: print(p["market_id"], p["position"], p["open_risk"]) asyncio.run(main()) ``` `market_stats` carries its history in the join reply instead (`channel.reply["markets"]`). `settlements`, `orderbook`, `ticker`, `trades`, `markets` and `market_updates` send no snapshot: take the starting state from `markets()`, `orders()` and the other client methods. ## Market data channels ### orderbook The aggregated book for the markets you name; `market_ids` is required. Each `book` push is the full book for one market: replace what you hold, do not merge. ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: ids = [m.market_id for m in await client.markets(status="open", limit=3)] async with client.websocket() as ws: book = await ws.orderbook(ids) print("selected", book.reply["selected_market_ids"]) await book.select_market_ids(ids[:1]) # change markets without rejoining try: msg = await book.next(timeout=10) print(msg.payload["market_id"], "bids", msg.payload["bids"][:2], "offers", msg.payload["offers"][:2]) except asyncio.TimeoutError: print("quiet book") asyncio.run(main()) ``` Each level has `price`, `quantity`, `liquidity` (that level), and `total_quantity` / `total_liquidity` (cumulative), best first. ### ticker A `ticker` push whenever a market's last price, top of book, volume or open interest moves. Filter by `sports` and `competitions`: ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: async with client.websocket() as ws: ticker = await ws.ticker(sports=["Baseball"]) print(ticker.reply) await ticker.select_filters(sports=None, competitions=["MLB"]) try: msg = await ticker.next(timeout=10) p = msg.payload print(p["market_symbol"], p["last_traded_price"], p["best_bid"], p["best_offer"]) except asyncio.TimeoutError: print("no ticker change in 10 s") asyncio.run(main()) ``` ### trades Every execution on the exchange, anonymised. `action` is the taker's side. This is not your fills: see [fills](#fills). ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: async with client.websocket() as ws: trades = await ws.trades() print(trades.reply) try: msg = await trades.next(timeout=10) print(msg.payload["market_symbol"], msg.payload["action"], msg.payload["quantity"], "@", msg.payload["price"]) except asyncio.TimeoutError: print("no trade in 10 s") asyncio.run(main()) ``` Filter with `market_ids=` and `event_ids=`; change them later with `select_filters(...)`. ### markets `market_created` and `market_updated` for every market. The payload maps market id to a market object; `market_updated` carries only `market_id`, the timestamps and the fields that changed. Prices arrive in cents on the wire and the SDK converts them to dollar strings, the same format as everywhere else. ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: async with client.websocket() as ws: markets = await ws.markets(rule_filters=None, message_types=["market_updated"]) print("rules you can filter on:", len(markets.reply["available_rules"])) try: msg = await markets.next(timeout=15) for market_id, change in msg.payload.items(): print(msg.event, market_id, change) except asyncio.TimeoutError: print("no market change in 15 s") asyncio.run(main()) ``` `select_rule_filters([...])` and `select_message_types([...])` change the filters without rejoining. ### market_stats A price series per market, for charts. The history is in the join reply; `market_stats` pushes changed buckets (upsert by `timestamp_us`), and `market_stats_snapshot` replaces a series. `price_percent` is a percent of `max_price`, not money. ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: ids = [m.market_id for m in await client.markets(status="open", limit=2)] async with client.websocket() as ws: stats = await ws.market_stats(ids, range="week") for series in stats.reply["markets"]: print(series["market_id"], len(series["points"]), "points") day = await stats.request_series(ids[:1], range="day") print("day range:", day["range"]) asyncio.run(main()) ``` ### market_updates `created` and `updated` for the markets you watch, and nothing until you do. Watches are sent again automatically after a reconnect. Prices are converted from cents to dollar strings. ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: ids = [m.market_id for m in await client.markets(status="open", limit=3)] async with client.websocket() as ws: updates = await ws.market_updates(watch=ids) print("watching", updates.watches) try: msg = await updates.next(timeout=15) print(msg.event, msg.payload["market_id"], msg.payload) except asyncio.TimeoutError: print("no update in 15 s") asyncio.run(main()) ``` ## Account channels Scoped to you. The SDK builds the topic (`orders:`), fetching your user id from `GET /api/v1/me` the first time it needs it. ### orders `all_orders` on join, then `new_open_order` (the whole order) each time one is accepted or filled. ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: market = None async for m in client.iter_markets(status="open", trading=True, sort_by="event_start", sort_direction="desc"): if m.event_status == "scheduled": market = m break async with client.websocket() as ws: orders = await ws.orders(market_ids=[market.market_id]) await orders.wait_snapshot() order = await client.place_order(market.market_id, "buy", "limit", price="0.01", quantity="1") while True: msg = await orders.next(timeout=15) if msg.event == "new_open_order" and msg.payload["id"] == order.id: print("pushed", msg.payload["status"], msg.payload["price"]) break await client.cancel_order(order.id) asyncio.run(main()) ``` `market_ids=` filters the snapshot and every push; `select_market_ids(None)` clears it. For cancel-on-disconnect, join with `cancel_on_disconnect=True` and optionally `ping_timeout` in milliseconds; the SDK pings at 60% of the granted timeout. See [Cancel-on-disconnect](/sdks/python/trading#cancel-on-disconnect). ### fills `all_trades` on join, then one `trade` per execution, including trades that later settle or are cancelled. ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: async with client.websocket() as ws: fills = await ws.fills(on_message=lambda m: print(m.event)) snap = await fills.wait_snapshot() trades = snap["all_trades"]["trades"] print(len(trades), "fills in the snapshot") for t in trades[:5]: print(t["id"], t["action"], t["filled"], "@", t["price"], "fee", t["total_fee"]) asyncio.run(main()) ``` ### positions `all_positions` on join, then `updated_positions` carrying only the positions that changed. ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: async with client.websocket() as ws: positions = await ws.positions(on_message=lambda m: print(m.event, len(m.payload["positions"]))) await positions.wait_snapshot() asyncio.run(main()) ``` ### settlements `new_settlements` whenever a market you hold settles. No snapshot; history is `client.settlements()`. ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: async with client.websocket() as ws: settlements = await ws.settlements() print(settlements.reply) try: msg = await settlements.next(timeout=5) print(msg.payload["settlements"]) except asyncio.TimeoutError: print("nothing settled in 5 s") asyncio.run(main()) ``` ### balances `balances` on join, then `update` when your balance changes and `payment_update` for payments. Pass `account_id=` to watch another account you own. ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: async with client.websocket() as ws: balances = await ws.balances() b = (await balances.wait_snapshot())["balances"] print("available", b["available_balance"], "cash", b["account_balance"]) asyncio.run(main()) ``` Balance pushes follow your activity (orders, fills, settlements, payments), not price moves. ### account Everything the five channels above carry, on one join: four snapshots, then the same events under the same names. Do not also join a per-type channel (you would get each message twice), and use `orders` instead if you need cancel-on-disconnect. ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: async with client.websocket() as ws: account = await ws.account() snap = await account.wait_snapshot() print(sorted(snap)) print("available", snap["balances"]["available_balance"]) asyncio.run(main()) ``` ### user_info `user_updated` right after the join, then whenever your profile changes. ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: async with client.websocket() as ws: info = await ws.user_info() profile = (await info.wait_snapshot())["user_updated"] print(profile["userStatus"], profile["firstName"]) asyncio.run(main()) ``` ## Other channels `join(topic, payload)` joins any documented topic, and `push(event, payload)` sends it a control event and returns the reply. For example, [event volume](/websockets/channels/events/) and the [order slip](/websockets/channels/order-slip/): ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: markets = await client.markets(status="open", trading=True, limit=3) async with client.websocket() as ws: events = await ws.join("events", {"event_ids": [m.event_id for m in markets]}) print(events.reply["events"]) slip = await ws.join(f"order_slip:{await ws.user_id()}") m = markets[0] reply = await slip.push( "add_order", {"market_id": m.market_id, "qty": 1, "side": "buy", "limit_price": 0.5, "max_price": float(m.max_price)}, ) print("ref", reply["ref"]) msg = await slip.next(timeout=10) print(msg.event, msg.payload["updates"][0]["risk_with_fee"]) asyncio.run(main()) ``` The order slip takes numbers, not strings, as its channel page describes; it costs an order and places nothing. ## Reconnects The SDK: - sends a heartbeat on the `phoenix` topic every 25 s (the server closes a socket silent for 60 s), and reconnects if one goes unanswered; - pings each joined channel every 30 s (`channel_ping_interval`), which also keeps the session behind `orders` and `account` alive; - on `orders` with cancel-on-disconnect, pings at 60% of the granted `ping_timeout`, a separate and much shorter deadline; - after a drop, reconnects with backoff, signs the handshake again, and rejoins every channel with its current filters and watches. Snapshots arrive again after the rejoin. What it cannot do is know what you missed while disconnected. Pass `on_reconnect` and call `orders()` (and anything else you show) again there: ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: async def resync(): open_orders = await client.orders(status=["open", "delayed"]) print("reconnected; open orders:", len(open_orders)) async with client.websocket(on_reconnect=resync) as ws: await ws.orders() await ws.fills() print("connected:", ws.connected, "reconnects so far:", ws.reconnects) asyncio.run(main()) ``` Channels are not ordered relative to each other: a fill can arrive before the order update that explains it. Key off ids. The intervals, the reconnect backoff and the join timeout are keyword arguments to `client.websocket()` and `STXWebSocket`: ```python import asyncio from stx import AsyncSTX, ReconnectPolicy async def main(): async with AsyncSTX() as client: ws = client.websocket( heartbeat_interval=25.0, # seconds between socket heartbeats channel_ping_interval=30.0, # ping on every joined channel; None turns it off reconnect_policy=ReconnectPolicy(initial_backoff=0.5, max_backoff=30), join_timeout=10.0, ) async with ws: await ws.ticker() print("connected:", ws.connected) asyncio.run(main()) ``` ## Errors A refused join raises `STXChannelException` with the server's reason in `.reply`, for example `{"reason": "market_ids_required"}` or `{"reason": "unauthorized"}`. So does a refused control event such as `select_market_ids([])` on `orderbook`. ## Keep a worker running `run_forever()` blocks until `close()` is called or reconnects give up: ```python import asyncio from stx import AsyncSTX async def main(): async with AsyncSTX() as client: ids = [m.market_id for m in await client.markets(status="open", limit=5)] async with client.websocket() as ws: await ws.orderbook(ids, on_message=lambda m: print("book", m.payload["market_id"])) await ws.fills(on_message=lambda m: print("fills", m.event)) asyncio.get_running_loop().call_later(10, lambda: asyncio.ensure_future(ws.close())) await ws.run_forever() asyncio.run(main()) ``` --- # TypeScript SDK > The TypeScript SDK for the STX exchange. Source: https://docs.stxapp.io/sdks/typescript/ `@stxapp/stx-typescript` is the TypeScript SDK for the STX exchange. Read markets, place and cancel orders, follow your account, and stream the [WebSocket channels](/websockets/), with every request signed by your API key and every response typed. Response fields are described in the [API reference](/api/rest/). ```bash npm install @stxapp/stx-typescript ``` ```ts import { STX } from "@stxapp/stx-typescript"; // Reads your API key and exchange from STX_* environment variables or ~/.stx/credentials. const client = new STX(); const me = await client.me(); const page = await client.markets({ status: "open", limit: 5 }); console.log(me.user_id, page.items.map((m) => m.symbol)); ``` [Authentication](/sdks/typescript/authentication/) shows how to give the client your API key, and [Environments](/environments/) lists the exchanges and where to get a key. ## Prices and quantities are strings Amounts come back as decimal strings, exactly as the API sends them (`order.price === "0.5600"`), and orders take strings too: `{ price: "0.56", quantity: "2" }`. Passing a number throws a `TypeError` before anything is sent. Use a decimal library for arithmetic, not `Number` or `parseFloat`. Response objects keep the API's snake_case field names (`order.client_order_id`); method options are camelCase (`clientOrderId`). ## Where to go next - [Installation](/sdks/typescript/installation/): Install and import. - [Authentication](/sdks/typescript/authentication/): Pass your API key to the client. - [Environments](/sdks/typescript/environments/): Pick the exchange to connect to. - [Markets](/sdks/typescript/markets/): Markets, events and pagination. - [Trading](/sdks/typescript/trading/): Place and cancel orders. - [Portfolio](/sdks/typescript/portfolio/): Balance, positions, fills and history. - [WebSockets](/sdks/typescript/websockets/): Stream channels and keep a live account view. - [Errors & retries](/sdks/typescript/errors-and-retries/): Typed errors and what the client retries. - [OAuth apps](/isv/typescript-sdk/): Act for other STX members from your app, with the OAuth client. - [API reference](/sdks/typescript/reference/): Every method and what it returns. - [Example scripts](https://github.com/stxapp/stx-typescript-demo): List markets, place and cancel an order, stream live data. Released under the MIT license. --- # Authentication > Pass your API key to the client: a credentials profile, environment variables, or constructor options. Source: https://docs.stxapp.io/sdks/typescript/authentication/ Building an app that acts for other STX members? See [ISV](/isv/). The client needs your API key ID and its Ed25519 private key, as a path to the PEM file or as the PEM text. Creating a key, how requests are signed, and why a signature fails are covered on [Authentication](/api/authentication/). There are three ways to hand the key to the client. When more than one is set, constructor options win over environment variables, and environment variables win over the profile. ## A credentials profile Add a profile to `~/.stx/credentials`: ```ini [default] region = us env = demo key_id = your-key-id key_file = ~/.stx/stx-key.pem ``` `region` and `env` pick the exchange; the values for each are listed under [Environments](/sdks/typescript/environments/). `new STX()` with no profile reads `STX_PROFILE`, then the `[default]` section, so the examples in this guide work as written: ```ts import { STX } from "@stxapp/stx-typescript"; const client = new STX(); ``` An API key works on one exchange only. To keep keys for more than one, add a section per key under any name you like, and pick one with `profile`: ```ts const client = new STX({ profile: "my-demo" }); ``` ## Environment variables ```bash export STX_REGION=us STX_ENV=demo export STX_KEY_ID=your-key-id export STX_PRIVATE_KEY=~/.stx/stx-key.pem # a path, or the PEM text ``` ```ts const client = new STX(); ``` `STX_HOST`, `STX_PROFILE` and `STX_CREDENTIALS` (the path of the credentials file) are also read. ## Constructor options ```ts import { Environments, STX } from "@stxapp/stx-typescript"; const client = new STX({ environment: Environments.USDemo, keyId: process.env.MY_KEY_ID, privateKey: process.env.MY_KEY_PEM, // a path, PEM text, bytes, or a KeyObject }); ``` If the key lives in an HSM or KMS, pass `signer: async (message) => signature` instead of `privateKey`; it returns the raw 64-byte Ed25519 signature. ## Check it works ```ts const me = await client.me(); console.log(me.user_id, me.scope); // scope: "read_only" or "read_write" ``` A bad key, or a machine clock that has drifted, throws `STXAuthenticationException`. --- # Environments > Select the STX exchange the client connects to. Source: https://docs.stxapp.io/sdks/typescript/environments/ The exchanges, their hosts and which one to start with are listed on [Environments](/environments/). In the SDK, pick one with `environment`, or with `region` and `env` (as options, as `STX_REGION` and `STX_ENV`, or in a credentials profile): | Exchange | `environment` | `region` | `env` | |---|---|---|---| | STX US demo | `Environments.USDemo` | `"us"` | `"demo"` | | STX Ontario demo | `Environments.OntarioDemo` | `"ontario"` | `"demo"` | | STX Ontario | `Environments.OntarioProduction` | `"ontario"` | `"production"` | ```ts import { Environments, STX } from "@stxapp/stx-typescript"; const client = new STX({ environment: Environments.USDemo, keyId, privateKey }); // same exchange: const same = new STX({ region: "us", env: "demo", keyId, privateKey }); ``` An API key works on one exchange only, so keep one credentials profile per exchange and switch with `profile`. To reach any other host, such as a local server, pass `host: "http://localhost:4000"` (or set `STX_HOST`). The URL's scheme is kept for both HTTP and the socket. --- # Errors & retries > The typed errors the SDK throws, and what it retries. Source: https://docs.stxapp.io/sdks/typescript/errors-and-retries/ What each endpoint can return is in the [API reference](/api/rest/), and request limits are in [Rate limits](/concepts/rate-limits/). The SDK turns every error response into a typed exception, so you can catch the case you care about: ```ts import { STX, STXNotFoundException, STXRateLimitException } from "@stxapp/stx-typescript"; const client = new STX(); try { await client.order(orderId); } catch (err) { if (err instanceof STXNotFoundException) { console.log("no such order"); } else if (err instanceof STXRateLimitException) { console.log("slow down, retry after", err.retryAfter, "s"); } else { throw err; } } ``` ## Exceptions | Exception | Thrown for | |---|---| | `STXValidationException` | 400 | | `STXAuthenticationException` | 401 | | `STXForbiddenException` | 403 | | `STXNotFoundException` | 404 | | `STXRejectedException` | 422 | | `STXGeoLocationException` | 422 from geo-fencing (`reason`) | | `STXRateLimitException` | 429 (`retryAfter`, in seconds) | | `STXServerException` | 5xx | | `STXTransportException` | no response: connection, DNS, TLS or timeout | | `STXConfigException` | a client misconfiguration, such as a missing key | | `STXChannelException`, `STXTimeoutException` | a refused or unanswered WebSocket join | All of them extend `STXException`. API errors extend `STXAPIException` and carry `message` (the API's error text), `statusCode`, `body`, `method` and `path`. ## What is retried | Failure | `GET`, `DELETE` | `POST` | |---|---|---| | 429 | retried after `Retry-After` | retried after `Retry-After` | | 5xx | retried | not retried | | No response | retried | not retried | | Any other 4xx | not retried | not retried | A failed `POST` may still have placed the order, so it is never resent; see [Avoid duplicate orders](/sdks/typescript/trading#avoid-duplicate-orders). Each attempt is signed again. The default is 3 attempts with exponential backoff from 500 ms, capped at 30 s. ```ts import { NO_RETRY, RetryPolicy, STX } from "@stxapp/stx-typescript"; const patient = new STX({ retry: new RetryPolicy({ maxAttempts: 5 }), timeoutMs: 10_000 }); const noRetries = new STX({ retry: NO_RETRY }); ``` ## Logging requests `onResponse` is called after every attempt with the operation, status, duration and bodies: ```ts const client = new STX({ onResponse: (e) => console.log(e.method, e.path, e.status, `${e.durationMs} ms`), }); ``` --- # Installation > Install @stxapp/stx-typescript and import it. Source: https://docs.stxapp.io/sdks/typescript/installation/ ```bash npm install @stxapp/stx-typescript ``` ```bash pnpm add @stxapp/stx-typescript ``` ```bash yarn add @stxapp/stx-typescript ``` ```bash bun add @stxapp/stx-typescript ``` Requires Node 18 or newer (or Bun). API-key signing uses Node's `crypto` module, so run the client on a server, not in a browser. ## Importing ```ts import { STX } from "@stxapp/stx-typescript"; ``` CommonJS works too: ```js const { STX } = require("@stxapp/stx-typescript"); ``` ## Check the install ```bash node -e 'import("@stxapp/stx-typescript").then((m) => console.log(m.VERSION))' ``` Pin an exact version in production. Before 1.0.0, a minor release can change the API. --- # Markets > Markets, events, filters, and cursor pagination. Source: https://docs.stxapp.io/sdks/typescript/markets/ ## Markets ```ts import { STX } from "@stxapp/stx-typescript"; const client = new STX(); const page = await client.markets({ status: "open", limit: 5 }); for (const market of page) { console.log(market.symbol, "last", market.last_traded_price, "bid", market.bids?.[0]?.price); } console.log("more pages:", page.hasMore); ``` | Option | Meaning | |---|---| | `marketIds`, `eventIds` | One id or a list | | `status` | One or a list, lowercase, e.g. `"open"`; see [Market status](/concepts/market-status/) | | `trading` | `true` for only the markets accepting orders now, `false` for only those that are not | | `sports`, `competitions` | One or a list | | `sortBy`, `sortDirection` | e.g. `sortBy: "event_start"`, `sortDirection: "desc"` | | `limit`, `cursor` | Page size and the cursor from the previous page | Filters combine with AND; a list filter matches any of its values. `client.market(id)` fetches one market and throws `STXNotFoundException` when there is none. ## Pagination A list method returns one `Page`: `page.items`, `page.cursor` (`null` on the last page), `page.hasMore` and `page.length`. A `Page` is iterable. To walk every page, use the `iter*` twin, which follows the cursor for you: ```ts import { STX } from "@stxapp/stx-typescript"; const client = new STX(); let market; for await (const m of client.iterMarkets({ trading: true, sortBy: "event_start", sortDirection: "desc" })) { if (m.event_status === "scheduled") { market = m; break; } } console.log(market?.market_id, market?.symbol, market?.max_price); ``` `trading: true` returns only the markets that accept orders now. Or page by hand: ```ts let page = await client.markets({ status: "open", limit: 100 }); while (true) { for (const m of page) console.log(m.symbol); if (!page.hasMore) break; page = await client.markets({ status: "open", limit: 100, cursor: page.cursor! }); } ``` ## Events ```ts const events = await client.events({ limit: 10 }); for (const e of events) console.log(e.event_id, e.title, e.status); for await (const e of client.iterEvents({ promoted: true })) console.log(e.title); ``` `events()` filters on `eventIds`, `sports`, `competitions`, `eventTypes`, `title`, `status` and `promoted`, and sorts with `sortBy` and `sortDirection`. ## Creating markets An app that STX has granted the `markets.write` scope can create player-prop markets on scheduled events, from an address on its IP allow-list. See [Creating markets](https://docs.stxapp.io/isv/creating-markets/) for the setup and the full list of outcomes. ```ts import { AppScopes, OAuthClient } from "@stxapp/stx-typescript/oauth"; import { Stats } from "@stxapp/stx-typescript"; const oauth = new OAuthClient({ clientId, clientSecret }); const stx = oauth.appClient([AppScopes.EVENTS, AppScopes.MARKETS_WRITE]); const roster = await stx.eventPlayers(eventId); const player = roster.teams[0].players[0]; const results = await stx.createMarkets(eventId, [ { playerId: player.player_id, stat: Stats.Basketball.POINTS, line: 18.5 }, { playerId: player.player_id, stat: Stats.Basketball.REBOUNDS, line: 4.5 }, ]); for (const r of results) console.log(r.status, r.status === "rejected" ? r.reason : r.market_id); ``` - **Lines** must be positive and end in `.5`: `18.5` is accepted; `18`, `18.25`, `0` and `"18.5"` come back `rejected` with `invalid_line`. - **Stats**: an event accepts the stats `eventPlayers()` (or `statsFor(event)`) returns for it. `Stats` lists them by sport for autocomplete. - **Results** come back one per market, in order: `created`, `exists` (already listed, same `market_id`) or `rejected` with a `reason`. Resending is safe. - **`validate: true`** checks players, stats and lines before sending, and returns failures as `rejected` without a request. It reads the event's players, so the token also needs `events`. - Up to `MAX_MARKETS_PER_REQUEST` (25) markets per call; more throws `TypeError` before sending. ## Live prices `bids` and `offers` are a snapshot of the top of the book. To follow prices as they move, stream them; see [WebSockets](/sdks/typescript/websockets/). --- # Portfolio > Read your balance, positions, fills, settlements and account history. Source: https://docs.stxapp.io/sdks/typescript/portfolio/ These calls read your own account. The fields are described in the [API reference](/api/rest/). To keep these figures current without polling, use the [live account view](/sdks/typescript/websockets#live-account-view). ```ts import { STX } from "@stxapp/stx-typescript"; const client = new STX(); const balance = await client.balance(); console.log(balance.available_balance); for (const p of await client.positions()) { console.log(p.market_id, p.position); } for await (const fill of client.iterFills({ marketIds: [marketId] })) { console.log(fill.trade_id, fill.action, fill.filled, "@", fill.price); } const settlements = await client.settlements({ limit: 50 }); ``` | Method | Filters | |---|---| | `balance()` | none | | `positions()` | `marketIds` | | `fills()` | `marketIds`, `orderIds`, `status` | | `settlements()` | `marketIds`, `type` | | `accountMarketStats()` | `marketIds`, `eventIds`, `sports`, `competitions`, `fromTime`, `toTime`, `excludeZeroSettlements` | | `deposits()`, `withdrawals()`, `adjustments()`, `fees()`, `loyalty()` | `limit`, `cursor` | Every list method returns one page and has an `iter*` twin (`iterFills`, `iterSettlements`, `iterDeposits`, ...) that walks all pages. See [pagination](/sdks/typescript/markets#pagination). --- # API reference > Every class and method in @stxapp/stx-typescript, generated from the source. Source: https://docs.stxapp.io/sdks/typescript/reference/ {/* Generated from the TypeScript source by tools/gen-reference.mjs. Do not edit by hand. */} Generated from the source of `@stxapp/stx-typescript`. For a walkthrough, start with the [overview](/sdks/typescript/). | Page | Covers | |---|---| | [STX](/sdks/typescript/reference/stx/) | The client: every method, its parameters and what it returns. | | [STXWebSocket](/sdks/typescript/reference/stx-websocket/) | The WebSocket client: connecting, joining channels and reconnecting. | | [Channel](/sdks/typescript/reference/channel/) | A joined WebSocket channel and the messages it delivers. | | [AccountView](/sdks/typescript/reference/account-view/) | The live view of your balance, open orders, fills and positions. | | [Page and retry policies](/sdks/typescript/reference/helpers/) | Pagination results and the retry and reconnect policies. | | [Errors](/sdks/typescript/reference/errors/) | Every exception the SDK throws. | --- # AccountView > The live view of your balance, open orders, fills and positions. Source: https://docs.stxapp.io/sdks/typescript/reference/account-view/ {/* Generated from the TypeScript source by tools/gen-reference.mjs. Do not edit by hand. */} A live view of your account. Get one with `ws.accountView()`: ```ts const ws = await client.websocket().connect(); const view = await ws.accountView({ onChange: (c) => render(c.view.state()) }); view.balance?.available_balance; // "123.4500" view.openOrders; // [{id, market_id, status, price, ...}] ``` ## Properties | Property | Type | Description | |---|---|---| | `onChange` | `(change: AccountChange) => void \| undefined` | | | `seeds` | `Record` | Join snapshots applied so far, per channel: 1 after the first seed, more after reconnects. | | `ws` | `STXWebSocket` | | | `balance` | `Record \| null` | The latest balance summary, or `null` before the first snapshot. | | `closed` | `boolean` | | | `fills` | `Record[]` | Fills, newest first. | | `openOrders` | `Record[]` | Open orders, newest first. | | `positions` | `Record[]` | Positions, one per market. | | `ready` | `boolean` | True once every channel has delivered its join snapshot. | ## Methods ### `apply()` ```ts apply(msg: ChannelMessage | { channel?: string; event: string; payload: any }): AccountChange | undefined ``` Apply one channel message. The view calls this for the channels it joined; call it yourself to feed messages from an `account:` channel or a channel you joined elsewhere. Unknown events are ignored. | Parameter | Type | Description | |---|---|---| | `msg` | `ChannelMessage \| { channel?: string; event: string; payload: any }` | | Returns `AccountChange \| undefined`. ### `close()` ```ts close(): Promise ``` Leave the four channels. The socket stays open. Returns `Promise`. ### `state()` ```ts state(): AccountState ``` The whole view as plain data. Returns `AccountState`. --- # Channel > A joined WebSocket channel and the messages it delivers. Source: https://docs.stxapp.io/sdks/typescript/reference/channel/ {/* Generated from the TypeScript source by tools/gen-reference.mjs. Do not edit by hand. */} ## `Channel` One joined topic. ### Properties | Property | Type | Description | |---|---|---| | `closed` | `boolean` | | | `joined` | `boolean` | | | `joinPayload` | `Record` | What the next (re)join sends; updated by the `select*` and `watch` helpers so a reconnect keeps them. | | `joinRef` | `string \| null` | | | `name` | `string` | The topic without the user id, e.g. `orders`. | | `onMessage` | `MessageHandler \| undefined` | | | `pingIntervalMs` | `number \| null` | | | `reply` | `Record` | The `response` object from the latest join reply, e.g. `{selected_market_ids: null}`. | | `snapshots` | `Record` | The latest snapshot payload per snapshot event name. | | `topic` | `string` | The wire topic, e.g. `orders:`. | | `watches` | `string[]` | Market ids watched on `market_updates`; re-sent after a reconnect. | ### Methods #### `[asyncIterator]()` {/* sdk:Channel.[asyncIterator] */} ```ts [asyncIterator](): AsyncIterator ``` Returns `AsyncIterator`. #### `leave()` ```ts leave(): Promise ``` Leave the topic. The channel stops receiving and its iterator ends. Returns `Promise`. #### `next()` ```ts next(timeoutMs?: number | null): Promise ``` The next message on this channel. Rejects with `STXChannelException` if none arrives within `timeoutMs` or the channel closes. | Parameter | Type | Description | |---|---|---| | `timeoutMs` (optional) | `number \| null` | | Returns `Promise`. #### `ping()` ```ts ping(): Promise ``` Channel `ping`. On `orders` with cancel-on-disconnect armed this resets the cancel deadline; on `account` and `orders` it keeps the session alive. Returns `Promise`. #### `push()` ```ts push(event: string, payload?: unknown, timeoutMs?: number): Promise ``` Send `event` on this channel and resolve to the reply's `response`. Rejects when the reply status is `error`. | Parameter | Type | Description | |---|---|---| | `event` | `string` | | | `payload` (optional) | `unknown` | | | `timeoutMs` (optional) | `number` | | Returns `Promise`. #### `requestSeries()` ```ts requestSeries(marketIds: readonly string[], range?: string): Promise ``` `market_stats`: fetch history at `range` (`day`, `week`, `month`, `all`) without changing the subscription. | Parameter | Type | Description | |---|---|---| | `marketIds` | `readonly string[]` | | | `range` (optional) | `string` | | Returns `Promise`. #### `requestSnapshot()` ```ts requestSnapshot(): Promise ``` `market:`: push the market's current `market_update` and `order_book_update` again now (after a gap, or to re-read a field). Returns `Promise`. #### `selectFilters()` ```ts selectFilters(filters: Record): Promise ``` `ticker` (`sports`, `competitions`) and `trades` (`market_ids`, `event_ids`): change filters without rejoining. | Parameter | Type | Description | |---|---|---| | `filters` | `Record` | | Returns `Promise`. #### `selectMarketIds()` ```ts selectMarketIds(marketIds: readonly string[] | null): Promise ``` Change the `market_ids` filter without rejoining. On `orders`, `fills`, `positions`, `settlements` and `account`, `null` clears the filter; `orderbook` and `market_stats` require at least one id. | Parameter | Type | Description | |---|---|---| | `marketIds` | `readonly string[] \| null` | | Returns `Promise`. #### `selectMessageTypes()` ```ts selectMessageTypes(messageTypes: readonly string[] | null): Promise ``` `markets`: receive `market_created`, `market_updated` or both. | Parameter | Type | Description | |---|---|---| | `messageTypes` | `readonly string[] \| null` | | Returns `Promise`. #### `selectRuleFilters()` ```ts selectRuleFilters(ruleFilters: readonly string[] | null): Promise ``` `markets`: change the `rules` filter; `null` disables it. | Parameter | Type | Description | |---|---|---| | `ruleFilters` | `readonly string[] \| null` | | Returns `Promise`. #### `waitSnapshot()` ```ts waitSnapshot(timeoutMs?: number | null): Promise> ``` Wait for the state-on-join and return it as `{event: payload}`. `orders` gives `{all_orders: {...}}`; `account` waits for all four of its snapshots. `market_stats` and `market:` return the join reply, which carries the series or the whole market. Channels with no snapshot (`settlements`, `ticker`, `trades`, `orderbook`, `markets`, `market_updates`) throw. | Parameter | Type | Description | |---|---|---| | `timeoutMs` (optional) | `number \| null` | | Returns `Promise>`. #### `watch()` ```ts watch(marketIds: readonly string[]): Promise ``` `market_updates`: start receiving `created`/`updated` for these markets. Re-sent automatically after a reconnect. | Parameter | Type | Description | |---|---|---| | `marketIds` | `readonly string[]` | | Returns `Promise`. ## `ChannelMessage` One pushed frame. ### Properties | Property | Type | Description | |---|---|---| | `event` | `string` | | | `joinRef` | `string \| null` | | | `payload` | `any` | The event's JSON as documented for the channel; money and quantities are strings. | | `ref` | `string \| null` | | | `topic` | `string` | The wire topic, e.g. `orders:`. | | `channel` | `string` | The topic without the user id: `"orders"` for `orders:`. | | `isSnapshot` | `boolean` | True for the state-on-join events (`all_orders`, `balances`, ...). | --- # Errors > Every exception the SDK throws. Source: https://docs.stxapp.io/sdks/typescript/reference/errors/ {/* Generated from the TypeScript source by tools/gen-reference.mjs. Do not edit by hand. */} Every exception extends `STXException`. Catch the class you care about with `instanceof`. | Exception | Extends | When | |---|---|---| | [`STXException`](#stxexception) | `Error` | Base for every error the SDK throws. | | [`STXConfigException`](#stxconfigexception) | `STXException` | The client is misconfigured: unknown region, missing key, bad PEM. | | [`STXAPIException`](#stxapiexception) | `STXException` | The API answered with an error status. Base for the classes below. | | [`STXValidationException`](#stxvalidationexception) | `STXAPIException` | 400: a parameter or body field was rejected, e.g. `price is required`. | | [`STXAuthenticationException`](#stxauthenticationexception) | `STXAPIException` | 401: the signature, key ID or timestamp was not accepted. A machine clock more than 30 seconds off also lands here. | | [`STXForbiddenException`](#stxforbiddenexception) | `STXAPIException` | 403: the key is read-only, or the account may not do this. When the server names the scopes that would satisfy the call (in the `WWW-Authenticate` challenge), `requiredScopes` lists them. | | [`STXNotFoundException`](#stxnotfoundexception) | `STXAPIException` | 404: the resource does not exist, or belongs to another account. | | [`STXRejectedException`](#stxrejectedexception) | `STXAPIException` | 422: the exchange refused the request, e.g. insufficient funds or a closed market. | | [`STXGeoLocationException`](#stxgeolocationexception) | `STXRejectedException` | 422 from the exchange's geo-fencing: this request's IP address is not allowed to trade, or the geolocation packet sent with it was missing, invalid, expired or from another user. A subclass of `STXRejectedException`, so existing `catch` blocks still see it. | | [`STXRateLimitException`](#stxratelimitexception) | `STXAPIException` | 429: too many requests. `retryAfter` is seconds, when sent. | | [`STXServerException`](#stxserverexception) | `STXAPIException` | 5xx: the server failed. Retried by the default policy on GET and DELETE. | | [`STXTransportException`](#stxtransportexception) | `STXException` | The request never got an answer: connection, DNS, TLS or timeout. | | [`STXChannelException`](#stxchannelexception) | `STXException` | A channel join or channel message was refused by the server. `reply` holds the server's `response` object, for example `{reason: "unauthorized"}`. | | [`STXTimeoutException`](#stxtimeoutexception) | `STXChannelException` | No reply, message or snapshot arrived on a channel in time. A subclass of `STXChannelException`. | ## `STXException` Base for every error the SDK throws. | Property | Type | Description | |---|---|---| | `body` | `unknown` | | | `method` | `string \| undefined` | | | `path` | `string \| undefined` | | | `statusCode` | `number \| undefined` | | ## `STXConfigException` The client is misconfigured: unknown region, missing key, bad PEM. ## `STXAPIException` The API answered with an error status. Base for the classes below. ## `STXValidationException` 400: a parameter or body field was rejected, e.g. `price is required`. ## `STXAuthenticationException` 401: the signature, key ID or timestamp was not accepted. A machine clock more than 30 seconds off also lands here. ## `STXForbiddenException` 403: the key is read-only, or the account may not do this. When the server names the scopes that would satisfy the call (in the `WWW-Authenticate` challenge), `requiredScopes` lists them. | Property | Type | Description | |---|---|---| | `requiredScopes` | `readonly string[]` | | ## `STXNotFoundException` 404: the resource does not exist, or belongs to another account. ## `STXRejectedException` 422: the exchange refused the request, e.g. insufficient funds or a closed market. ## `STXGeoLocationException` 422 from the exchange's geo-fencing: this request's IP address is not allowed to trade, or the geolocation packet sent with it was missing, invalid, expired or from another user. A subclass of `STXRejectedException`, so existing `catch` blocks still see it. `reason` classifies it; `ipAddress` is the address the exchange saw, when its message names one. Never retried. | Property | Type | Description | |---|---|---| | `ipAddress` | `string \| undefined` | | | `reason` | `"ip_not_allowed" \| "packet_invalid" \| "packet_other_user" \| "packet_ip_mismatch" \| "location_not_allowed"` | | ## `STXRateLimitException` 429: too many requests. `retryAfter` is seconds, when sent. | Property | Type | Description | |---|---|---| | `retryAfter` | `number \| undefined` | | ## `STXServerException` 5xx: the server failed. Retried by the default policy on GET and DELETE. ## `STXTransportException` The request never got an answer: connection, DNS, TLS or timeout. ## `STXChannelException` A channel join or channel message was refused by the server. `reply` holds the server's `response` object, for example `{reason: "unauthorized"}`. | Property | Type | Description | |---|---|---| | `reply` | `unknown` | | | `topic` | `string` | | ## `STXTimeoutException` No reply, message or snapshot arrived on a channel in time. A subclass of `STXChannelException`. --- # Page and retry policies > Pagination results and the retry and reconnect policies. Source: https://docs.stxapp.io/sdks/typescript/reference/helpers/ {/* Generated from the TypeScript source by tools/gen-reference.mjs. Do not edit by hand. */} ## `Page` ### Constructor ```ts new Page(items: T[], cursor: string | null) ``` | Parameter | Type | Description | |---|---|---| | `items` | `T[]` | | | `cursor` | `string \| null` | | ### Properties | Property | Type | Description | |---|---|---| | `cursor` | `string \| null` | | | `items` | `T[]` | | | `hasMore` | `boolean` | True when another page exists. | | `length` | `number` | | ### Methods #### `[iterator]()` {/* sdk:Page.[iterator] */} ```ts [iterator](): Iterator ``` Returns `Iterator`. #### `at()` ```ts at(index: number): T | undefined ``` The item at `index`; negative counts from the end. | Parameter | Type | Description | |---|---|---| | `index` | `number` | | Returns `T \| undefined`. ## `RetryPolicy` How many times to try a call, and how long to wait between tries. The default is 3 attempts, 500 ms initial backoff doubling to a 30 s cap, with jitter. `new RetryPolicy({ maxAttempts: 1 })` (or `NO_RETRY`) turns retries off. ### Constructor ```ts new RetryPolicy(opts?: RetryPolicyOptions) ``` | Parameter | Type | Description | |---|---|---| | `opts` (optional) | `RetryPolicyOptions` | | | `opts.initialBackoffMs` (optional) | `number` | Milliseconds before the first retry. Default 500. | | `opts.jitter` (optional) | `boolean` | Randomise each wait between 50% and 100% of its value. Default true. | | `opts.maxAttempts` (optional) | `number` | Total attempts, including the first. Default 3. | | `opts.maxBackoffMs` (optional) | `number` | Cap on any one wait, in milliseconds. Default 30000. | | `opts.retryableExceptions` (optional) | `readonly ExceptionClass[]` | Exception classes eligible for a retry. | ### Properties | Property | Type | Description | |---|---|---| | `initialBackoffMs` | `number` | | | `jitter` | `boolean` | | | `maxAttempts` | `number` | | | `maxBackoffMs` | `number` | | | `retryableExceptions` | `readonly ExceptionClass[]` | | ### Methods #### `computeBackoffMs()` ```ts computeBackoffMs(attempt: number, exc: STXException): number ``` Milliseconds to wait after attempt `attempt` (1-based) failed. | Parameter | Type | Description | |---|---|---| | `attempt` | `number` | | | `exc` | `STXException` | | Returns `number`. #### `shouldRetry()` ```ts shouldRetry(exc: STXException, attempt: number, idempotent: boolean): boolean ``` Whether to try again after `exc` ended attempt number `attempt`. | Parameter | Type | Description | |---|---|---| | `exc` | `STXException` | | | `attempt` | `number` | | | `idempotent` | `boolean` | | Returns `boolean`. ## `ReconnectPolicy` Backoff between reconnect attempts. ### Constructor ```ts new ReconnectPolicy(opts?: ReconnectPolicyOptions) ``` | Parameter | Type | Description | |---|---|---| | `opts` (optional) | `ReconnectPolicyOptions` | | | `opts.initialBackoffMs` (optional) | `number` | | | `opts.jitter` (optional) | `boolean` | | | `opts.maxAttempts` (optional) | `number \| null` | `null` (the default) never gives up. | | `opts.maxBackoffMs` (optional) | `number` | | ### Properties | Property | Type | Description | |---|---|---| | `initialBackoffMs` | `number` | | | `jitter` | `boolean` | | | `maxAttempts` | `number \| null` | | | `maxBackoffMs` | `number` | | ### Methods #### `delayMs()` ```ts delayMs(attempt: number): number ``` | Parameter | Type | Description | |---|---|---| | `attempt` | `number` | | Returns `number`. --- # STX > The client: every method, its parameters and what it returns. Source: https://docs.stxapp.io/sdks/typescript/reference/stx/ {/* Generated from the TypeScript source by tools/gen-reference.mjs. Do not edit by hand. */} The STX client. Configuration comes from the options, then environment variables, then a profile in ~/.stx/credentials: ```ts const client = new STX(); const me = await client.me(); const page = await client.markets({ status: "open", limit: 10 }); ``` ## Constructor ```ts new STX(opts?: STXOptions) ``` | Parameter | Type | Description | |---|---|---| | `opts` (optional) | `STXOptions` | | | `opts.env` (optional) | `string \| null` | `"production"` or `"demo"` (`"prod"` and `"live"` are accepted). | | `opts.environment` (optional) | `string \| STXEnvironment` | A published environment, `Environments.OntarioDemo`, or its name, `"ontario-demo"`. Instead of `region` + `env`; `host` still wins. | | `opts.fetch` (optional) | `FetchLike` | Inject a fetch implementation (tests, proxies). | | `opts.geoLocation` (optional) | `GeoLocationProvider` | A geolocation packet, or a function returning one, sent as `geo_location` with every order write. Only needed where the exchange is geo-fenced and this machine's IP address is not whitelisted for the account. | | `opts.host` (optional) | `string \| null` | A hostname or URL, overriding region/env: `"demo.stxapp.ca"`, `"http://localhost:4000"`. | | `opts.keyId` (optional) | `string \| null` | The API key ID. | | `opts.onResponse` (optional) | `(event: ResponseEvent) => void` | Called once per HTTP attempt, after it completes or fails: an audit or activity log hook. Never throws into the request; an exception here is swallowed. | | `opts.privateKey` (optional) | `PrivateKeyInput \| null` | The Ed25519 private key: PEM text, a path to a PEM file, PEM bytes, or a KeyObject. | | `opts.profile` (optional) | `string \| null` | A section of ~/.stx/credentials to read. | | `opts.region` (optional) | `string \| null` | A known region, e.g. `"ontario"` or `"us"`. With `env`, picks the host. | | `opts.retry` (optional) | `RetryPolicy` | A `RetryPolicy`; `NO_RETRY` disables retries. | | `opts.signer` (optional) | `Signer` | Instead of `privateKey`: signs message bytes, for keys in an HSM or KMS. | | `opts.timeoutMs` (optional) | `number` | Milliseconds per HTTP request. Default 30000. | | `opts.verifyTls` (optional) | `boolean \| null` | Set false only for a local server. Honoured by the WebSocket; for HTTP requests, pass a `fetch` whose dispatcher skips verification (Node's global fetch has no per-call switch). | ## Properties | Property | Type | Description | |---|---|---| | `baseUrl` | `string` | | | `credentials` | `ApiKeyCredentials \| undefined` | The API key, when one is configured. | | `geoLocation` | `GeoLocationProvider \| undefined` | | | `onResponse` | `(event: ResponseEvent) => void \| undefined` | | | `profile` | `string` | | | `retry` | `RetryPolicy` | | | `socketUrl` | `string` | | | `timeoutMs` | `number` | | | `verifyTls` | `boolean` | | ## Methods ### `acceptTerms()` ```ts acceptTerms(deviceId: string, opts?: AcceptTermsOptions): Promise ``` `POST /api/v1/tnc/accept`: accept the current terms. Resolves to the server's message. | Parameter | Type | Description | |---|---|---| | `deviceId` | `string` | | | `opts` (optional) | `AcceptTermsOptions` | | | `opts.acceptHouseRules` (optional) | `boolean` | | | `opts.acceptPrivacy` (optional) | `boolean` | | | `opts.acceptTerms` (optional) | `boolean` | | Returns `Promise`. ### `accountMarketStats()` ```ts accountMarketStats(query?: MarketStatsQuery): Promise> ``` `GET /api/v1/account/market_stats`: your exposure and P&L per market. Not the `market_stats` WebSocket channel, which carries price history. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `MarketStatsQuery` | | | `query.competitions` (optional) | `StrList` | | | `query.cursor` (optional) | `string` | | | `query.eventIds` (optional) | `StrList` | | | `query.excludeZeroSettlements` (optional) | `boolean` | | | `query.fromTime` (optional) | `number` | | | `query.limit` (optional) | `number` | | | `query.marketIds` (optional) | `StrList` | | | `query.sports` (optional) | `StrList` | | | `query.toTime` (optional) | `number` | | Returns `Promise>`. ### `adjustments()` ```ts adjustments(query?: PageQuery): Promise> ``` `GET /api/v1/portfolio/adjustments`: manual balance adjustments. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `PageQuery` | | | `query.cursor` (optional) | `string` | | | `query.limit` (optional) | `number` | | Returns `Promise>`. ### `balance()` ```ts balance(): Promise ``` `GET /api/v1/account/balance`: balance, liabilities and fee schedule. The same object the `balances` channel pushes. Returns `Promise`. ### `cancelAllOrders()` ```ts cancelAllOrders(opts?: GeoOption): Promise ``` `DELETE /api/v1/orders/all`: cancel everything resting on the account. | Parameter | Type | Description | |---|---|---| | `opts` (optional) | `GeoOption` | | | `opts.geoLocation` (optional) | `string \| null` | Overrides the client's `geoLocation` for this call; `null` sends none. | Returns `Promise`. ### `cancelOrder()` ```ts cancelOrder(orderId: string, opts?: GeoOption): Promise ``` `DELETE /api/v1/orders/{order_id}`: request a cancel. A cancel is a request: a fill already in flight can still land. | Parameter | Type | Description | |---|---|---| | `orderId` | `string` | | | `opts` (optional) | `GeoOption` | | | `opts.geoLocation` (optional) | `string \| null` | Overrides the client's `geoLocation` for this call; `null` sends none. | Returns `Promise`. ### `cancelOrders()` ```ts cancelOrders(orderIds: readonly string[], opts?: GeoOption): Promise ``` `DELETE /api/v1/orders/batched`: cancel the named orders. | Parameter | Type | Description | |---|---|---| | `orderIds` | `readonly string[]` | | | `opts` (optional) | `GeoOption` | | | `opts.geoLocation` (optional) | `string \| null` | Overrides the client's `geoLocation` for this call; `null` sends none. | Returns `Promise`. ### `close()` ```ts close(): Promise ``` Nothing to release (fetch pools connections itself); present so `await using` and explicit cleanup work. Returns `Promise`. ### `createMarkets()` ```ts createMarkets(eventId: string, markets: readonly NewMarketInput[], opts?: CreateMarketsOptions): Promise ``` `POST /api/v1/markets`: create player-prop markets on a scheduled event, up to `MAX_MARKETS_PER_REQUEST` per call. Needs an OAuth app token with `markets.write`, which STX grants, from the client's IP allow-list. Returns one result per market, in order: `created` or `exists` (both with `market_id`), or `rejected` with a `reason`. A rejected market is a result, not an error, and resending is safe: a market that is already listed comes back `exists`. A refused request throws (`403` without `markets.write`, `404` unknown event, `422` event not open). Each `line` must be a positive number ending in `.5`, e.g. `18.5`. More than `MAX_MARKETS_PER_REQUEST` markets throws `TypeError` before any request. `validate` also needs the `events` scope. | Parameter | Type | Description | |---|---|---| | `eventId` | `string` | | | `markets` | `readonly NewMarketInput[]` | | | `opts` (optional) | `CreateMarketsOptions` | | | `opts.validate` (optional) | `boolean` | Check each market against the event's players and stats, and the line rule, before sending. Failing markets come back `rejected` without a request; the rest are sent with the event's own stat key. The exchange checks every market either way. Reads `eventPlayers()`, so the token also needs the `events` scope. | Returns `Promise`. ### `deposits()` ```ts deposits(query?: PageQuery): Promise> ``` `GET /api/v1/portfolio/deposits`. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `PageQuery` | | | `query.cursor` (optional) | `string` | | | `query.limit` (optional) | `number` | | Returns `Promise>`. ### `eventPlayers()` ```ts eventPlayers(eventId: string): Promise ``` `GET /api/v1/events/{event_id}/players`: the event's team rosters and the stats a market on it can use. `teams` is empty until the rosters are published. Needs an OAuth app token with `events`, or an API key. | Parameter | Type | Description | |---|---|---| | `eventId` | `string` | | Returns `Promise`. ### `events()` ```ts events(query?: EventsQuery): Promise> ``` `GET /api/v1/events`: one page of events. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `EventsQuery` | | | `query.competitions` (optional) | `StrList` | | | `query.cursor` (optional) | `string` | | | `query.eventIds` (optional) | `StrList` | | | `query.eventTypes` (optional) | `StrList` | | | `query.limit` (optional) | `number` | | | `query.promoted` (optional) | `boolean` | | | `query.sortBy` (optional) | `string` | `"start_time"`. | | `query.sortDirection` (optional) | `"asc" \| "desc"` | | | `query.sports` (optional) | `StrList` | | | `query.status` (optional) | `string` | `scheduled`, `in_progress`, `completed` or `cancelled`. | | `query.title` (optional) | `string` | | Returns `Promise>`. ### `fees()` ```ts fees(query?: PageQuery): Promise> ``` `GET /api/v1/portfolio/fees`: fee and fee-refund entries. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `PageQuery` | | | `query.cursor` (optional) | `string` | | | `query.limit` (optional) | `number` | | Returns `Promise>`. ### `fills()` ```ts fills(query?: FillsQuery): Promise> ``` `GET /api/v1/fills`: one page of your executions. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `FillsQuery` | | | `query.cursor` (optional) | `string` | | | `query.limit` (optional) | `number` | | | `query.marketIds` (optional) | `StrList` | | | `query.orderIds` (optional) | `StrList` | | | `query.status` (optional) | `string` | `created`, `open`, `settled` or `cancelled`. | Returns `Promise>`. ### `geoCheck()` ```ts geoCheck(opts?: GeoOption): Promise ``` Ask the exchange whether this machine may trade right now, without placing or cancelling anything. Sends `DELETE /api/v1/orders/00000000-0000-0000-0000-000000000000` (an order id that cannot exist) with the geolocation packet, if one is configured. The exchange runs its geo check before it looks the order up, so a 404 means the check passed, and a geo 422 means it did not. On an exchange without geo-fencing it always passes. Needs a `read_write` key (the probe is a write route). The exchange records a failed check in its geolocation log exactly as it would a real order from the same place, and may raise a compliance alert, so call it once at start-up, not in a loop. | Parameter | Type | Description | |---|---|---| | `opts` (optional) | `GeoOption` | | | `opts.geoLocation` (optional) | `string \| null` | Overrides the client's `geoLocation` for this call; `null` sends none. | Returns `Promise`. ### `iterAccountMarketStats()` ```ts iterAccountMarketStats(query?: WithoutCursor): AsyncGenerator ``` Every per-market stat row, following the cursor. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `WithoutCursor` | | | `query.competitions` (optional) | `StrList` | | | `query.eventIds` (optional) | `StrList` | | | `query.excludeZeroSettlements` (optional) | `boolean` | | | `query.fromTime` (optional) | `number` | | | `query.limit` (optional) | `number` | | | `query.marketIds` (optional) | `StrList` | | | `query.sports` (optional) | `StrList` | | | `query.toTime` (optional) | `number` | | Returns `AsyncGenerator`. ### `iterAdjustments()` ```ts iterAdjustments(query?: { limit?: number }): AsyncGenerator ``` Every adjustment, following the cursor. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `{ limit?: number }` | | | `query.limit` (optional) | `number` | | Returns `AsyncGenerator`. ### `iterDeposits()` ```ts iterDeposits(query?: { limit?: number }): AsyncGenerator ``` Every deposit, following the cursor. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `{ limit?: number }` | | | `query.limit` (optional) | `number` | | Returns `AsyncGenerator`. ### `iterEvents()` ```ts iterEvents(query?: WithoutCursor): AsyncGenerator ``` Every event matching the filters, following the cursor. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `WithoutCursor` | | | `query.competitions` (optional) | `StrList` | | | `query.eventIds` (optional) | `StrList` | | | `query.eventTypes` (optional) | `StrList` | | | `query.limit` (optional) | `number` | | | `query.promoted` (optional) | `boolean` | | | `query.sortBy` (optional) | `string` | `"start_time"`. | | `query.sortDirection` (optional) | `"asc" \| "desc"` | | | `query.sports` (optional) | `StrList` | | | `query.status` (optional) | `string` | `scheduled`, `in_progress`, `completed` or `cancelled`. | | `query.title` (optional) | `string` | | Returns `AsyncGenerator`. ### `iterFees()` ```ts iterFees(query?: { limit?: number }): AsyncGenerator ``` Every fee entry, following the cursor. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `{ limit?: number }` | | | `query.limit` (optional) | `number` | | Returns `AsyncGenerator`. ### `iterFills()` ```ts iterFills(query?: WithoutCursor): AsyncGenerator ``` Every fill matching the filters, following the cursor. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `WithoutCursor` | | | `query.limit` (optional) | `number` | | | `query.marketIds` (optional) | `StrList` | | | `query.orderIds` (optional) | `StrList` | | | `query.status` (optional) | `string` | `created`, `open`, `settled` or `cancelled`. | Returns `AsyncGenerator`. ### `iterLoyalty()` ```ts iterLoyalty(query?: { limit?: number }): AsyncGenerator ``` Every loyalty entry, following the cursor. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `{ limit?: number }` | | | `query.limit` (optional) | `number` | | Returns `AsyncGenerator`. ### `iterMarkets()` ```ts iterMarkets(query?: WithoutCursor): AsyncGenerator ``` Every market matching the filters, following the cursor. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `WithoutCursor` | | | `query.competitions` (optional) | `StrList` | | | `query.eventIds` (optional) | `StrList` | | | `query.limit` (optional) | `number` | Default 100, maximum 200. | | `query.marketIds` (optional) | `StrList` | | | `query.sortBy` (optional) | `string` | `"event_start"`. | | `query.sortDirection` (optional) | `"asc" \| "desc"` | | | `query.sports` (optional) | `StrList` | | | `query.status` (optional) | `StrList` | One status or a list, e.g. `["pre_open", "open"]`. | | `query.trading` (optional) | `boolean` | `true` for only the markets accepting orders now, `false` for only those that are not. | Returns `AsyncGenerator`. ### `iterOrders()` ```ts iterOrders(query?: WithoutCursor): AsyncGenerator ``` Every order matching the filters, following the cursor. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `WithoutCursor` | | | `query.clientOrderIds` (optional) | `StrList` | | | `query.limit` (optional) | `number` | | | `query.marketIds` (optional) | `StrList` | | | `query.orderIds` (optional) | `StrList` | | | `query.status` (optional) | `StrList` | One status or a list, e.g. `["open", "delayed"]`. | Returns `AsyncGenerator`. ### `iterSettlements()` ```ts iterSettlements(query?: WithoutCursor): AsyncGenerator ``` Every settlement, following the cursor. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `WithoutCursor` | | | `query.limit` (optional) | `number` | | | `query.marketIds` (optional) | `StrList` | | | `query.type` (optional) | `string` | `closed_short`, `closed_long`, `expired_short` or `expired_long`. | Returns `AsyncGenerator`. ### `iterWithdrawals()` ```ts iterWithdrawals(query?: { limit?: number }): AsyncGenerator ``` Every withdrawal, following the cursor. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `{ limit?: number }` | | | `query.limit` (optional) | `number` | | Returns `AsyncGenerator`. ### `loyalty()` ```ts loyalty(query?: PageQuery): Promise> ``` `GET /api/v1/portfolio/loyalty`: loyalty entries. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `PageQuery` | | | `query.cursor` (optional) | `string` | | | `query.limit` (optional) | `number` | | Returns `Promise>`. ### `market()` ```ts market(marketId: string): Promise ``` One market by id. Throws `STXNotFoundException` if there is none. | Parameter | Type | Description | |---|---|---| | `marketId` | `string` | | Returns `Promise`. ### `markets()` ```ts markets(query?: MarketsQuery): Promise> ``` `GET /api/v1/markets`: one page of markets. Filters combine with AND; list filters match any value. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `MarketsQuery` | | | `query.competitions` (optional) | `StrList` | | | `query.cursor` (optional) | `string` | | | `query.eventIds` (optional) | `StrList` | | | `query.limit` (optional) | `number` | Default 100, maximum 200. | | `query.marketIds` (optional) | `StrList` | | | `query.sortBy` (optional) | `string` | `"event_start"`. | | `query.sortDirection` (optional) | `"asc" \| "desc"` | | | `query.sports` (optional) | `StrList` | | | `query.status` (optional) | `StrList` | One status or a list, e.g. `["pre_open", "open"]`. | | `query.trading` (optional) | `boolean` | `true` for only the markets accepting orders now, `false` for only those that are not. | Returns `Promise>`. ### `me()` ```ts me(): Promise ``` `GET /api/v1/me`: who this key belongs to. Returns the `user_id` the account channels are keyed by, the `account_id`, and the key's `scope` (`read_only` or `read_write`). Returns `Promise`. ### `order()` ```ts order(orderId: string): Promise ``` `GET /api/v1/orders/{order_id}`: one of your orders. | Parameter | Type | Description | |---|---|---| | `orderId` | `string` | | Returns `Promise`. ### `orders()` ```ts orders(query?: OrdersQuery): Promise> ``` `GET /api/v1/orders`: one page of your orders, newest first. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `OrdersQuery` | | | `query.clientOrderIds` (optional) | `StrList` | | | `query.cursor` (optional) | `string` | | | `query.limit` (optional) | `number` | | | `query.marketIds` (optional) | `StrList` | | | `query.orderIds` (optional) | `StrList` | | | `query.status` (optional) | `StrList` | One status or a list, e.g. `["open", "delayed"]`. | Returns `Promise>`. ### `placeOrder()` ```ts placeOrder(marketId: string, action: string, orderType: string, opts: OrderOptions): Promise ``` `POST /api/v1/orders`: place one order. `action` is `"buy"` or `"sell"`; `orderType` is `"limit"` or `"market"`. `price` (dollars, required for a limit order) and `quantity` (contracts) are decimal strings, e.g. `{ price: "0.56", quantity: "2" }`. Numbers are refused client-side. The exchange validates the order: a bad price step or a fractional quantity throws `STXValidationException` (400) or `STXRejectedException` (422) with the API's message; a geo-fencing refusal throws `STXGeoLocationException`. A POST is never retried after a 5xx or a dropped connection; pass `clientOrderId` so you can look the order up if that happens. | Parameter | Type | Description | |---|---|---| | `marketId` | `string` | | | `action` | `string` | | | `orderType` | `string` | | | `opts` | `OrderOptions` | | | `opts.cancelOnDisconnect` (optional) | `boolean` | | | `opts.clientOrderId` (optional) | `string` | | | `opts.deviceId` (optional) | `string` | | | `opts.expiration` (optional) | `string` | `"good_till_start"` or `"good_till_time"`. | | `opts.expirationTime` (optional) | `number` | Unix **microseconds**, with `expiration: "good_till_time"`. | | `opts.geoLocation` (optional) | `string \| null` | Overrides the client's `geoLocation` for this call; `null` sends none. | | `opts.price` (optional) | `string` | Dollars, as a decimal string: `"0.56"`. Required for a limit order. | | `opts.quantity` | `string` | Contracts, as a decimal string: `"2"`. | Returns `Promise`. ### `placeOrders()` ```ts placeOrders(orders: readonly NewOrderInput[], opts?: GeoOption): Promise ``` `POST /api/v1/orders/batched`: place several orders in one call. One geo check covers the batch. Returns one result per order, in order; a rejected order has `errors` instead of `order` and does not stop the others. | Parameter | Type | Description | |---|---|---| | `orders` | `readonly NewOrderInput[]` | | | `opts` (optional) | `GeoOption` | | | `opts.geoLocation` (optional) | `string \| null` | Overrides the client's `geoLocation` for this call; `null` sends none. | Returns `Promise`. ### `positions()` ```ts positions(query?: { marketIds?: StrList }): Promise ``` `GET /api/v1/positions`: your open positions, the same objects the `positions` channel sends on join. `position` is positive when long, negative when short. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `{ marketIds?: StrList }` | | | `query.marketIds` (optional) | `StrList` | | Returns `Promise`. ### `settlements()` ```ts settlements(query?: SettlementsQuery): Promise> ``` `GET /api/v1/portfolio/settlements`: settlements on your account. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `SettlementsQuery` | | | `query.cursor` (optional) | `string` | | | `query.limit` (optional) | `number` | | | `query.marketIds` (optional) | `StrList` | | | `query.type` (optional) | `string` | `closed_short`, `closed_long`, `expired_short` or `expired_long`. | Returns `Promise>`. ### `statsFor()` ```ts statsFor(event: string | { event_id?: string | null }): Promise ``` The stats `event` accepts for new markets. | Parameter | Type | Description | |---|---|---| | `event` | `string \| { event_id?: string \| null }` | | Returns `Promise`. ### `userId()` ```ts userId(): Promise ``` The user id from `me()`, fetched once and cached. If `me()` is not permitted, the id comes from `balance()`, which carries the same `user_id`. Returns `Promise`. ### `websocket()` ```ts websocket(opts?: Omit): STXWebSocket ``` An `STXWebSocket` using this client's host and key. Account channels need your user id; the socket fetches it through `me()` on first use. | Parameter | Type | Description | |---|---|---| | `opts` (optional) | `Omit` | | | `opts.channelPingIntervalMs` (optional) | `number \| null` | Milliseconds between channel `ping` frames on every joined topic; `null` disables them. `orders` with cancel-on-disconnect pings faster, from the granted timeout. Default 30000. | | `opts.env` (optional) | `string \| null` | `"production"` or `"demo"` (`"prod"` and `"live"` are accepted). | | `opts.environment` (optional) | `string \| STXEnvironment` | A published environment, `Environments.OntarioDemo`, or its name, `"ontario-demo"`. Instead of `region` + `env`; `host` still wins. | | `opts.heartbeatIntervalMs` (optional) | `number` | Milliseconds between socket heartbeats. The server closes a socket silent for 60 s. Default 25000. | | `opts.host` (optional) | `string \| null` | A hostname or URL, overriding region/env: `"demo.stxapp.ca"`, `"http://localhost:4000"`. | | `opts.joinTimeoutMs` (optional) | `number` | Milliseconds to wait for a join or push reply. Default 10000. | | `opts.keyId` (optional) | `string \| null` | The API key ID. | | `opts.logger` (optional) | `Logger` | | | `opts.onReconnect` (optional) | `() => void \| Promise` | Called after every successful reconnect and rejoin: the moment to read open orders and anything else you loaded with `STX` again. | | `opts.privateKey` (optional) | `PrivateKeyInput \| null` | The Ed25519 private key: PEM text, a path to a PEM file, PEM bytes, or a KeyObject. | | `opts.profile` (optional) | `string \| null` | A section of ~/.stx/credentials to read. | | `opts.queueSize` (optional) | `number` | Messages buffered per channel for iteration; the oldest is dropped when full. Default 10000. | | `opts.reconnect` (optional) | `boolean` | Reconnect after a drop. Default true. | | `opts.reconnectPolicy` (optional) | `ReconnectPolicy` | | | `opts.region` (optional) | `string \| null` | A known region, e.g. `"ontario"` or `"us"`. With `env`, picks the host. | | `opts.signer` (optional) | `Signer` | Instead of `privateKey`: signs message bytes, for keys in an HSM or KMS. | | `opts.userId` (optional) | `string` | Your user id, if you already have it (skips `GET /me`). | | `opts.verifyTls` (optional) | `boolean \| null` | Set false only for a local server. Honoured by the WebSocket; for HTTP requests, pass a `fetch` whose dispatcher skips verification (Node's global fetch has no per-call switch). | Returns `STXWebSocket`. ### `withdrawals()` ```ts withdrawals(query?: PageQuery): Promise> ``` `GET /api/v1/portfolio/withdrawals`. | Parameter | Type | Description | |---|---|---| | `query` (optional) | `PageQuery` | | | `query.cursor` (optional) | `string` | | | `query.limit` (optional) | `number` | | Returns `Promise>`. --- # STXWebSocket > The WebSocket client: connecting, joining channels and reconnecting. Source: https://docs.stxapp.io/sdks/typescript/reference/stx-websocket/ {/* Generated from the TypeScript source by tools/gen-reference.mjs. Do not edit by hand. */} Phoenix-channels client for the documented STX topics. Build it from an `STX` client, which supplies the host, the key and your user id: ```ts const client = new STX(); const ws = await client.websocket().connect(); const book = await ws.orderbook([""], { onMessage: console.log }); const orders = await ws.orders(); console.log(await orders.waitSnapshot()); await ws.runForever(); ``` or standalone with the same settings options as `STX`. ## Constructor ```ts new STXWebSocket(opts?: STXWebSocketOptions) ``` | Parameter | Type | Description | |---|---|---| | `opts` (optional) | `STXWebSocketOptions` | | | `opts.channelPingIntervalMs` (optional) | `number \| null` | Milliseconds between channel `ping` frames on every joined topic; `null` disables them. `orders` with cancel-on-disconnect pings faster, from the granted timeout. Default 30000. | | `opts.env` (optional) | `string \| null` | `"production"` or `"demo"` (`"prod"` and `"live"` are accepted). | | `opts.environment` (optional) | `string \| STXEnvironment` | A published environment, `Environments.OntarioDemo`, or its name, `"ontario-demo"`. Instead of `region` + `env`; `host` still wins. | | `opts.heartbeatIntervalMs` (optional) | `number` | Milliseconds between socket heartbeats. The server closes a socket silent for 60 s. Default 25000. | | `opts.host` (optional) | `string \| null` | A hostname or URL, overriding region/env: `"demo.stxapp.ca"`, `"http://localhost:4000"`. | | `opts.joinTimeoutMs` (optional) | `number` | Milliseconds to wait for a join or push reply. Default 10000. | | `opts.keyId` (optional) | `string \| null` | The API key ID. | | `opts.logger` (optional) | `Logger` | | | `opts.onReconnect` (optional) | `() => void \| Promise` | Called after every successful reconnect and rejoin: the moment to read open orders and anything else you loaded with `STX` again. | | `opts.privateKey` (optional) | `PrivateKeyInput \| null` | The Ed25519 private key: PEM text, a path to a PEM file, PEM bytes, or a KeyObject. | | `opts.profile` (optional) | `string \| null` | A section of ~/.stx/credentials to read. | | `opts.queueSize` (optional) | `number` | Messages buffered per channel for iteration; the oldest is dropped when full. Default 10000. | | `opts.reconnect` (optional) | `boolean` | Reconnect after a drop. Default true. | | `opts.reconnectPolicy` (optional) | `ReconnectPolicy` | | | `opts.region` (optional) | `string \| null` | A known region, e.g. `"ontario"` or `"us"`. With `env`, picks the host. | | `opts.rest` (optional) | `STX` | An `STX` client to take host, key and user id from. | | `opts.signer` (optional) | `Signer` | Instead of `privateKey`: signs message bytes, for keys in an HSM or KMS. | | `opts.userId` (optional) | `string` | Your user id, if you already have it (skips `GET /me`). | | `opts.verifyTls` (optional) | `boolean \| null` | Set false only for a local server. Honoured by the WebSocket; for HTTP requests, pass a `fetch` whose dispatcher skips verification (Node's global fetch has no per-call switch). | ## Properties | Property | Type | Description | |---|---|---| | `channelPingIntervalMs` | `number \| null` | | | `heartbeatIntervalMs` | `number` | | | `joinTimeoutMs` | `number` | | | `onReconnect` | `() => void \| Promise \| undefined` | | | `queueSize` | `number` | | | `reconnect` | `boolean` | | | `reconnectPolicy` | `ReconnectPolicy` | | | `reconnects` | `number` | Successful reconnects so far. | | `url` | `string` | | | `closed` | `boolean` | | | `connected` | `boolean` | | ## Methods ### `account()` ```ts account(opts?: { marketIds?: readonly string[]; onMessage?: MessageHandler }): Promise ``` `account:{user_id}`: everything the five channels above carry, on one join. Do not also join a per-type channel (you would get every message twice), and use `orders` if you need cancel-on-disconnect. | Parameter | Type | Description | |---|---|---| | `opts` (optional) | `{ marketIds?: readonly string[]; onMessage?: MessageHandler }` | | | `opts.marketIds` (optional) | `readonly string[]` | | | `opts.onMessage` (optional) | `MessageHandler` | | Returns `Promise`. ### `accountView()` ```ts accountView(opts?: AccountViewOptions): Promise ``` A live view of your account: joins `balances`, `orders`, `fills` and `positions`, seeds each from its join snapshot and applies every pushed update, re-seeding from the new snapshots after a reconnect. Resolves once all four snapshots have arrived. See `AccountView`. | Parameter | Type | Description | |---|---|---| | `opts` (optional) | `AccountViewOptions` | | | `opts.accountId` (optional) | `string` | Which of your accounts the balance is for; omit for your only account. | | `opts.keepFlatPositions` (optional) | `boolean` | Keep positions whose quantity is zero. Default false: a flat position leaves `positions`. | | `opts.marketIds` (optional) | `readonly string[]` | Restrict orders, fills and positions to these markets. The balance is always the whole account. | | `opts.maxFills` (optional) | `number` | The most fills held; the oldest go first. Default 1000. | | `opts.onChange` (optional) | `(change: AccountChange) => void` | Called after every applied message, snapshots included. | | `opts.snapshotTimeoutMs` (optional) | `number \| null` | Wait this long for the four join snapshots before `accountView()` rejects. Default 15000; `null` waits forever. | Returns `Promise`. ### `balances()` ```ts balances(opts?: { accountId?: string; onMessage?: MessageHandler }): Promise ``` `balances:{user_id}`: `balances` on join, then `update` and `payment_update`. `accountId` picks one of your accounts. | Parameter | Type | Description | |---|---|---| | `opts` (optional) | `{ accountId?: string; onMessage?: MessageHandler }` | | | `opts.accountId` (optional) | `string` | | | `opts.onMessage` (optional) | `MessageHandler` | | Returns `Promise`. ### `close()` ```ts close(): Promise ``` Leave every channel and close the socket. Safe to call twice. Returns `Promise`. ### `connect()` ```ts connect(): Promise ``` Open the socket. Idempotent; resolves to this socket. Returns `Promise`. ### `fills()` ```ts fills(opts?: { marketIds?: readonly string[]; onMessage?: MessageHandler }): Promise ``` `fills:{user_id}`: `all_trades` on join, then one `trade` per execution or status change. | Parameter | Type | Description | |---|---|---| | `opts` (optional) | `{ marketIds?: readonly string[]; onMessage?: MessageHandler }` | | | `opts.marketIds` (optional) | `readonly string[]` | | | `opts.onMessage` (optional) | `MessageHandler` | | Returns `Promise`. ### `join()` ```ts join(topic: string, payload?: Record, onMessage?: MessageHandler, opts?: { pingIntervalMs?: number | null }): Promise ``` Join `topic` with `payload` and wait for the reply. Rejects with `STXChannelException` carrying the server's reason (for example `market_ids_required` or `unauthorized`) if the join is refused. | Parameter | Type | Description | |---|---|---| | `topic` | `string` | | | `payload` (optional) | `Record` | | | `onMessage` (optional) | `MessageHandler` | | | `opts` (optional) | `{ pingIntervalMs?: number \| null }` | | | `opts.pingIntervalMs` (optional) | `number \| null` | | Returns `Promise`. ### `market()` ```ts market(marketId: string, opts?: { onMessage?: MessageHandler }): Promise ``` `market:`: one market in full. The join reply (`channel.reply`) is the market's current state, every market field plus `ob`, its aggregated book (`{b, o}`, each level `{p, q, l, tc, tl}`). Then `market_update` carries the fields that changed and `order_book_update` a fresh `{ob}` about every 200 ms. The event's live status text rides along: `event_brief` and `detailed_event_brief` (the score and clock while in play, e.g. `"CHC 3 - 4 BOS : Bottom 8th 1 Outs"`, the start time before), with `event_status`. `marketId` is the market id or symbol. Prices are converted from cents to dollar strings; book levels are written as strings. A resulted or voided market sends one last `market_update` and the server closes the topic. | Parameter | Type | Description | |---|---|---| | `marketId` | `string` | | | `opts` (optional) | `{ onMessage?: MessageHandler }` | | | `opts.onMessage` (optional) | `MessageHandler` | | Returns `Promise`. ### `markets()` ```ts markets(opts?: { messageTypes?: readonly string[]; onMessage?: MessageHandler; ruleFilters?: readonly string[] }): Promise ``` `markets`: `market_created` and `market_updated` for every market. Each payload maps market id to a market object; `market_updated` carries only the changed fields. Prices arrive in cents on the wire and are converted to dollar strings here. | Parameter | Type | Description | |---|---|---| | `opts` (optional) | `{ messageTypes?: readonly string[]; onMessage?: MessageHandler; ruleFilters?: readonly string[] }` | | | `opts.messageTypes` (optional) | `readonly string[]` | | | `opts.onMessage` (optional) | `MessageHandler` | | | `opts.ruleFilters` (optional) | `readonly string[]` | | Returns `Promise`. ### `marketStats()` ```ts marketStats(marketIds: readonly string[], opts?: { onMessage?: MessageHandler; range?: string }): Promise ``` `market_stats`: a price series per market. The history is in the join reply (`channel.reply.markets`); `market_stats` pushes changed buckets (upsert by `timestamp_us`) and `market_stats_snapshot` replaces a series. | Parameter | Type | Description | |---|---|---| | `marketIds` | `readonly string[]` | | | `opts` (optional) | `{ onMessage?: MessageHandler; range?: string }` | | | `opts.onMessage` (optional) | `MessageHandler` | | | `opts.range` (optional) | `string` | | Returns `Promise`. ### `marketUpdates()` ```ts marketUpdates(opts?: { onMessage?: MessageHandler; watch?: readonly string[] }): Promise ``` `market_updates`: `created` and `updated` for the markets you watch. Nothing arrives until you watch something; pass `watch` or call `channel.watch([...])`. Prices are converted from cents to dollar strings. | Parameter | Type | Description | |---|---|---| | `opts` (optional) | `{ onMessage?: MessageHandler; watch?: readonly string[] }` | | | `opts.onMessage` (optional) | `MessageHandler` | | | `opts.watch` (optional) | `readonly string[]` | | Returns `Promise`. ### `orderbook()` ```ts orderbook(marketIds: readonly string[], opts?: { onMessage?: MessageHandler }): Promise ``` `orderbook`: the aggregated book, one `book` push per market. Each push is a full snapshot of that market's book; replace what you hold rather than merging. `marketIds` is required. | Parameter | Type | Description | |---|---|---| | `marketIds` | `readonly string[]` | | | `opts` (optional) | `{ onMessage?: MessageHandler }` | | | `opts.onMessage` (optional) | `MessageHandler` | | Returns `Promise`. ### `orders()` ```ts orders(opts?: { cancelOnDisconnect?: boolean; marketIds?: readonly string[]; onMessage?: MessageHandler; pingTimeout?: number }): Promise ``` `orders:{user_id}`: `all_orders` on join, then `new_open_order`. `cancelOnDisconnect: true` arms cancel-on-disconnect for orders placed with `cancelOnDisconnect: true`. `pingTimeout` is in milliseconds, clamped by the server to 5000 to 20000; the granted value is in `channel.reply.ping_timeout` and the SDK pings at 60% of it for as long as the channel is joined. | Parameter | Type | Description | |---|---|---| | `opts` (optional) | `{ cancelOnDisconnect?: boolean; marketIds?: readonly string[]; onMessage?: MessageHandler; pingTimeout?: number }` | | | `opts.cancelOnDisconnect` (optional) | `boolean` | | | `opts.marketIds` (optional) | `readonly string[]` | | | `opts.onMessage` (optional) | `MessageHandler` | | | `opts.pingTimeout` (optional) | `number` | | Returns `Promise`. ### `positions()` ```ts positions(opts?: { marketIds?: readonly string[]; onMessage?: MessageHandler }): Promise ``` `positions:{user_id}`: `all_positions` on join, then `updated_positions` deltas with only the changed positions. | Parameter | Type | Description | |---|---|---| | `opts` (optional) | `{ marketIds?: readonly string[]; onMessage?: MessageHandler }` | | | `opts.marketIds` (optional) | `readonly string[]` | | | `opts.onMessage` (optional) | `MessageHandler` | | Returns `Promise`. ### `runForever()` ```ts runForever(): Promise ``` Resolve once `close()` is called (or reconnects are exhausted). Returns `Promise`. ### `settlements()` ```ts settlements(opts?: { marketIds?: readonly string[]; onMessage?: MessageHandler }): Promise ``` `settlements:{user_id}`: `new_settlements` as they are recorded. No snapshot; history is `STX.settlements()`. | Parameter | Type | Description | |---|---|---| | `opts` (optional) | `{ marketIds?: readonly string[]; onMessage?: MessageHandler }` | | | `opts.marketIds` (optional) | `readonly string[]` | | | `opts.onMessage` (optional) | `MessageHandler` | | Returns `Promise`. ### `ticker()` ```ts ticker(opts?: { competitions?: readonly string[]; onMessage?: MessageHandler; sports?: readonly string[] }): Promise ``` `ticker`: a `ticker` push whenever a market's price, top of book, volume or open interest moves. | Parameter | Type | Description | |---|---|---| | `opts` (optional) | `{ competitions?: readonly string[]; onMessage?: MessageHandler; sports?: readonly string[] }` | | | `opts.competitions` (optional) | `readonly string[]` | | | `opts.onMessage` (optional) | `MessageHandler` | | | `opts.sports` (optional) | `readonly string[]` | | Returns `Promise`. ### `trades()` ```ts trades(opts?: { eventIds?: readonly string[]; marketIds?: readonly string[]; onMessage?: MessageHandler }): Promise ``` `trades`: every execution on the exchange, anonymised. `action` is the taker's side. Not your fills: see `fills`. | Parameter | Type | Description | |---|---|---| | `opts` (optional) | `{ eventIds?: readonly string[]; marketIds?: readonly string[]; onMessage?: MessageHandler }` | | | `opts.eventIds` (optional) | `readonly string[]` | | | `opts.marketIds` (optional) | `readonly string[]` | | | `opts.onMessage` (optional) | `MessageHandler` | | Returns `Promise`. ### `userId()` ```ts userId(): Promise ``` Your user id, from `userId` or `GET /api/v1/me`. Returns `Promise`. ### `userInfo()` ```ts userInfo(opts?: { onMessage?: MessageHandler }): Promise ``` `user_info:{user_id}`: `user_updated` right after joining, then on every profile change. | Parameter | Type | Description | |---|---|---| | `opts` (optional) | `{ onMessage?: MessageHandler }` | | | `opts.onMessage` (optional) | `MessageHandler` | | Returns `Promise`. --- # Trading > Place and cancel orders, set expiration, and arm cancel-on-disconnect. Source: https://docs.stxapp.io/sdks/typescript/trading/ Placing and cancelling needs a `read_write` key. The examples run on the US demo exchange with 1-cent buy orders that will not fill. How orders behave on the exchange is covered in [Order types](/concepts/order-types/) and [Risk controls](/risk-controls/); this page shows the SDK calls. The examples use a market that is open, accepting orders (`trading` is `true`), and whose event has not started: ```ts import { STX } from "@stxapp/stx-typescript"; const client = new STX(); let market; for await (const m of client.iterMarkets({ trading: true, sortBy: "event_start", sortDirection: "desc" })) { if (m.event_status === "scheduled") { market = m; break; } } if (!market?.market_id) throw new Error("no tradeable market"); const marketId = market.market_id; ``` ## Place an order ```ts const order = await client.placeOrder(marketId, "buy", "limit", { price: "0.01", quantity: "1", clientOrderId: "my-order-1", }); console.log(order.id, order.status); await client.cancelOrder(order.id!); ``` `placeOrder(marketId, action, orderType, options)` takes `action` `"buy"` or `"sell"` and `orderType` `"limit"` or `"market"`. Options: | Option | Type | |---|---| | `price` | Dollar string, e.g. `"0.56"`. Required for `"limit"`, omitted for `"market"` | | `quantity` | Contract string, e.g. `"2"` | | `clientOrderId` | Your own id, echoed back and filterable | | `expiration`, `expirationTime` | See [Expiration](#expiration) | | `cancelOnDisconnect` | See [Cancel-on-disconnect](#cancel-on-disconnect) | | `deviceId` | Optional label | | `geoLocation` | See [Geo-fencing](#geo-fencing) | If the exchange refuses the order, the call throws with the exchange's message: ```ts import { STXRejectedException, STXValidationException } from "@stxapp/stx-typescript"; try { await client.placeOrder(marketId, "buy", "limit", { price: "0.01", quantity: "1.5" }); } catch (err) { if (err instanceof STXRejectedException || err instanceof STXValidationException) { console.error(err.statusCode, err.message); } else { throw err; } } ``` A market order leaves out `price`: ```ts await client.placeOrder(marketId, "buy", "market", { quantity: "1" }); ``` ## Place several orders ```ts const results = await client.placeOrders([ { marketId, action: "buy", orderType: "limit", price: "0.01", quantity: "1" }, { marketId, action: "buy", orderType: "limit", price: "0.02", quantity: "1" }, ]); for (const r of results) { console.log(r.ok ? r.order!.id : r.errors); } ``` Each result holds either `order` or `errors`, in the order you sent them. One rejection does not stop the rest. ## Cancel ```ts await client.cancelOrder(orderId); // one await client.cancelOrders([idA, idB]); // several await client.cancelAllOrders(); // everything on the account ``` Each returns cancellations with `order_id` and `status`. An order can still fill while a cancel is in flight, so reconcile against fills. ## Read orders ```ts for await (const o of client.iterOrders({ status: ["open", "delayed"] })) { console.log(o.id, o.price, o.filled, "/", o.quantity); } const one = await client.order(orderId); ``` `orders()` filters on `orderIds`, `clientOrderIds`, `marketIds` and `status`. ## Avoid duplicate orders The client never resends a `POST` after a server error or a dropped connection, because the order may already be on the book. Send a `clientOrderId` and look it up before trying again: ```ts import { randomUUID } from "node:crypto"; import { STXServerException, STXTransportException } from "@stxapp/stx-typescript"; const clientOrderId = randomUUID(); let order; try { order = await client.placeOrder(marketId, "buy", "limit", { price: "0.01", quantity: "1", clientOrderId }); } catch (err) { if (!(err instanceof STXServerException || err instanceof STXTransportException)) throw err; order = (await client.orders({ clientOrderIds: [clientOrderId] })).items[0]; } ``` ## Expiration The two expiration modes are described in [Risk controls](/risk-controls/#order-expiration). In the SDK: ```ts await client.placeOrder(marketId, "buy", "limit", { price: "0.01", quantity: "1", expiration: "good_till_start" }); await client.placeOrder(marketId, "buy", "limit", { price: "0.01", quantity: "1", expiration: "good_till_time", expirationTime: (Date.now() + 10 * 60_000) * 1000, // microseconds }); ``` :::caution `expirationTime` is Unix time in **microseconds**. `Date.now()` returns milliseconds, so multiply it by 1000. ::: ## Cancel-on-disconnect How cancel-on-disconnect works is described in [Risk controls](/risk-controls/#cancel-orders-on-disconnect). In the SDK, arm it on the `orders` channel and opt each order in. The client then keeps pinging the channel for you while the socket is up. ```ts const ws = await client.websocket().connect(); const orders = await ws.orders({ cancelOnDisconnect: true, pingTimeout: 5000 }); console.log("granted timeout (ms):", orders.reply.ping_timeout); await client.placeOrder(marketId, "buy", "limit", { price: "0.01", quantity: "1", cancelOnDisconnect: true }); ``` ## Geo-fencing STX Ontario checks where each order write comes from. A server at a fixed IP address needs that address whitelisted on your account. Otherwise, give the client a geolocation packet, or a function that returns a fresh one, and it is attached to every order write: ```ts const client = new STX({ geoLocation: () => getLatestPacket() }); ``` Check before trading, without placing anything (needs a `read_write` key; call it once at start-up): ```ts const check = await client.geoCheck(); if (!check.allowed) console.error(check.reason, check.message); ``` A refused write throws `STXGeoLocationException` with a `reason`. --- # WebSockets > Stream market data and your account over one socket. Source: https://docs.stxapp.io/sdks/typescript/websockets/ The client streams the channels documented under [WebSockets](/websockets/): what each channel sends, its payloads and its limits are covered there. This page shows the SDK calls. ## Connect and join ```ts import { STX } from "@stxapp/stx-typescript"; const client = new STX(); const ws = await client.websocket().connect(); const ticker = await ws.ticker(); console.log(ticker.reply); await ws.close(); ``` `client.websocket()` uses the client's host and key, and signs the handshake with the key. Connecting joins nothing; each join method returns a `Channel`. A client with no API key connects unsigned: the market data channels accept that, and the account channels refuse the join. ## Receive messages Each message has `topic`, `event` and `payload`. Take them with a callback, one at a time, or with `for await`: ```ts const ids = (await client.markets({ status: "open", limit: 5 })).items.map((m) => m.market_id!); // A callback await ws.trades({ marketIds: ids, onMessage: (msg) => console.log(msg.event, msg.payload) }); // One message, with a timeout in milliseconds const book = await ws.orderbook(ids); try { const msg = await book.next(5_000); console.log(msg.payload.market_id); } catch { console.log("nothing in 5 s"); } // Every message, until the channel closes for await (const msg of book) { console.log(msg.payload.market_id, msg.payload.bids[0]); } ``` Money fields are dollar strings on every channel, including the ones that send cents on the wire. ## Snapshots Account channels send your current state right after the join. `waitSnapshot()` returns it, keyed by event name: ```ts const positions = await ws.positions(); const snap = await positions.waitSnapshot(10_000); console.log(snap.all_positions.positions); ``` ## Channels | SDK call | Channel | |---|---| | `ws.orderbook(marketIds)` | [Order book, ticker and trades](/websockets/channels/market-data/) | | `ws.ticker({ sports, competitions })` | [Order book, ticker and trades](/websockets/channels/market-data/) | | `ws.trades({ marketIds, eventIds })` | [Order book, ticker and trades](/websockets/channels/market-data/) | | `ws.market(marketId)` | [Market order book](/websockets/order-book/) | | `ws.markets({ ruleFilters, messageTypes })` | [Markets](/websockets/channels/markets/) | | `ws.marketStats(marketIds, { range })` | [Market stats](/websockets/channels/market-stats/) | | `ws.marketUpdates({ watch })` | [Market updates](/websockets/channels/market-updates/) | | `ws.orders({ marketIds, cancelOnDisconnect, pingTimeout })` | [Orders](/websockets/channels/orders/) | | `ws.fills({ marketIds })` | [Fills](/websockets/channels/fills/) | | `ws.positions({ marketIds })` | [Positions](/websockets/channels/positions/) | | `ws.settlements({ marketIds })` | [Settlements](/websockets/channels/settlements/) | | `ws.balances({ accountId })` | [Balances](/websockets/channels/balances/) | | `ws.account({ marketIds })` | [Account](/websockets/channels/account/) | | `ws.userInfo()` | [User info](/websockets/channels/user-info/) | Every join method also takes `onMessage`. To change filters after joining, call `channel.selectMarketIds([...])` (and `selectFilters`, `selectRuleFilters`, `selectMessageTypes`); on `market_updates`, `channel.watch([...])`. `ws.join(topic, payload)` joins any other topic. ## Live account view `ws.accountView()` keeps your balance, open orders, fills and positions current. It joins the `balances`, `orders`, `fills` and `positions` channels, starts from their snapshots, applies every update, and starts over from fresh snapshots after a reconnect. It resolves once all four snapshots are in. ```ts const ws = await client.websocket().connect(); const view = await ws.accountView({ onChange: (change) => render(change.view.state()), // change.kind: "balance" | "orders" | "fills" | "positions" | "payment" }); console.log(view.balance?.available_balance, view.openOrders.length, view.fills.length, view.positions.length); // No refetch needed: the new order and the balance change arrive on the socket. await client.placeOrder(marketId, "buy", "limit", { price: "0.01", quantity: "1" }); ``` `view.state()` returns plain data you can send to a browser. Options: `marketIds`, `accountId`, `maxFills` (default 1000), `keepFlatPositions`, `snapshotTimeoutMs` (default 15000). `view.close()` leaves the four channels and keeps the socket open. ## Reconnects The client sends the heartbeat and channel pings for you. After a drop it reconnects with backoff, signs again, and rejoins every channel with its current filters. It cannot know what you missed while offline, so read again anything you loaded with a client method (open orders, history) in `onReconnect`: ```ts import { ReconnectPolicy, STX } from "@stxapp/stx-typescript"; const client = new STX(); const ws = await client .websocket({ reconnectPolicy: new ReconnectPolicy({ initialBackoffMs: 500, maxBackoffMs: 30_000, maxAttempts: 20 }), onReconnect: async () => { const open = await client.orders({ status: ["open", "delayed"] }); console.log("reconnected, open orders:", open.length); }, }) .connect(); ``` `reconnect: false` turns reconnecting off. `ws.connected` and `ws.reconnects` report the socket's state. [After a reconnect](/websockets/#after-a-reconnect) explains what the exchange does and does not replay. ## Errors A refused join throws `STXChannelException`, with the server's reason in `err.reply`. No reply within `joinTimeoutMs` (default 10000) throws `STXTimeoutException`. ## Keep a worker running `runForever()` resolves when you call `close()` or the socket gives up: ```ts const ws = await client.websocket().connect(); await ws.fills({ onMessage: (m) => console.log("fill", m.payload) }); process.on("SIGINT", () => void ws.close()); await ws.runForever(); ``` --- # Support > Where to ask, and what to include when a request is failing. Source: https://docs.stxapp.io/support/ ## Ask in Discord **https://discord.gg/yF9eVzPzNZ** That is the fastest way to reach us, and answers there are visible to everyone else building on the exchange, so your question helps the next person too. Integration questions, unexpected responses, missing operations, feature requests: all welcome. You will be talking to the engineers who build the exchange, not a support tier. ## Before you ask about a failing request A few details turn "it does not work" into an answer in one round trip: - The **endpoint and method** (`POST /api/v1/orders`, `GET /api/v1/orders`) or the **channel topic** (`market:`, `orders:`) - The **environment** you are on, and the key ID you signed with (never the private key) - The **exact error text**, and the HTTP status if there was one - For signing failures: whether the [test vector](/api/authentication/#check-your-implementation) reproduces on your machine. If it does, the problem is the timestamp, the key ID, or the key's status rather than your crypto, and knowing that immediately narrows it down Never paste your private key or its contents. We never need it, and we cannot help you faster by seeing it. The key ID alone is fine. ## Things that are already answered | Symptom | Where | | --- | --- | | `unauthorized` on a signed request | [When a signature will not verify](/api/authentication/#when-a-signature-will-not-verify) | | Signature will not verify at all | [test vector](/api/authentication/#check-your-implementation) | | Socket closes after 60 seconds of silence | [Keep the connection alive](/websockets/#keep-the-connection-alive) | | `request_snapshot` or `ping` seems ignored | [Frame format](/websockets/#frame-format): reuse the `join_ref` | | Prices out by a factor of 100 | [Wire format](/websockets/channels/wire-format/): REST and most channels send prices as dollar strings (`"0.4400"`), but the `markets` and `market_updates` channels, and the market record on [`market:{market_id}`](/websockets/order-book/), send them as JSON numbers in cents (`44`). [Place an order](/api/rest/orders/place-order/) takes the dollar string, below the market's `max_price` | | `Operation not supported for key` | [REST API reference](/api/rest/) | ## A shared Slack channel For an integration of any size we are happy to open a shared Slack channel with our engineers, which tends to work better than a ticket queue for live development. Ask in Discord or through support and we will set it up. ## Reporting something security-sensitive If you believe you have found a vulnerability, do not open it in Discord or a public issue. Contact us privately through support and we will route it to the right people. --- # WebSocket channels Source: https://docs.stxapp.io/websockets/ Live updates run over Phoenix channels on a single socket. Join the topics you care about; each pushes events as things change. | Use Case | Channel | | --- | --- | | The book for a market you trade | [`orderbook`](/websockets/channels/market-data/#orderbook) | | Price history for a chart | [`market_stats`](/websockets/channels/market-stats/) | | Prices and trades across markets | [`ticker`, `trades`](/websockets/channels/market-data/) | | Traded volume for an event | [`events`](/websockets/channels/events/) | | Your orders as they are accepted and filled | [`orders:{user_id}`](/websockets/channels/orders/) | | Your fills | [`fills:{user_id}`](/websockets/channels/fills/) | | Your positions and settlements | [`positions:{user_id}`](/websockets/channels/positions/), [`settlements:{user_id}`](/websockets/channels/settlements/) | | Your balance and exposure | [`balances:{user_id}`](/websockets/channels/balances/) | | All of your account on one topic | [`account:{user_id}`](/websockets/channels/account/) | | What an order would cost before you place it | [`order_slip:{user_id}`](/websockets/channels/order-slip/) | | Markets appearing, changing status, or moving | [`markets`](/websockets/channels/markets/), [`market_updates`](/websockets/channels/market-updates/) | :::tip[Money is a string] The account channels and the three market-data feeds write money as dollar strings (`"39.0000"`) and contract counts as quantity strings (`"100.00"`), the same as the REST API; see [Wire format](/websockets/channels/wire-format/). The two market-metadata feeds, `markets` and `market_updates`, are the exception: their prices are JSON numbers in cents, as each page states field by field. [`betslip:`](/websockets/channels/order-slip/), the topic `order_slip:` supersedes, is a different exception: its money **is** in dollars, but as a string rounded to two decimal places (`"145.00"`, not `"145.0000"`), and its quantities are JSON numbers. It is not in cents. ::: ## Why the socket rather than REST REST answers what was true when you asked. For anything you have to react to (a fill, a book move, a balance change), the socket is the only way to know promptly, and it is what STX's own apps use. Two things follow from that: - **Poll REST for state you can afford to be stale about**, like the market list at startup. Stream everything else. - **Reconcile periodically anyway.** Take a REST snapshot on a slow cadence and compare: a dropped message on a live connection is silent, and there are no sequence numbers to detect one. ## This is not a plain WebSocket STX runs **Phoenix channels** over the socket, which behaves differently from what "WebSocket API" usually implies. Three differences matter before you write any code. **Connecting subscribes you to nothing.** A raw WebSocket usually starts streaming once it opens. Here the socket is only a pipe: you then **join** one or more topics, and until you do, a healthy connection sits in complete silence. Most first integrations that look broken are a socket that connected and never joined. **One socket multiplexes every channel.** You do not open a connection per feed. One socket carries your orders, your fills, your positions and the book for every market you trade, each as a separate topic on the same wire. Do not open a socket per topic: the keep-alive is per socket, so eight connections means eight heartbeats for no benefit. **Frames are arrays, not objects.** ```json [join_ref, ref, topic, event, payload] ``` There is a protocol layer here (joins, replies, refs, heartbeats), not just your data. A [Phoenix client library](#use-a-phoenix-client-rather-than-raw-frames) handles it for you, and reading the frame format below is how you debug it when something is wrong. ## Sign the socket, then join what you need **Sign every connection**, whichever channels you plan to join. The handshake takes the same `X-STX-ACCESS-*` headers as REST (see [Authentication](/api/authentication/)), with one difference: the socket signs `GET` and the path **without** its query string, so `timestamp + "GET" + "/socket/websocket"` even though you connect to `/socket/websocket?vsn=2.0.0`. Send a `User-Agent` header on the handshake as well. A handshake without one is refused with `403`, signed or not. Browsers always send one; Node's `ws` and some other WebSocket clients do not unless you set it. The market data channels accept an unsigned socket; the account channels need a signed one, and refuse a join on an unsigned socket with `{"reason":"unauthorized"}`. Signing anyway costs nothing: one signed connection carries both the market feeds and your account channels, and an unsigned socket has to be thrown away and replaced the moment you want your own fills. ## Account channels Topics suffixed with your user id, so you only ever receive your own data. Get that id from `GET /api/v1/me`; it is a UUID. Joining a topic whose id is not yours fails with `{"reason":"unauthorized"}`. | Channel | Topic | Snapshot on join | Pushes | | --- | --- | --- | --- | | [Orders](/websockets/channels/orders/) | `orders:{user_id}` | `all_orders` | `new_open_order`: order accepted or filled. | | [Fills](/websockets/channels/fills/) | `fills:{user_id}` | `all_trades` | `trade`, one per execution. | | [Positions](/websockets/channels/positions/) | `positions:{user_id}` | `all_positions` | `updated_positions`: position opened, changed or closed. | | [Settlements](/websockets/channels/settlements/) | `settlements:{user_id}` | *none* | `new_settlements`: a market you hold settles. | | [Balances](/websockets/channels/balances/) | `balances:{user_id}` | `balances` | `update` and `payment_update`. | | [Account](/websockets/channels/account/) | `account:{user_id}` | all four of the above | everything the five above push. | | [User info](/websockets/channels/user-info/) | `user_info:{user_id}` | *see page* | `user_updated`: profile, limits and account state. | | [Order slip](/websockets/channels/order-slip/) | `order_slip:{user_id}` (also `betslip:{user_id}`, deprecated) | *none* | `order_numbers_batch`: projected cost of orders you registered, as the book moves. `self_match_batch`: one of them became placeable, or stopped being. | The first five take an optional [`market_ids` filter](/websockets/channels/wire-format/#filtering); `balances` takes an optional `account_id` instead. ## Market data channels Not scoped to your account: the same data every participant sees. | Channel | Topic | Pushes | | --- | --- | --- | | [Order book, ticker and trades](/websockets/channels/market-data/) | `orderbook`, `ticker`, `trades` | The aggregated book, per-market price summaries, and executed trades. **The feeds to trade from**: one join covers every market you name. | | [Markets](/websockets/channels/markets/) | `markets` | `market_created` and `market_updated`, for every market. **`market_updated` is a delta**: it carries the market's id, a timestamp and just the fields that changed. | | [Events](/websockets/channels/events/) | `events` | `event`: traded volume per event, across every market on it. **The whole current value, not a delta.** Join carries each event's value so a client paints before the next trade. | | [Market stats](/websockets/channels/market-stats/) | `market_stats` | `market_stats` and `market_stats_snapshot`, the price series for markets you name. **The feed behind a price chart**: history on join, then changed buckets. | | [Market updates](/websockets/channels/market-updates/) | `market_updates` | `created` and `updated`, for markets you [`watch`](/websockets/channels/market-updates/#watching-markets); nothing is pushed until you do. A bandwidth-conscious alternative to `markets`. | :::tip[One call, several channels] A single `POST /api/v1/orders` normally produces four separate pushes, in order: `orders` → `fills` → `positions` → `balances`. Subscribe to all four if you are reconciling state, not just watching orders, or join [`account`](/websockets/channels/account/) once and get all of them. ::: ## Frame format Phoenix channels use a five-element JSON array on the wire: ```json [join_ref, ref, topic, event, payload] ``` | Element | What it is | | --- | --- | | `join_ref` | The `ref` you used when you joined this topic. **Reuse it for every later message to that topic**: a frame carrying the wrong `join_ref` is dropped without a reply, which looks like the server ignoring you. | | `ref` | A per-message id. The reply echoes it, so use it to match replies to requests. | | `topic` | e.g. `orders:`, `orderbook`, `markets` | | `event` | `phx_join`, `heartbeat`, `ping`, `select_market_ids`, or a server event name | | `payload` | A JSON object. Use `{}`, not `""`, when there is nothing to send. | If a channel message or a join appears to be ignored, check the `join_ref` before anything else; it is the most common cause. ## What the socket does not promise **Pushes are batched.** Market data is coalesced: the order book publishes on roughly a 200 ms cadence and market updates about every 2 seconds, so a channel is a stream of current state, not a tick-by-tick tape. **Channels are not ordered relative to each other.** `orders` and `fills` are separate processes: a fill can arrive before the order update that explains it, and several fills can arrive for one order update. Key off ids and reconcile, rather than assuming arrival order means anything. **A cancel does not stop a fill already in flight.** A fill can land after you send a cancel, because someone took the liquidity before the cancel reached the book. Treat a cancel as a request, and the resulting fill or status as the answer. **Balance pushes are event-driven, not price-driven.** `balances` fires when you deposit or withdraw, place or cancel, get filled, or a market settles. It does **not** fire when prices move, so marking your positions to market is your job, from the order book feed. ## Keep the connection alive The server closes a socket that has been **silent for 60 seconds**. Send a heartbeat on the `phoenix` topic well inside that window. Every 15 to 30 seconds is comfortable: ```json ["3","3","phoenix","heartbeat",{}] ``` The heartbeat is per *socket*, not per channel: one heartbeat keeps every channel on that connection alive. Inbound server pushes do **not** reset the timer: a market that is quiet will not keep your socket up, so heartbeat on a timer rather than only when idle. :::caution[Timers] There are **two independent timers**, and the socket heartbeat only resets one of them. | | Resets it | Deadline | Consequence of missing it | | --- | --- | --- | --- | | Socket keep-alive | `heartbeat` on the `phoenix` topic | 60s | The connection closes | | `cancel_on_disconnect` | `ping` on the `orders` topic | **5–20s**, whatever you asked for at join | **Your flagged orders are cancelled** | A 30-second heartbeat keeps the socket up and still misses the `ping` deadline, so your book is cancelled on a connection that never dropped. When `cancel_on_disconnect` is on, send the channel `ping` inside your configured `ping_timeout`, and send it on a timer, not in response to traffic. See [Risk controls](/risk-controls/) for the join payload and the grace period. ::: ## Use a Phoenix client rather than raw frames The frame format above is documented so you can implement it anywhere, but the [Phoenix client libraries](https://hexdocs.pm/phoenix/js/) already handle `join_ref` bookkeeping, socket heartbeats, and rejoining topics after a reconnect. There are implementations for JavaScript, Python, Java and C#. What they do **not** do for you: sign the handshake, send any channel-level pings a feature asks for, or reconcile a book after a gap. Those are yours either way. ## After a reconnect Rejoin your topics and pull fresh state rather than assuming yours survived: 1. Rejoin every topic (a Phoenix client does this for you) 2. **Replace** the book you hold from the next `orderbook` push rather than merging into it 3. Take a REST snapshot of your live orders (`GET /api/v1/orders?status=created,requested,accepted,delayed,open`) and positions (`GET /api/v1/positions`) and reconcile Step 2 matters because `orderbook` carries no sequence number, so a client cannot tell a gap from a quiet market. Every push is a full snapshot, so replacing is both safe and cheap. --- # Account Channel Source: https://docs.stxapp.io/websockets/channels/account/ Topic: `account:{user_id}` Everything the five per-type channels carry, on one join. The events and payloads are byte-identical to theirs; this is a subscription convenience, not a different feed. ``` ["1","1","account:","phx_join",{}] ``` ## What arrives On join you receive four snapshots: | Event | Payload | Documented at | | --- | --- | --- | | `all_orders` | `{"orders": [...]}` | [Orders](/websockets/channels/orders/) | | `all_trades` | `{"trades": [...]}` | [Fills](/websockets/channels/fills/) | | `all_positions` | `{"positions": [...]}` | [Positions](/websockets/channels/positions/) | | `balances` | the balances object | [Balances](/websockets/channels/balances/) | There is no settlements snapshot, because [`settlements`](/websockets/channels/settlements/) does not send one either. After that, changes arrive under the same event names the per-type channels use, so nothing needs re-tagging if you migrate to this topic: | Event | Carries | | --- | --- | | `new_open_order` | one order | | `trade` | one of your executions | | `updated_positions` | changed positions | | `new_settlements` | new settlements | | `update` | the balances object | | `payment_update` | a payment status change | `account:` does not accept an `account_id`; it serves the account the socket resolved at connect. To reach a second account you own, join [`balances`](/websockets/channels/balances/#naming-an-account) separately for it. ## Filtering by market It takes the same optional `market_ids` filter as the per-type channels (see [filtering](/websockets/channels/wire-format/#filtering)), with one deliberate difference: **balance frames are never filtered out.** `balances`, `update` and `payment_update` describe the account, not a market, so a `market_ids` filter has nothing to match them against and lets them through. ``` ["1","1","account:","phx_join",{"market_ids":[""]}] ["1","2","account:","select_market_ids",{"market_ids":null}] ``` The filter distinguishes a snapshot from a delta. On join you receive `all_orders` even when the filter leaves it empty: `[]` tells you there is nothing in those markets, which is worth knowing. Afterwards, an update the filter empties is simply not sent, rather than arriving as an empty list. ## Keeping it alive ``` ["1","3","account:","ping",{}] ``` The `ping` refreshes the session behind the channel. Without it the inactivity timer reaps that session and every fanned-in feed silently stops, so send it on a timer even though this topic arms no cancellation. ## Use Cases | Use case | Message to send | | --- | --- | | Join and receive your whole account | `["3","3","account:","phx_join",{}]` | | Join filtered to two markets | `["3","3","account:","phx_join",{"market_ids":["",""]}]` | | Change the market filter without rejoining | `["3","4","account:","select_market_ids",{"market_ids":null}]` | | Keep the session alive | `["3","5","account:","ping",{}]` | :::caution[Do not also join a per-type channel] A socket joined to both `account:` and, say, `orders:` receives every order twice. Pick one. ::: :::danger[No cancel-on-disconnect] `account:` does not support `cancel_on_disconnect`. Its `ping` keeps the session alive but arms no cancellation, so if you rely on resting orders being pulled when your socket drops, use [`orders`](/websockets/channels/orders/) and the per-type channels instead, for everything, not alongside `account:`. See [Risk controls](/risk-controls/) for the join payload and its `ping` contract. ::: ## Choosing between them `account:` if you want everything on one join; the per-type channels if you want a subset without a filter, or if you need `cancel_on_disconnect`. --- # Balances Channel Source: https://docs.stxapp.io/websockets/channels/balances/ Topic: `balances:{user_id}` Delivers one account's balance and fee summary as it changes, plus notifications about your payments. Money arrives as dollar strings; see [Wire format](/websockets/channels/wire-format/). This channel is scoped to an account rather than to markets, so it takes no `market_ids` filter. It takes an optional `account_id` instead. ## Balances object ### Identity - `account_id` : The id of the account. - `user_id` : The id of the user that owns the account. ### Balances and liabilities - `account_balance` : The account's cash balance. Unaffected by placing an order; not all of it may be available. - `available_balance` : The balance available to withdraw or place additional orders with. - `buy_order_liability` : The account's total liability from buy orders, including the potential trade fee reserve. - `sell_order_liability` : The account's total liability from sell orders, including the potential trade fee reserve. - `position_premium_liability` : The account's total liability from position premiums. Routinely negative. - `escrow` : The account's escrow balance. :::caution[These four are rounded, in opposite directions] `available_balance` is rounded **down** to the cent and the three liabilities **up**, in both signs, so neither overstates what you can spend nor understates what you owe. They therefore need not reconcile to the cent; treat each as authoritative on its own rather than deriving one from the others. ::: ### Lifetime totals - `total_deposits` : The account's total deposits. - `total_withdrawals` : The account's total withdrawals. - `total_adjustments` : The account's total balance adjustments. - `total_settlement_pnl` : Total gross profit and loss from all settlements. - `total_fees` : Total fees from all settlements and other fees. - `total_trade_count` : The number of trades the account has made across every market. An integer. - `total_traded` : The risk the account has committed across every market. ### Loyalty - `loyalty_tier` : The tier the account is in, one of `rookie`, `veteran`, `all_star`, `mvp` or `hall_of_fame`. Every account has one; new accounts start at `rookie`. - `points` : Loyalty points the account has accumulated. A whole number, not a money string. ### Fees - `fee_schedule` : The account's fee schedule, one of `fixed_percent`, `revenue_share`, `loyalty_tier`, `fixed_percent_market_group`, `fixed_percent_event`, `on_trade` or `loyalty_tier_on_trade`. See [Fees](/concepts/fees/). - `base_fee_percent` : The fee percentage, when the account is on the `fixed_percent` schedule. Null for any other schedule. A number, not a money string. - `taker_factor` / `maker_factor` : Per-trade fee factors, present only for the `on_trade` and `loyalty_tier_on_trade` schedules. Null otherwise. Numbers, not money strings. ## Use Cases | Use case | Message to send | | --- | --- | | Join and receive the balance summary for your account | `["3","3","balances:","phx_join",{}]` | | Join for a specific account you own | `["3","3","balances:","phx_join",{"account_id":""}]` | | Check the connection is alive | `["3","4","balances:","ping",{}]` | ### Joining ```json ["3","3","balances:","phx_join",{}] ``` The reply is empty; there is no filter to echo: ```json {"status":"ok","response":{}} ``` ### Naming an account A user may hold more than one account. Omit `account_id` and you get the one the socket resolved when it connected; name one to reach another: ```json ["3","3","balances:","phx_join",{"account_id":""}] ``` `GET /api/v1/me` returns your `account_id`. An id belonging to another user (or one that is not a valid UUID) fails the join with `unauthorized`, which does not reveal whether the account exists: ```json {"status":"error","response":{"reason":"unauthorized"}} ``` If you join twice, once per account, each socket receives only its own account's frames. ### Initial response after joining The join event is `balances`, and it carries the same object that later `update` frames carry. ```json [null, null, "balances:a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "balances", Balances] ``` ```json [ null, null, "balances:a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "balances", { "account_id": "c1c3614c-32ae-4bea-bae4-fc3f52047790", "user_id": "7fb55544-3a7e-4f6b-bd66-fc89bd3a1087", "account_balance": "154.7500", "available_balance": "91.3000", "buy_order_liability": "20.4500", "sell_order_liability": "28.0000", "position_premium_liability": "15.0000", "escrow": "0.0000", "total_deposits": "150.0000", "total_withdrawals": "0.0000", "total_adjustments": "0.0000", "total_settlement_pnl": "5.0000", "total_fees": "0.2500", "total_trade_count": 4, "total_traded": "185.8220", "loyalty_tier": "rookie", "points": 186, "fee_schedule": "on_trade", "base_fee_percent": null, "taker_factor": 0.02, "maker_factor": 0.01 } ] ``` ### Pushed when the summary changes ```json [null, null, "balances:a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "update", Balances] ``` ```json [ null, null, "balances:a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "update", { "account_id": "c1c3614c-32ae-4bea-bae4-fc3f52047790", "user_id": "7fb55544-3a7e-4f6b-bd66-fc89bd3a1087", "account_balance": "159.5000", "available_balance": "91.0500", "buy_order_liability": "20.4500", "sell_order_liability": "28.0000", "position_premium_liability": "20.0000", "escrow": "0.0000", "total_deposits": "150.0000", "total_withdrawals": "0.0000", "total_adjustments": "0.0000", "total_settlement_pnl": "10.0000", "total_fees": "0.5000", "total_trade_count": 5, "total_traded": "218.4400", "loyalty_tier": "rookie", "points": 218, "fee_schedule": "on_trade", "base_fee_percent": null, "taker_factor": 0.02, "maker_factor": 0.01 } ] ``` :::caution[Balance pushes are event-driven, not price-driven] This channel fires when you deposit or withdraw, place or cancel, get filled, or a market settles. It does **not** fire when prices move, so marking your [positions](/websockets/channels/positions/) to market is your job. ::: ### Pushed when a payment's status changes Sent as your payments are processed. - `id` : The id of the payment. - `account_id` : The id of the account the payment is for. - `provider` : The name of the payment provider, e.g. `paysafe`. - `provider_id` : The transaction id from the provider. - `status` : `pending_approval`, `initiated`, `received`, `pending`, `held`, `completed`, `failed` or `cancelled`. - `data` : Provider-specific details about the payment. Shape varies by provider and payment method. - `type` : `deposit`, `withdrawal` or `adjustment`. - `amount` : The payment amount, as a dollar string. ```json [ null, null, "balances:a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "payment_update", { "id": "73335df1-37ef-434d-aa4e-7ba1f985ca00", "account_id": "b6e736c0-926f-4150-b613-fe3da9ef2a3f", "provider": "paysafe", "provider_id": "25dd5124-11f5-46fa-b49e-48acb6bb53d1", "status": "completed", "data": {"handle": "SCtzjJMtI2CrWKSk", "method": "card"}, "type": "deposit", "amount": "100.0000" } ] ``` Interac SendMoney deposits are not pushed at `initiated` or `pending`; the first frame you see for one is at a later status. --- # Event volume (events) Source: https://docs.stxapp.io/websockets/channels/events/ Topic: `events` Traded volume per event: one number covering every market on it, so a client showing "$12,430 vol" on an event card need not fetch that event's markets and add them up. One topic covers every event, narrowed by the join payload. Ten events is one join, not ten. ## Joining ``` ["1","1","events","phx_join",{"event_ids":["",""]}] ``` **At least one valid `event_id` is required.** As on `orderbook`, an absent or unusable list is an error, not "no filter"; every event on the exchange is not served here. ```json {"status":"error","response":{"reason":"event_ids_required"}} ``` The reply echoes the filter and carries each event's current value, so a client paints before the next trade: ```json {"status":"ok","response":{ "selected_event_ids":[""], "events":[{"event_id":"","volume":"12430.0000"}]}} ``` An event with nothing recorded is **omitted from `events`**, not reported as zero, so "not known yet" stays distinct from "nothing traded". Its id is still echoed in `selected_event_ids`. `orderbook` and `ticker` send no snapshot because their producers republish on a cadence. Event volume moves only on a trade, so without this a client sits blank while the event is quiet. ## Server pushes `event`, one message per event whose value changed: ```json {"event_id":"...","volume":"12455.0000"} ``` `volume` is the event's **lifetime traded volume in dollars** across every active market on it, a dollar string; see [Wire format](/websockets/channels/wire-format/#the-format). **Every push is the whole current value, not a delta.** Replace what you hold for that `event_id`; never add to it. Pushes are coalesced to at most one per event per second, and an unchanged figure pushes nothing. :::tip[Ignore keys you do not know] Fields will be added: market counts, open interest, event status. Treat an unknown key as ignorable rather than an error and a client written today keeps working. ::: ## Changing which events stream ``` ["1","2","events","select_event_ids",{"event_ids":[""]}] ``` Same non-empty rule; an unusable list keeps the current selection and replies with an error. The reply has the join reply's shape, current values included. ## Use Cases | Use case | Message to send | | --- | --- | | Join for two events | `["3","3","events","phx_join",{"event_ids":["",""]}]` | | Change which events stream | `["3","4","events","select_event_ids",{"event_ids":[""]}]` | | Keep the channel alive | `["3","5","events","ping",{}]` | --- # Fills Channel Source: https://docs.stxapp.io/websockets/channels/fills/ Topic: `fills:{user_id}` Delivers **your** executions as they trade and settle. Money arrives as dollar strings and contract counts as quantity strings; see [Wire format](/websockets/channels/wire-format/). To look up what one order filled at after the fact, call [`GET /api/v1/fills?order_ids=`](/api/rest/fills/list-fills/). It takes several comma-separated order ids, combines with `market_ids` and `status`, and pages by `cursor` like the unfiltered list. :::caution[`fills` is not the market-wide `trades` topic] `fills:{user_id}` is yours. [`trades`](/websockets/channels/market-data/#trades) is every participant's, anonymized. They are different feeds and are not interchangeable. ::: ## Fill object ### Identity and provenance - `id` : The unique id of the trade. - `market_id` : The id of the market the trade is on. - `order_id` : The id of the order this trade belongs to. - `client_order_id` : The client-supplied id of the order that created this trade, if any. - `liquidity_action` : Whether the linked order was a liquidity `provider` or `taker`. - `device_id` : The device id from the order that created this trade. Null if the order didn't carry one. - `ip_address` : The IP address from the order that created this trade. Null if the order didn't carry one. ### Lifecycle - `action` : Whether the trade is a `buy` or a `sell`. - `status` : `created`, `open`, `settled` or `cancelled`. - `time` : When the trade was created, ISO 8601. - `inserted_at` : The same instant, as an integer in Unix microseconds. - `settled_at` : When the trade's status became `settled`, ISO 8601. Null until then, and stays null if the trade is cancelled instead. - `expires_at` : When the traded contracts expire, as an integer in Unix microseconds. Null when unknown. - `amended` : Whether STX has changed the trade's price after it executed. - `settlements_count` : The number of settlements where this trade was the opening side. An integer, not a quantity string. ### Contracts All quantity strings. - `filled` : The number of contracts this trade represents. Can be fractional. - `remaining` : How many of the trade's contracts are still unsettled. - `closing` : How many of the trade's contracts close an opposing position, rather than opening a new one. Zero for a purely opening trade. - `closed_contracts` : How many contracts were closed by a later trade. - `expired_contracts` : How many contracts were settled at market expiry. ### Price and premium All dollar strings. - `price` : The price the trade executed at. - `premium` : The premium paid or received for the trade (`filled` × `pc_premium`). - `pc_premium` : The premium per contract. On a **sell** this is `price`, the premium you receive; on a **buy** it is `-price`, the premium you pay, so this field is negative on every buy. - `pc_risk` : The risk per contract: `price` on a buy, `max_price - price` on a sell. - `pc_to_win` : What each contract gains if the position wins: `max_price - price` on a buy, `price` on a sell. It is **not** the same as `pc_risk`; the two are only equal at the midpoint. - `remaining_premium` : The premium on the unsettled portion. - `remaining_risk` : The risk on the unsettled portion. - `remaining_to_win` : What the unsettled portion gains if the position wins. - `original_premium` : The premium the trade represented once finalized, excluding any portion later used to close an opposing position. - `original_risk` : The same, for risk. - `original_to_win` : The same, for the gain if the position wins. ### Profit, loss and fees All dollar strings. - `gross_pnl` : The trade's profit or loss before fees, accruing as settlements land. - `closed_pnl` : Profit or loss from contracts closed by a later trade. - `expired_pnl` : Profit or loss from contracts settled at market expiry. - `trade_fee` : The fee charged for this trade specifically. - `total_fee` : The all-in fee: `trade_fee` plus the settlement fees this trade has incurred. **This is the fee to display.** See [`total_fee`](/websockets/channels/wire-format/#total_fee-is-the-all-in-fee). - `closed_fee` : The fee paid on contracts closed by a later trade. - `expired_fee` : The fee paid on contracts settled at market expiry. - `remaining_potential_fee` : The maximum fee that could still be charged on the unsettled portion. - `max_potential_fee` : Identical to `remaining_potential_fee`. It does not include fees already charged on the settled portion. ### Loyalty - `points` : Loyalty points earned from the trade, updated as it settles. A number rounded to two places, not a money string. ## Use Cases | Use case | Message to send | | --- | --- | | Join and receive your open trades | `["3","3","fills:","phx_join",{}]` | | Join filtered to one market (see [filtering](/websockets/channels/wire-format/#filtering)) | `["3","3","fills:","phx_join",{"market_ids":[""]}]` | | Change the market filter without rejoining | `["3","4","fills:","select_market_ids",{"market_ids":null}]` | | Check the connection is alive | `["3","5","fills:","ping",{}]` | ### Joining ```json ["3","3","fills:","phx_join",{}] ``` ```json {"status":"ok","response":{"selected_market_ids":null}} ``` ### Initial response after joining ```json [null, null, "fills:a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "all_trades", { "trades": [Fill]}] ``` `trades` is a list of `Fill` objects, described above, empty if you have none open, and also empty when a `market_ids` filter matches nothing. ```json [ null, null, "fills:a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "all_trades", { "trades": [ { "id": "787582ec-3863-4760-bbcb-398d3dca8fe5", "market_id": "687e9cdb-a391-4118-aa80-1122bb14779f", "order_id": "c1c3614c-32ae-4bea-bae4-fc3f52047790", "client_order_id": null, "liquidity_action": "provider", "device_id": "web-chrome-114", "ip_address": "203.0.113.42", "status": "open", "time": "2026-11-01T19:20:34.890853Z", "inserted_at": 1793640034890853, "settled_at": null, "expires_at": 1825736399999999, "amended": false, "settlements_count": 1, "action": "sell", "filled": "20.00", "remaining": "10.00", "closing": "0.00", "closed_contracts": "10.00", "expired_contracts": "0.00", "price": "0.2400", "premium": "4.8000", "pc_premium": "0.2400", "pc_risk": "0.7600", "pc_to_win": "0.2400", "remaining_premium": "2.4000", "remaining_risk": "7.6000", "remaining_to_win": "2.4000", "original_premium": "4.8000", "original_risk": "15.2000", "original_to_win": "4.8000", "gross_pnl": "1.2000", "closed_pnl": "1.2000", "expired_pnl": "0.0000", "trade_fee": "0.0600", "total_fee": "0.1200", "closed_fee": "0.0600", "expired_fee": "0.0000", "remaining_potential_fee": "0.3800", "max_potential_fee": "0.3800", "points": 20.0 } ] } ] ``` ### Pushed when a trade is created or updated One frame **per trade**, not a batch, including trades that just transitioned to `settled` or `cancelled`, so clients can drop them from their active view. An order that sweeps several resting orders therefore produces several frames. ```json [null, null, "fills:a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "trade", Fill] ``` ```json [ null, null, "fills:a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "trade", { "id": "787582ec-3863-4760-bbcb-398d3dca8fe5", "market_id": "687e9cdb-a391-4118-aa80-1122bb14779f", "order_id": "c1c3614c-32ae-4bea-bae4-fc3f52047790", "client_order_id": null, "liquidity_action": "provider", "device_id": "web-chrome-114", "ip_address": "203.0.113.42", "status": "settled", "time": "2026-11-01T19:20:34.890853Z", "inserted_at": 1793640034890853, "settled_at": "2026-11-01T19:25:10.221450Z", "expires_at": 1825736399999999, "amended": false, "settlements_count": 2, "action": "sell", "filled": "20.00", "remaining": "0.00", "closing": "0.00", "closed_contracts": "20.00", "expired_contracts": "0.00", "price": "0.2400", "premium": "4.8000", "pc_premium": "0.2400", "pc_risk": "0.7600", "pc_to_win": "0.2400", "remaining_premium": "0.0000", "remaining_risk": "0.0000", "remaining_to_win": "0.0000", "original_premium": "4.8000", "original_risk": "15.2000", "original_to_win": "4.8000", "gross_pnl": "3.0000", "closed_pnl": "3.0000", "expired_pnl": "0.0000", "trade_fee": "0.0600", "total_fee": "0.2100", "closed_fee": "0.1500", "expired_fee": "0.0000", "remaining_potential_fee": "0.0000", "max_potential_fee": "0.0000", "points": 20.0 } ] ``` --- # Order book, ticker and trades Source: https://docs.stxapp.io/websockets/channels/market-data/ Three topics carry market data, the same for every participant, in the same [wire format](/websockets/channels/wire-format/) as the account channels and the [REST API](/api/rest/). | Topic | Carries | Filters | | --- | --- | --- | | `orderbook` | aggregated book levels | `market_ids` (**required**) | | `ticker` | per-market price summary | `sports`, `competitions` | | `trades` | executed trades, anonymous | `market_ids`, `event_ids` | Each is a **single topic covering every market**, narrowed by the join payload. Watching ten markets is one join, not ten. ## Field names Money is a dollar string (`"0.3900"`), quantities are quantity strings (`"100.00"`), and counts are plain integers. Field names are `snake_case` throughout, and the offer side is called `offer`, not `ask`, the same names the REST market payload uses. ## Filters Every filter follows one contract: - omitted, `null`, `[]`, or a list with no usable entry means **no filter**; - unusable entries are dropped rather than rejecting the join; - the join reply echoes what was actually applied, so compare it against what you sent to catch a typo; - naming two filters **narrows**: a message must match both; - `select_filters` (or `select_market_ids` on `orderbook`) changes them without rejoining. `orderbook` is the one exception: it requires at least one valid `market_id`. ## `orderbook` ``` ["1","1","orderbook","phx_join",{"market_ids":["",""]}] ``` An absent or unusable list is an error, not "no filter": ```json {"status":"error","response":{"reason":"market_ids_required"}} ``` Pushes `"book"` on the matching engine's publish cadence, one message per market: ```json { "market_id": "...", "bids": [{"price":"0.3900","quantity":"100.00","liquidity":"39.0000", "total_quantity":"100.00","total_liquidity":"39.0000"}], "offers": [], "timestamp": "2026-09-02T21:16:39.717812Z", "timestamp_us": 1788383799717812 } ``` Levels are best-first. `liquidity` is that level alone; `total_quantity` and `total_liquidity` are cumulative through it. :::caution[Every push is a full snapshot] This is not a delta protocol. Replace the book you hold for that `market_id` wholesale on each message rather than applying it incrementally. ::: Change markets without rejoining: ``` ["1","2","orderbook","select_market_ids",{"market_ids":[""]}] ``` The non-empty requirement still applies; an unusable list leaves the current selection in place and replies with an error. ## `ticker` ``` ["1","1","ticker","phx_join",{}] ["1","1","ticker","phx_join",{"sports":["Football"],"competitions":["NFL"]}] ``` Pushes `"ticker"` for each market whose price, book top, volume or open interest moved: ```json { "market_id": "...", "market_symbol": "STXNFL-...", "event_id": "...", "event_symbol": "STXNFL-...", "sport": "Football", "competition": "NFL", "last_traded_price": "0.3900", "last_traded_quantity": "100.00", "best_bid": "0.3800", "best_bid_quantity": "250.00", "best_offer": "0.4000", "best_offer_quantity": "175.00", "bid_depth": 4, "offer_depth": 6, "open_interest": "1200.00", "total_volume": "48000.00", "timestamp": "...", "timestamp_us": 1788383799717812 } ``` `bid_depth` and `offer_depth` count price levels, so they are integers rather than quantity strings. Any field can be `null` on a market that has not traded or has an empty side of the book. `event_symbol` is `null` while the event list is still warming. Filter values are matched exactly as the market carries them, so `"Football"` and `"football"` are different. :::tip[No snapshot on join] This is a change feed; nothing arrives until a market moves. Fetch `GET /api/v1/markets` for the initial state, then keep it current from here. ::: ## `trades` ``` ["1","1","trades","phx_join",{}] ["1","1","trades","phx_join",{"market_ids":[""],"event_ids":[""]}] ``` Pushes `"trade"`, one per execution: ```json { "market_id": "...", "market_symbol": "STXNFL-...", "event_id": "...", "event_symbol": "STXNFL-...", "price": "0.3200", "quantity": "100.00", "action": "buy", "timestamp": "...", "timestamp_us": 1788383799717812 } ``` `action` is the **taker's** side: `"buy"` when the incoming order bought from the book, `"sell"` when it sold into it. This feed is anonymous: it carries no account, user or order identifier for any market participant. :::caution[`trades` is not `fills:{user_id}`] `trades` is every trader's executions; [`fills:{user_id}`](/websockets/channels/fills/) is yours. They differ by one letter and are not interchangeable. ::: --- # Price history (market_stats) Source: https://docs.stxapp.io/websockets/channels/market-stats/ Topic: `market_stats` Price over time for the markets you name, as a series of buckets. This is the feed behind a price chart: the whole history arrives in the join reply, then only changed buckets are pushed. One topic covers every market, narrowed by the join payload. Ten markets is one join, not ten. :::note Unrelated to `GET /api/v1/account/market_stats`, which returns your own position and P&L statistics. This channel carries prices, not your account. ::: ## Joining ``` ["1","1","market_stats","phx_join",{"market_ids":["",""]}] ["1","1","market_stats","phx_join",{"market_ids":[""],"range":"week"}] ``` **At least one valid `market_id` is required.** Unlike the per-account filters on this socket, an absent or unusable list is an error rather than "no filter": the full history of every market on the exchange is not something this serves. ```json {"status":"error","response":{"reason":"market_ids_required"}} ``` `range` sets how far back the join snapshot reaches: `"day"`, `"week"`, `"month"` or `"all"` (the default). An unknown value falls back to `"all"`. The reply echoes the filter and range, and carries the series: ```json {"status":"ok","response":{ "selected_market_ids":[""], "range":"all", "markets":[{"market_id":"","points":[ {"timestamp_us":1789671360000000,"price_percent":43.5}]}]}} ``` A market id naming no market is echoed in `selected_market_ids` but contributes no entry to `markets`. ## Points `price_percent` is the bucket's closing price as a **percent of that market's `max_price`**, so a plain JSON number from `0.0` to `100.0` rather than money. It is not `probability` from the market payload, which is a modeled value from the pricing feed. `timestamp_us` is the bucket's start, in Unix microseconds. Points ascend by it, and a bucket with no price is omitted rather than sent as zero. No volume is carried: this is a price series. Per-market traded volume is `total_volume` on [`ticker`](/websockets/channels/market-data/). ## Server pushes - `market_stats` carries `{"market_id":"...","points":[point]}`, the buckets that changed. **A delta, not a snapshot.** Buckets are 60 seconds wide but flush every 2 seconds or so, so the same `timestamp_us` is re-sent as the current minute fills: **upsert by `timestamp_us` rather than appending.** - `market_stats_snapshot` has the same shape and replaces that market's series wholesale. Sent after STX cancels a trade, which rewrites buckets already delivered and cannot be reconciled from a delta. Sent even when the series is now empty. ## Changing which markets stream ``` ["1","2","market_stats","select_market_ids",{"market_ids":[""]}] ``` The same non-empty requirement applies; an unusable list leaves the current selection in place and replies with an error. This changes the subscription only and sends no series, so adding a market cannot overwrite a window you set with `request_series`. Fetch the new market's history with that instead. ## Fetching history at another range ``` ["1","3","market_stats","request_series",{"market_ids":[""],"range":"day"}] ``` Replies with series for exactly those markets at that range: ```json {"status":"ok","response":{"range":"day","markets":[{"market_id":"","points":[]}]}} ``` The subscription is untouched. Range belongs to one history request rather than to the socket, so a client drawing three markets over three windows is one join plus three of these. The ids need not be subscribed, since price history is the same for every participant. Live deltas are range-independent: every bucket update for a subscribed market is pushed whatever range was last requested. ## Use Cases | Use case | Message to send | | --- | --- | | Join for two markets | `["3","3","market_stats","phx_join",{"market_ids":["",""]}]` | | Join with a week of history | `["3","3","market_stats","phx_join",{"market_ids":[""],"range":"week"}]` | | Change which markets stream | `["3","4","market_stats","select_market_ids",{"market_ids":[""]}]` | | Fetch one day for a chart | `["3","5","market_stats","request_series",{"market_ids":[""],"range":"day"}]` | | Keep the channel alive | `["3","6","market_stats","ping",{}]` | --- # Watched markets (market_updates) Source: https://docs.stxapp.io/websockets/channels/market-updates/ Delivers live updates for markets you explicitly subscribe to: a bandwidth conscious alternative to the [`markets`](/websockets/channels/markets/) channel, which broadcasts every market to every subscriber. There's no snapshot on join: the full market list is available from `GET /api/v1/markets`. This channel only pushes changes, and only for the markets you [watch](#watching-markets). ## Payload fields The fields below use full names; nothing here is abbreviated. They are a subset of the [`markets`](/websockets/channels/markets/) channel's payload: `featured`, `featured_home`, `stat_detail`, `event_title` and `event_short_title` are sent there and not here. The market's id is sent under both `market_id` and `id`, and the change time is sent as both an ISO 8601 `timestamp` and a `unix_timestamp` in microseconds. Pick whichever pair suits your client. :::caution[Prices on this channel are in cents] As on [`markets`](/websockets/channels/markets/), `max_price`, `price`, `last_traded_price` and the `price` inside `bids`, `offers` and `recent_trades` are **JSON numbers in cents**, not the dollar strings the REST API sends. A market whose `max_price` is `"1.0000"` over REST reads `100` here, and `price` is sent with one decimal place (`53.0`). Divide by 100 before placing an order with one of these values. ::: ### Fields set when the market is created - `market_id` / `id` : The market's id, sent under both keys. - `event_id` : The id of the event the market is attached to. - `symbol` : A unique symbol string for the market. - `description` : The market's description. - `title` : The market's human-readable title. - `short_title` : The market's human-readable short title. - `group_title` : The market's human-readable title used for grouping in the UI. - `grouping_id` : Identity of the set of mutually exclusive outcomes the market belongs to. Stable for the life of the market, and shared by its siblings. Sent when the full object is sent; absent from a change-only push, since it never changes. - `grouping_name` : The same grouping in words, e.g. `Over/Under`. For display only: two different groupings can render the same name. - `question` : The question the market is asking. - `position` : The text used to describe the position. - `event_type` : The type of event the market is attached to. - `sport` : The sport the event is in, e.g. `Basketball`, `Tennis`. - `competition` : The competition the event is in, e.g. `NFL`, `US Open`. - `participants` : The market's participants (teams or opponents). Structure depends on the event type. - `keywords` : Keywords associated with the market, such as team mascot names. - `rules` : The rules that govern the market. - `specifier` : The specifier for `rules`, used to determine the market's result and status. Null if the rule doesn't need one. - `max_price` : The settlement value of one winning contract, in cents, and the ceiling on order prices: an order must price strictly below it. `100` on a $1 market. Read it per market. - `order_price_rules` : Price ranges, in cents, and the step a price display uses within each (see below). - `sort` : A list of fields to use for the market's default sorting. - `in_play_delay_sec` : The delay, in seconds, orders wait in queue while the event is `in_progress`. #### Order Price Rules An array describing how the order price can change within given price ranges: ```json [ {"from": 1, "to": 19, "inc": 1}, {"from": 20, "to": 79, "inc": 10}, {"from": 80, "to": 99, "inc": 1} ] ``` This example is for a market whose `max_price` is `100` cents. Between 1 and 19 cents (inclusive; 0 is never a valid price) a price steps by 1 cent. Between 20 and 79 it steps by 10 cents, and between 80 and 99 by 1 cent. The last range ends one cent below `max_price`, the highest valid order price. The ranges are derived from `max_price`, so read them per market. Any whole-cent price below `max_price` is a valid order price. ### Fields that change as the market trades - `timestamp` / `unix_timestamp` : When this change happened, sent as both an ISO 8601 string and a unix timestamp in microseconds. - `status` : The market's status (`scheduled`, `pre_open`, `open`, `suspended`, `closed`, `resulted`, `cancelled`, `voided`). See [Market and order status](/concepts/market-status/). - `result` : The market's result (`pending`, `won`, `lost`, `void`, `settled`, `push`). - `settled_at` : When the market was resulted or voided, UTC. - `archived` : Whether the market is archived. - `trading` : Whether trading is enabled on the market. - `filters` : The filters the market appears under. - `trading_filters` : The filters used for organizing trades, settlements and related items. - `home_category` : The category the market appears in. It lets a client tell `Upcoming`, `Live` and uncategorized markets apart. - `event_status` : The status of the event the market is attached to (`scheduled`, `in_progress`, `completed`, `cancelled`). - `event_start` : The event's start time, as an integer of Unix microseconds. - `event_brief` : A brief string for the market's event. - `detailed_event_brief` : A more detailed brief string for the market's event. - `last_traded_price` : The price of the last executed trade, in cents. - `volume_24h` : The volume traded on the market in the last 24 hours. - `total_volume` : The total volume traded on the market: the sum of `quantity` across all of its trades. - `price_change_24h` : The change in price over the last 24 hours. - `recent_trades` : The market's last 15 trades. Each `price` is in cents. - `bids` : The market's top bids, sorted by price descending, so the best bid is first. Each entry has a `price` in cents and the accumulated `quantity` at that price. - `offers` : The market's top offers, also sorted by price descending, so the best offer is **last**. Same structure as `bids`. - `price` : The price the market is currently trading at, in cents. - `probability` : The market's effective win probability, from the pricing feed or set manually. - `manual_probability` : Whether `probability` was set manually rather than from the pricing feed. - `last_probability_at` : When the last probability was received from the feed. ## Use Cases | Use case | Message to send | | --- | --- | | Join the channel (no snapshot is pushed) | `["3","3","market_updates","phx_join",{}]` | | Watch specific markets for updates | `["4","4","market_updates","watch",["017511eb-930b-492a-8933-2284067e3039"]]` | | Check the connection and see your current watches | `["4","5","market_updates","ping",{}]` | ### Joining ```json ["3","3","market_updates","phx_join",{}] ``` ### Watching markets Joining alone gets you nothing; you have to tell the channel which markets to watch. After that, `created` and `updated` events for those markets are pushed to you as they happen. Should you reconnect for any reason, `watch` needs to be re-submitted after joining. Request: ```json ["4","4","market_updates","watch",["017511eb-930b-492a-8933-2284067e3039"]] ``` Response: ```json [ "4", "4", "market_updates", "phx_reply", { "response": { "subscriptions": { "updates": "level_1", "watches": ["017511eb-930b-492a-8933-2284067e3039"] } }, "status": "ok" } ] ``` If `watch`'s payload isn't a list of market ids, you get an error reply instead. ### Event: "created" Pushed for a watched market when it changes state from `scheduled` to `pre_open`, carrying every field listed under [Payload fields](#payload-fields). ```json [ null, null, "market_updates", "created", { "market_id": "db5a4f48-764c-4ae7-9b72-e0c23666f3e3", "id": "db5a4f48-764c-4ae7-9b72-e0c23666f3e3", "event_id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301", "symbol": "SOCCER-tourney-2208050938-tx-mt-RG219M", "description": "Contracts for this market settle into $1 if the montana beat the texas and settle into $0 if they do not.", "title": "tourney tx @ mt", "short_title": "tx @ mt", "group_title": "tx @ mt", "grouping_id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301:moneyline:full", "grouping_name": "Moneyline", "question": "Will the montana defeat the texas?", "position": "montana", "event_type": "soccer_game", "sport": "Soccer", "competition": "tourney", "participants": ["tx", "mt"], "keywords": ["montana", "texas"], "rules": "home_winner", "specifier": null, "max_price": 100, "order_price_rules": [ {"from": 1, "to": 19, "inc": 1}, {"from": 20, "to": 79, "inc": 10}, {"from": 80, "to": 99, "inc": 1} ], "sort": ["event_start", "price", "short_title"], "in_play_delay_sec": 5, "timestamp": "2022-08-05T13:39:22.639479Z", "unix_timestamp": 1659555562639479, "status": "pre_open", "result": "pending", "settled_at": null, "archived": false, "trading": true, "filters": [ {"category": "tourney", "manual": false, "section": "Upcoming", "subcategory": "Games"} ], "trading_filters": [ {"category": "tourney", "manual": false, "section": "tx", "subcategory": "Teams"}, {"category": "tourney", "manual": false, "section": "mt", "subcategory": "Teams"} ], "home_category": "Upcoming", "event_status": "scheduled", "event_start": 1659706680000000, "event_brief": "Aug 05 at 09:38 AM EDT", "detailed_event_brief": "Aug 05 at 09:38 AM EDT", "last_traded_price": null, "volume_24h": null, "total_volume": null, "price_change_24h": null, "recent_trades": [], "bids": [], "offers": [], "price": null, "probability": 0.677168, "manual_probability": false, "last_probability_at": "2022-08-05T13:39:22.639637Z" } ] ``` ### Event: "updated" Pushed for a watched market whenever it changes. Only the fields that changed are included, alongside the market's id and the change time: `market_id`, `id`, `timestamp` and `unix_timestamp` are always present. ```json [ null, null, "market_updates", "updated", { "market_id": "d6ea3806-1b32-44b4-87f1-9edff2e41cb3", "id": "d6ea3806-1b32-44b4-87f1-9edff2e41cb3", "timestamp": "2022-08-03T21:32:06.239764Z", "unix_timestamp": 1659555126239764, "bids": [{"price": 10, "quantity": 2}] } ] ``` --- # All markets (markets) Source: https://docs.stxapp.io/websockets/channels/markets/ Delivers live updates to market metadata as it changes: status transitions, price and probability moves, top-of-book, filters, and similar. This channel does not push the initial market list: get that from `GET /api/v1/markets`, then join here for what changes afterward. #### MarketPayload fields Every push (`market_created` or `market_updated`) is a map from market UUID to a payload object: a fixed set of fields, converted to wire types (UUIDs to strings, prices to cents). `market_created` always includes every field below. :::caution[Prices on this channel are in cents] `max_price`, `price`, `last_traded_price` and the `price` inside `bids`, `offers` and `recent_trades` are **JSON numbers in cents**, not the dollar strings the REST API and the account channels send. On a market whose `max_price` is `"1.0000"` over REST, `max_price` here is `100`, and a REST price of `"0.5300"` is `53`. Divide by 100 before placing an order with one of these values. ::: `market_updated` includes only `market_id`, `timestamp`, `unix_timestamp` plus whichever fields actually changed since the last push. Nothing below is guaranteed to stay fixed for a market's lifetime. Every one of these fields, including `title`, `description` and `symbol`, can be edited by STX, and any such edit is what a later `market_updated` diff reflects. ##### Always present - `market_id` : The id of the market. - `timestamp` : Server time when this payload was generated, as an ISO 8601 string. - `unix_timestamp` : The same instant, as UNIX microseconds. ##### Identity and description - `symbol` : The market's unique symbol, e.g. `STXNBA-26MAR250000CLECHI-GAMECHI`. - `title` : The human readable title for the market. - `short_title` : The human readable short title for the market. - `group_title` : The human readable group title for the market; used in groupings in the app. - `grouping_id` : Identity of the set of mutually exclusive outcomes this market belongs to. Stable for the life of the market, and shared by its siblings. Present on `market_created`; absent from `market_updated`, since it never changes. - `grouping_name` : The same grouping in words, e.g. `Over/Under`. For display only: two different groupings can render the same name. - `description` : The description of the market. - `question` : The question that the market is asking. - `position` : The text to use in describing the position. ##### Event linkage - `event_id` : The id of the event that the market is attached to. - `event_type` : The type of event associated with the market. - `sport` : Sport that the event is in, e.g. `Basketball`, `Tennis`. - `competition` : Competition that the event is in, e.g. `NFL`, `US Open`. - `participants` : Market participants (teams or opponents), an array of `{name, role, short_name, abbreviation}`. Exact fields populated are event type dependent. - `keywords` : Keywords associated with the market, such as team mascot names, used for search. ##### Rules and pricing - `rules` : The rule that governs the market's result and status. - `specifier` : Further specifies `rules` where needed (e.g. the spread line, or which player/stat for a player prop). - `stat_detail` : Decoded detail for player-stat-line markets (player, stat, line); `null` for every other `rules` value. - `max_price` : The settlement value of one winning contract, in cents, and the ceiling on order prices: an order must price strictly below it. `100` on a $1 market. Read it per market. - `order_price_rules` : Price ranges, in cents, and the step a price display uses within each. See below. - `sort` : A list of these field names, in priority order, for the client to use when sorting a market list. ###### Order Price Rules An array of ranges: ```json [{"from": 1, "to": 19, "inc": 1}, {"from": 20, "to": 79, "inc": 10}, {"from": 80, "to": 99, "inc": 1}] ``` This example is for a market whose `max_price` is `100` cents. Between 1 and 19 cents (inclusive; 0 is not a valid order price) a price steps by 1 cent. Between 20 and 79 cents it steps by 10 cents, and between 80 and 99 by 1 cent. The last range ends one cent below `max_price`, the highest valid order price. The ranges are derived from `max_price`, so read them per market rather than assuming these values. They describe the steps STX's own price controls use; any whole-cent price below `max_price` is a valid order price. ##### Status and lifecycle - `status` : The market's status. See [Market and order status](/concepts/market-status/). - `result` : The market's result once known: `pending`, `won`, `lost`, `void`, `settled` or `push`. - `settled_at` : The UTC time when the market was resulted or voided. - `trading` : Whether the market is accepting orders right now; see the `suspended` derivation in [Market and order status](/concepts/market-status/#suspended-is-not-one-of-them). - `in_play_delay_sec` : The delay, in seconds, orders wait in queue while the market's event is `in_progress`. - `archived` : Whether the market is archived. ##### Categorization - `featured` : Whether the market is featured (shown first in the UI). - `featured_home` : Whether the market is featured on the home page. - `filters` : The list of filters the market appears under, for browsing/organizing markets. - `trading_filters` : The list of filters used for organizing trades, settlements and related items. - `home_category` : `Upcoming`, `Live`, or `null` if the market fits neither. ##### Event display - `event_status` : The status of the event this market is attached to. See [Market and order status](/concepts/market-status/#events-have-their-own-statuses). - `event_start` : The start time of the event, as an integer of Unix microseconds. - `event_title` : The title of the event associated with the market. - `event_short_title` : The short title of the event associated with the market. - `event_brief` : A short string describing the current state of the market's event. - `detailed_event_brief` : A longer version of `event_brief`. ##### Trading activity - `last_traded_price` : The price of the last executed trade, in cents. - `volume_24h` : Contracts traded on this market in the last 24 hours. - `total_volume` : Contracts traded on this market across its lifetime. - `price_change_24h` : The change in price over the last 24 hours. - `recent_trades` : The last 15 trades on the market. Each `price` is in cents. - `bids` : The top bids on the market: an array of `{price, quantity}`, price in cents and quantity accumulated at that price. Sorted by price descending, so the best bid is first. - `offers` : The top offers on the market. Same structure as `bids`, also sorted by price descending, so the best offer is **last**. - `price` : The price the market is currently trading at, in cents. ##### Probability - `probability` : The market's effective win probability, whether manual or feed-derived. - `manual_probability` : Whether `probability` was set manually rather than from the pricing feed. - `last_probability_at` : When the server last received a probability update from the feed. #### Joining the Channel Clients join the topic `markets`. An optional join payload controls filtering: ```json { "rule_filters": ["home_winner", "spread"], "message_types": ["market_updated", "market_created"] } ``` - `rule_filters` : Only markets whose `rules` field matches one of these values are delivered. Omit or pass `null` to receive every market. Unknown values are silently ignored; if nothing valid is left, filtering is disabled. - `message_types` : Which broadcast types to receive: `market_updated`, `market_created`, or both. Defaults to both when omitted, invalid, or `null`. The join reply echoes back the resolved configuration: ```json { "available_rules": ["home_winner", "spread", "..."], "selected_rule_filters": ["spread"], "selected_message_types": ["market_updated", "market_created"] } ``` `available_rules` is the full set of rule identifiers you can filter on. #### Changing filters after joining Send `select_rule_filters` or `select_message_types` on the channel at any time to change what you receive, without rejoining. - `select_rule_filters` : `{"rule_filters": [...]}`, or `{"rule_filters": null}` to disable rule filtering. Replies with `{"selected_rule_filters": [...] | null}`. - `select_message_types` : `{"message_types": [...]}`, or `{"message_types": null}` to reset to both types. Replies with `{"selected_message_types": [...]}`. #### Use Cases | Use case | Message to send | | --- | --- | | Join and receive every market, both event types | `["0","1","markets","phx_join",{}]` | | Join filtered to `spread` markets only | `["0","1","markets","phx_join",{"rule_filters":["spread"]}]` | | Join receiving only `market_updated` events | `["0","1","markets","phx_join",{"message_types":["market_updated"]}]` | | Narrow the rule filter after joining | `["0","2","markets","select_rule_filters",{"rule_filters":["home_winner"]}]` | | Disable rule filtering after joining | `["0","3","markets","select_rule_filters",{"rule_filters":null}]` | | Switch to only `market_created` events after joining | `["0","4","markets","select_message_types",{"message_types":["market_created"]}]` | | Reset to receiving both event types after joining | `["0","5","markets","select_message_types",{"message_types":null}]` | #### Delta updates to markets `market_updated` carries only the fields that changed, plus the mandatory `market_id`, `timestamp` and `unix_timestamp`. A **status change is pushed immediately** when it happens. Every other field change is coalesced and pushed at most once per broadcast interval (2 seconds by default), but only while the market is `pre_open` or `open`; outside that window there's nothing left to coalesce on a timer, so the next change rides along with the next status-change push instead. ##### Status change ``` [null,null,"markets","market_updated",{"017511eb-930b-492a-8933-2284067e3039": {"market_id":"017511eb-930b-492a-8933-2284067e3039","status":"pre_open","timestamp":"2021-03-24T13:11:42.113364Z","unix_timestamp":1616591502113364}}] ``` ##### Game starts ``` [null,null,"markets","market_updated",{"256910ed-213c-44bb-8b9b-66d48689e42b": {"event_brief":"PHX 0 - 0 ORL : Q1 12:00","event_status":"in_progress","market_id":"256910ed-213c-44bb-8b9b-66d48689e42b","timestamp":"2021-03-24T13:24:51.138859Z","unix_timestamp":1616592291138859}}] ``` ##### Game closes ``` [null,null,"markets","market_updated",{"256910ed-213c-44bb-8b9b-66d48689e42b": {"settled_at":"2021-03-24T13:26:57.791844Z","market_id":"256910ed-213c-44bb-8b9b-66d48689e42b","price":100,"result":"won","status":"resulted","timestamp":"2021-03-24T13:26:57.802121Z","unix_timestamp":1616592417802121}}] ``` ##### Probability changed ``` [null,null,"markets","market_updated",{"fcab0df4-2c78-462c-a52e-9e859467bd29": {"last_probability_at":"2021-03-24T13:12:01.979223Z","market_id":"fcab0df4-2c78-462c-a52e-9e859467bd29","probability":0.319857,"timestamp":"2021-03-24T13:12:01.979329Z","unix_timestamp":1616591521979329}}] ``` #### Newly created market push `market_created` fires when a market first becomes tradeable: on the transition to `pre_open`, or (for a market with no pre-open phase) on the transition straight from `scheduled` to `open`. Unlike `market_updated`, it always carries every field listed under [MarketPayload fields](#marketpayload-fields). ```json [null,null,"markets","market_created",{ "017511eb-930b-492a-8933-2284067e3039": { "market_id": "017511eb-930b-492a-8933-2284067e3039", "event_id": "86c9d896-62ab-4c59-8f1c-4cc7850c19c3", "timestamp": "2026-03-24T12:58:30.869725Z", "unix_timestamp": 1774360710869725, "symbol": "STXNBA-26MAR250000CLECHI-GAMECHI", "title": "NBA - Week 14 CLE @ CHI", "short_title": "Bulls to win", "group_title": "Chicago Bulls @ Cleveland Cavaliers", "grouping_id": "86c9d896-62ab-4c59-8f1c-4cc7850c19c3:moneyline:full", "grouping_name": "Moneyline", "description": "Contracts for this market settle into $1 if the Chicago Bulls beat the Cleveland Cavaliers and settle into $0 if they do not.", "question": "Will the Chicago Bulls defeat the Cleveland Cavaliers?", "position": "Chicago Bulls", "event_type": "basketball_game", "sport": "Basketball", "competition": "NBA", "participants": [ {"name": "Chicago Bulls", "role": "home", "short_name": "Bulls", "abbreviation": "CHI"}, {"name": "Cleveland Cavaliers", "role": "away", "short_name": "Cavaliers", "abbreviation": "CLE"} ], "keywords": ["Bulls", "Cavaliers"], "rules": "home_winner", "specifier": null, "stat_detail": null, "max_price": 100, "order_price_rules": [ {"from": 1, "to": 19, "inc": 1}, {"from": 20, "to": 79, "inc": 10}, {"from": 80, "to": 99, "inc": 1} ], "sort": ["event_start", "price", "short_title"], "status": "pre_open", "result": "pending", "settled_at": null, "trading": true, "in_play_delay_sec": 5, "archived": false, "featured": false, "featured_home": false, "filters": [{"category": "NBA", "section": "Week 14", "subcategory": "Game"}], "trading_filters": [], "home_category": "Upcoming", "event_status": "scheduled", "event_start": 1774396800000000, "event_title": "Chicago Bulls @ Cleveland Cavaliers", "event_short_title": "CHI @ CLE", "event_brief": "Starts March 25, 00:00", "detailed_event_brief": "", "last_traded_price": null, "volume_24h": null, "total_volume": 0, "price_change_24h": null, "recent_trades": [], "bids": [], "offers": [], "price": null, "probability": 0.677168, "manual_probability": false, "last_probability_at": "2026-03-24T12:58:30.869637Z" } }] ``` --- # Order Slip Channel Source: https://docs.stxapp.io/websockets/channels/order-slip/ Topic: `order_slip:{user_id}` Costs an order before you place it. Register the order you are considering, and this channel replies with what it would cost you (risk, fee, how much of it would fill immediately and at which prices), then re-pushes those numbers every time the book moves underneath it. Registering an order here places nothing. Nothing is sent to the matching engine, no funds are committed, and the numbers are a projection of the book as it stands. Place the order through `POST /api/v1/orders` when you want it live. :::caution[`betslip:{user_id}` is superseded, and it sends different numbers] This channel replaces `betslip:{user_id}`. That topic still works (same events, same fields, same limits), so nothing breaks if you are already on it. **It does not send the same number formats.** `betslip:` predates the dollar format and still sends money rounded to two decimal places and quantities as JSON numbers: | Field | `order_slip:` | `betslip:` (deprecated) | | --- | --- | --- | | `risk`, `fee`, `risk_with_fee`, level `price`, level `fee` | `"144.9999999"` | `"145.00"` | | `fill_qty`, `unfilled_qty`, level `qty` | `"3.00"` | `3.0` | The two-decimal form is lossy: an order price carries up to seven decimals, so `betslip:` can round a risk of 144.9999999 to `"145.00"`. `order_slip:` keeps the value. Move to `order_slip:{user_id}`. It is a one-line topic change plus parsing money and quantity as decimal strings, the same way you already parse `orders` and `fills`. `betslip:` will be removed. If an environment refuses a join to `order_slip:{user_id}`, it is running a build from before the rename; `betslip:{user_id}` works there. ::: Money and quantities on `order_slip:` follow the standard [wire format](/websockets/channels/wire-format/): money as a decimal string with a minimum of four decimal places and any further precision preserved, quantities as decimal strings with a minimum of two. Parse both with a variable-scale decimal type. :::tip[These are projections, not balances] The authoritative amounts are the ones on `fills:{user_id}` after the order actually executes. Use these to display and to decide, not to reconcile. ::: ## Registering an order ### add_order | Field | Required | Type | Meaning | | --- | --- | --- | --- | | `market_id` | yes | string | The market's UUID. | | `qty` | yes | number | Contracts you are considering. Must be positive. | | `side` | yes | string | `"buy"` or `"sell"`. | | `max_price` | yes | number | That market's `max_price`, **in dollars**: `1` for a market whose REST `max_price` is `"1.0000"`. Sets the price ceiling the risk and fee are computed against. | | `limit_price` | no | number | Your limit, in dollars (`0.55`). Must be positive if given. **Omit it for a market order**; the projection then sweeps the book at any price. | :::caution[Send dollars, not the `markets` channel's cents] `max_price` and `limit_price` here are dollars, the unit of the REST API. The [`markets`](/websockets/channels/markets/) and [`market_updates`](/websockets/channels/market-updates/) channels send `max_price` in cents (`100` for a $1 market). Passing that value here is accepted without error and costs the order as if each contract paid $100: a sell of one contract at `0.42` then projects a risk of `99.5800` instead of `0.5800`. Take `max_price` from `GET /api/v1/markets`, or divide the channel value by 100. ::: Reply carries a `ref` identifying this entry. Keep it: it is echoed on every push, and it is what `remove_order` takes. ```json ["3","4","order_slip:","add_order",{"market_id":"687e9cdb-a391-4118-aa80-1122bb14779f","qty":100,"side":"buy","limit_price":0.55,"max_price":1}] ``` ```json {"status":"ok","response":{"ref":"8f3a1c22-7b40-4e19-9d51-2c7e6a8b0f33","self_match":false}} ``` You may hold **10 registered entries** at once. An eleventh is refused with `max_orders_reached`. Remove one first. Entries live only as long as the channel does. They are not restored after a disconnect: rejoin, then re-add everything you were tracking. :::caution[Send only the events listed here] This channel accepts `add_order`, `remove_order` and `ping`. Any other event drops the channel, and your registered entries go with it; you will have to rejoin and re-add every one. `select_market_ids`, which the other account channels take, is not among them: there is no filter here. ::: ### remove_order ```json ["3","5","order_slip:","remove_order",{"ref":"8f3a1c22-7b40-4e19-9d51-2c7e6a8b0f33"}] ``` ```json {"status":"ok","response":{}} ``` ### Rejected registrations `add_order` and `remove_order` reply `{"status":"error","response":{"reason":"..."}}`. | `reason` | Cause | | --- | --- | | `missing required field` | One of `market_id`, `qty`, `side` or `max_price` is absent. | | `market_id must be a string` | `market_id` was sent as something other than a string. | | `invalid market_id` | `market_id` is a string but not a UUID. | | `side must be 'buy' or 'sell'` | Any other value. | | `qty must be a number`, `max_price must be a number`, `limit_price must be a number` | That field was not a number or a numeric string. `5` and `"5"` are both accepted; `true`, `null`, a list or an object are not. | | `invalid decimal` | The value is a string or float but could not be read as a decimal: `"abc"`, `""`. | | `qty must be positive`, `max_price must be positive`, `limit_price must be positive` | Zero or negative. | | `max_orders_reached` | You already hold 10 entries. | | `limit_exceeded` | The market's order book refused the registration because this connection already holds its per-market maximum. Both limits are 10 by default, so `max_orders_reached` is normally hit first and this is not seen; it becomes reachable when the two are configured apart. Remove an entry on that market. | | `market_unavailable` | That market has no order book right now. **Transient** on `pre_open`, `open`, `closed` and `cancelled`, where a book exists and may be restarting; retry. **Permanent** on `scheduled`, `resulted` and `voided`, where no book is ever started, so retrying cannot succeed. | | `fee_unavailable` | Fees could not be determined for your account on that market. | | `not_found` | `remove_order` was given a `ref` that is not registered. | | `ref required` | `remove_order` was sent without a `ref`. | | `invalid ref` | `ref` is not a UUID. | ## Pushed as the book moves ### order_numbers_batch Every registered entry whose numbers changed, in one frame. ```json [null, null, "order_slip:", "order_numbers_batch", {"updates": [OrderNumbers]}] ``` - `ref` : The entry these numbers are for. - `market_id` : The market the entry is on. - `self_match` : See [Self-match](#self-match) below. - `risk` : What the order would cost you if it filled as projected: the filling portion plus, for a limit order, the remainder left resting. - `fee` : Fee on the same basis. The filling portion is charged the taker rate, any resting remainder the maker rate. - `risk_with_fee` : `risk` + `fee`. What to show as the total. - `fill_qty` : Contracts that would fill immediately. - `unfilled_qty` : Contracts that would not. For a limit order this is what rests on the book; for a market order it is what the book cannot cover. - `est_fill_at_price` : The immediate fill, broken down by price level, best price first. Each entry is `price`, `qty` and `fee`. A resting remainder is **not** a level here; this lists only what fills now. ```json [ null, null, "order_slip:a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "order_numbers_batch", { "updates": [ { "ref": "8f3a1c22-7b40-4e19-9d51-2c7e6a8b0f33", "market_id": "687e9cdb-a391-4118-aa80-1122bb14779f", "self_match": false, "risk": "54.2000", "fee": "0.5000", "risk_with_fee": "54.7000", "fill_qty": "60.00", "unfilled_qty": "40.00", "est_fill_at_price": [ {"price": "0.5300", "qty": "40.00", "fee": "0.2000"}, {"price": "0.5500", "qty": "20.00", "fee": "0.1000"} ] } ] } ] ``` Working that example through, for a buy of 100 at a limit of `0.5500` on a market whose `max_price` is `1`: | | Quantity | Price | Risk | Fee | | --- | --- | --- | --- | --- | | Fills now | 40 | `0.5300` | `21.2000` | `0.2000` taker | | Fills now | 20 | `0.5500` | `11.0000` | `0.1000` taker | | Rests | 40 | `0.5500` | `22.0000` | `0.2000` maker | | **Total** | | | **`54.2000`** | **`0.5000`** | `risk` counts the resting remainder, so it does not equal the sum of `est_fill_at_price`; that list is only what fills now. `risk_with_fee` is `54.2000 + 0.5000`. Risk per contract is the price for a **buy** and `max_price - price` for a **sell**: what you stand to lose either way, not what you pay. A newly registered entry is costed immediately; its first `order_numbers_batch` arrives on registration, not on the next tick. After that, nothing is pushed while the book is still, so silence normally means the projection has not changed. :::caution[Silence does not prove the entry is still registered] If a market's order book becomes unavailable and has not come back after about five seconds, every entry you hold on that market is discarded. **No event is sent.** The numbers you last received simply stop updating, and they will look no different from a quiet market. Usually `remove_order` on that `ref` then returns `not_found`, which is your signal. **It is not guaranteed.** If the book comes back but refuses one of your re-registrations, the entry stays on your slip and still counts against your 10: it receives nothing further, and `remove_order` on it replies `{}` as though it had been live. So neither silence nor a clean `remove_order` proves an entry is still being costed. If stale numbers would be costly to show, re-add entries you have not heard from rather than trusting the last push. ::: ### self_match_batch ```json [null, null, "order_slip:", "self_match_batch", {"updates": [{"ref": "...", "self_match": true}]}] ``` Only entries whose flag actually flipped are included. ## Self-match `self_match` is `true` when you already hold an open order on the opposite side within crossing range of the entry (for a market order, any opposite-side order at all). The exchange rejects a self-crossing order outright rather than filling around it. So when this flag is `true`, the numbers alongside it describe a fill that cannot currently happen, and placing the order would be refused. The entry stays registered anyway, because the condition is yours to clear: cancel the order that is in the way and the flag clears by itself. That is what `self_match_batch` tells you: the book need not have moved for an entry to become placeable, so this is the only signal that it did. Treat the flag as advisory. It is read from a cache that trails the exchange by a moment, and it fails open: if it cannot be determined at registration, the entry is returned unflagged rather than blocked. The engine remains the authority at placement. ## Use Cases | Use case | Message to send | | --- | --- | | Join | `["3","3","order_slip:","phx_join",{}]` | | Cost a limit order | `["3","4","order_slip:","add_order",{"market_id":"","qty":100,"side":"buy","limit_price":0.55,"max_price":1}]` | | Cost a market order | `["3","5","order_slip:","add_order",{"market_id":"","qty":100,"side":"buy","max_price":1}]` | | Stop tracking one entry | `["3","6","order_slip:","remove_order",{"ref":""}]` | | Check the connection is alive | `["3","7","order_slip:","ping",{}]` | ### Joining ```json ["3","3","order_slip:","phx_join",{}] ``` ```json {"status":"ok","response":{}} ``` No snapshot and no filter. The join reply is empty and nothing follows until you register an entry. Joining a topic whose user id is not yours fails with `{"reason":"unauthorized"}`. --- # Orders Channel Source: https://docs.stxapp.io/websockets/channels/orders/ Topic: `orders:{user_id}` Delivers your orders as they are accepted and filled. Money arrives as dollar strings and contract counts as quantity strings; see [Wire format](/websockets/channels/wire-format/). ## Order object ### Identity - `id` : The unique id of the order. - `market_id` : The id of the market. - `client_order_id` : The id you supplied when placing the order, if any. - `fix_order` : `true` if the order arrived over FIX rather than REST. ### Sizing - `quantity` : The number of contracts to buy or sell, as a quantity string. Null on an order sized by `amount` instead. - `filled` : How many of the order's contracts have been filled so far, as a quantity string. - `filled_percentage` : The same, as an integer percentage (0–100), truncated. - `amount` : Order size in dollars, for an order entered by amount rather than by `quantity`, which only an STX app does. **Null on an order sized by `quantity`**; the two are alternatives, not a value and its derivation. - `filled_amount` : The portion of `amount` that has been filled. Only meaningful on an order sized by `amount`; it reads `"0.0000"` otherwise. ### Pricing - `price` : The order's price, as a dollar string. Null when `order_type` is `market`. Stored to seven decimal places, so this field can be wider than the usual four. - `avg_price` : The average price the filled portion traded at, as a dollar string rounded to the cent. Null until something fills. - `total_value` : Total premium across every fill on this order: the sum of `filled × price` over its trades. `"0.0000"` until something fills. - `odds_type` : Legacy. `decimal` or `american`, when an STX app entered the order in odds rather than price. Null for orders placed through the API. - `odds_value` : Legacy. The odds that app order was entered at, as a string. Null for orders placed through the API. ### Lifecycle - `action` : Whether the order is a `buy` or a `sell`. - `order_type` : `limit` or `market`. A limit order sets a ceiling for a buy or a floor for a sell. If it cannot fill completely it rests on the book for the remainder. A market order has no price limit and either fills completely or has its remainder cancelled. - `status` : One of `created`, `requested`, `accepted`, `delayed`, `open`, `filled`, `rejected`, `cancelled` or `partially_cancelled`; see [Market and order status](/concepts/market-status/). - `time` : When the order was created, ISO 8601. - `inserted_at` : The same instant, as an integer in Unix microseconds. - `accepted_at` : When the matching engine accepted the order, as Unix microseconds. Null while the order is still pending. - `expires_at` : When the contracts this order trades expire, as Unix microseconds: the market's expiration, copied onto the order when it is placed. Null when the market has no event. Not the same as `expiration_time`, which is when a `good_till_time` order stops resting on the book. - `cancellation_reason` : Why the order was cancelled: `by_player`, `market_closed`, `market_cancelled`, `expired`, `by_operator`, `auto_matching`, `on_disconnect`, `negative_balance`, `insufficient_assets`, `member_position_limit`, `admin_trade_cancelled` or `admin_trade_price_change`. Null unless `status` is `cancelled` or `partially_cancelled`. - `rejection_reason` : Why the order was rejected, e.g. `insufficient_assets`, `account_limits_reached`, `market_liability_limit`, `member_position_limit`, `match_with_self`, `invalid_geo_location`, `wrong_market_state` or `validation_error`. Null unless `status` is `rejected`. ### Risk controls See [Risk controls](/risk-controls/) for what these do. - `expiration` : `good_till_start`, `good_till_time`, or null for an order that rests until cancelled. - `expiration_time` : When a `good_till_time` order expires, as Unix microseconds. Null for every other `expiration`. - `delayed_until` : When an order held by the in-play delay reaches the book, as Unix microseconds. Null when no delay applies. - `placed_pre_start` : Whether the order was placed before the event started. ### Provenance - `ip_address` : The IP address the order was placed from. Null if none was recorded. - `device_id` : The device id the order was placed from. Null if none was recorded. - `ux_action` : The intent the order was placed with in the app: `buy_yes`, `buy_no`, `sell_yes` or `sell_no`. Null for an order placed through the API. ## Use Cases | Use case | Message to send | | --- | --- | | Join and receive your open orders | `["3","3","orders:","phx_join",{}]` | | Join filtered to two markets (see [filtering](/websockets/channels/wire-format/#filtering)) | `["3","3","orders:","phx_join",{"market_ids":["",""]}]` | | Join with `cancel_on_disconnect` enabled (see [Risk controls](/risk-controls/)) | `["3","3","orders:","phx_join",{"cancel_on_disconnect":true,"ping_timeout":5000}]` | | Change the market filter without rejoining | `["3","4","orders:","select_market_ids",{"market_ids":[""]}]` | | Keep a `cancel_on_disconnect` session alive | `["3","5","orders:","ping",{}]` | ### Joining ```json ["3","3","orders:","phx_join",{}] ``` The reply echoes the filter that was applied, and the `cancel_on_disconnect` settings when you asked for them: ```json {"status":"ok","response":{"selected_market_ids":null}} ``` ```json {"status":"ok","response":{"selected_market_ids":null,"cancel_on_disconnect":true,"ping_timeout":5000}} ``` `ping_timeout` is clamped to 5000–20000 ms, so read the value back from the reply rather than assuming the one you sent was honored. ### Initial response after joining ```json [null, null, "orders:a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "all_orders", { "orders": [Order]}] ``` `orders` is a list of `Order` objects, described above, empty if you have none open, and also empty when a `market_ids` filter matches nothing. ```json [ null, null, "orders:a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "all_orders", { "orders": [ { "id": "7dc6671e-2852-44f4-803d-4405d8a85407", "market_id": "002b030c-5ac2-41f4-92de-175e666b0a70", "client_order_id": null, "fix_order": false, "quantity": "20.00", "filled": "0.00", "filled_percentage": 0, "price": "0.1000", "avg_price": null, "amount": null, "filled_amount": "0.0000", "total_value": "0.0000", "odds_type": null, "odds_value": null, "action": "sell", "order_type": "limit", "status": "open", "time": "2026-11-06T21:34:30.376858Z", "inserted_at": 1794173670376858, "accepted_at": 1794173670381204, "expires_at": 1825736399999999, "cancellation_reason": null, "rejection_reason": null, "expiration": null, "expiration_time": null, "delayed_until": null, "placed_pre_start": true, "ip_address": "203.0.113.42", "device_id": "web-chrome-114", "ux_action": null } ] } ] ``` ### Pushed when an order is accepted or filled ```json [null, null, "orders:a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "new_open_order", Order] ``` One frame per order, carrying the whole object rather than a diff. ```json [ null, null, "orders:a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "new_open_order", { "id": "7dc6671e-2852-44f4-803d-4405d8a85407", "market_id": "002b030c-5ac2-41f4-92de-175e666b0a70", "client_order_id": null, "fix_order": false, "quantity": "20.00", "filled": "10.00", "filled_percentage": 50, "price": "0.1000", "avg_price": "0.1000", "amount": null, "filled_amount": "0.0000", "total_value": "1.0000", "odds_type": null, "odds_value": null, "action": "sell", "order_type": "limit", "status": "open", "time": "2026-11-06T21:34:30.376858Z", "inserted_at": 1794173670376858, "accepted_at": 1794173670381204, "expires_at": 1825736399999999, "cancellation_reason": null, "rejection_reason": null, "expiration": null, "expiration_time": null, "delayed_until": null, "placed_pre_start": true, "ip_address": "203.0.113.42", "device_id": "web-chrome-114", "ux_action": null } ] ``` :::tip[The fill itself arrives on another channel] `new_open_order` tells you the order's state changed. The execution that caused it (price, fee, premium) arrives separately on [`fills`](/websockets/channels/fills/), and the two channels are not ordered relative to each other. Key off `order_id` and reconcile rather than assuming arrival order. ::: --- # Positions Channel Source: https://docs.stxapp.io/websockets/channels/positions/ Topic: `positions:{user_id}` Delivers your positions as they change. Money arrives as dollar strings and contract counts as quantity strings; see [Wire format](/websockets/channels/wire-format/). :::tip[Need a one-off read? Use REST] [`GET /api/v1/positions`](/api/rest/account/list-open-positions/) returns the same list this channel sends as `all_positions` on join, with the same fields in the same order, and takes the same optional `market_ids` filter as a comma-separated query parameter. It is a snapshot of the moment you asked. This channel is the live feed: take the REST snapshot to seed or reconcile your state, then apply `updated_positions` deltas from here. ::: ## Position object ### Identity - `id` : The unique id of the position record. - `account_id` : The id of the account the position belongs to. - `market_id` : The id of the market the position is on. - `event_id` : The id of the event the market is attached to. ### Exposure - `position` : The account's net position in the market: positive if long (bought), negative if short (sold). A quantity string. - `premium` : The total premium paid or received for the **open** portion of the position. - `average_open_premium` : The average premium per contract for the open portion. - `buy_order_liability` : The liability from the account's open buy orders on this market. - `sell_order_liability` : The liability from the account's open sell orders on this market. - `position_premium_liability` : The liability from the position's premium that counts against available balance. Routinely negative. ### Risk and potential - `max_risk` : The account-level risk on the position, netting in already settled profit and loss. - `open_risk` : The risk on the position's currently open (unsettled) contracts; this is what's shown as "Risk" in the app. - `max_potential_profit` : The total possible profit for the position if everything settles favorably. - `open_potential_profit` : The possible profit on the position's open contracts. - `max_potential_fee` : The total potential fee across the position's settlements. - `open_potential_fee` : The potential fee on the position's open contracts when they settle. ### Realized - `total_settlement_pnl` : The profit or loss the account has realized from settlements so far. - `gross_pnl` : `total_settlement_pnl` plus any pending-close profit or loss that hasn't settled yet. - `total_fee` : The total fees paid across the position's settlements. - `contracts_settled` : The number of contracts settled in the position so far. A quantity string. ## Use Cases | Use case | Message to send | | --- | --- | | Join and receive your open positions | `["3","3","positions:","phx_join",{}]` | | Join filtered to one market (see [filtering](/websockets/channels/wire-format/#filtering)) | `["3","3","positions:","phx_join",{"market_ids":[""]}]` | | Change the market filter without rejoining | `["3","4","positions:","select_market_ids",{"market_ids":null}]` | | Check the connection is alive | `["3","5","positions:","ping",{}]` | ### Joining ```json ["3","3","positions:","phx_join",{}] ``` ```json {"status":"ok","response":{"selected_market_ids":null}} ``` ### Initial response after joining ```json [null, null, "positions:a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "all_positions", { "positions": [Position]}] ``` `positions` is a list of `Position` objects, described above, ordered by `position` descending, empty if you have none open, and also empty when a `market_ids` filter matches nothing. ```json [ null, null, "positions:a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "all_positions", { "positions": [ { "id": "0e1f2a3b-4c5d-46a9-9d3a-7dc6671e2852", "account_id": "c1c3614c-32ae-4bea-bae4-fc3f52047790", "market_id": "687e9cdb-a391-4118-aa80-1122bb14779f", "event_id": "86c9d896-62ab-4c59-8f1c-4cc7850c19c3", "position": "-70.00", "premium": "49.0000", "average_open_premium": "0.7000", "buy_order_liability": "0.0000", "sell_order_liability": "0.0000", "position_premium_liability": "-49.0000", "max_risk": "21.0000", "open_risk": "21.0000", "max_potential_profit": "49.0000", "open_potential_profit": "49.0000", "max_potential_fee": "2.4500", "open_potential_fee": "2.4500", "total_settlement_pnl": "0.0000", "gross_pnl": "0.0000", "total_fee": "0.0000", "contracts_settled": "0.00" } ] } ] ``` ### Pushed when the account's positions change ```json [null, null, "positions:a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "updated_positions", { "positions": [Position]}] ``` This event is a **delta**: it carries only the positions that changed, not your whole book. When a `market_ids` filter is active and none of the changed positions match it, no frame is sent at all rather than one carrying an empty list. ```json [ null, null, "positions:a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "updated_positions", { "positions": [ { "id": "0e1f2a3b-4c5d-46a9-9d3a-7dc6671e2852", "account_id": "c1c3614c-32ae-4bea-bae4-fc3f52047790", "market_id": "687e9cdb-a391-4118-aa80-1122bb14779f", "event_id": "86c9d896-62ab-4c59-8f1c-4cc7850c19c3", "position": "-50.00", "premium": "35.0000", "average_open_premium": "0.7000", "buy_order_liability": "0.0000", "sell_order_liability": "0.0000", "position_premium_liability": "-35.0000", "max_risk": "14.0000", "open_risk": "15.0000", "max_potential_profit": "36.0000", "open_potential_profit": "35.0000", "max_potential_fee": "1.8000", "open_potential_fee": "1.7500", "total_settlement_pnl": "1.0000", "gross_pnl": "1.0000", "total_fee": "0.0500", "contracts_settled": "20.00" } ] } ] ``` :::caution[Positions are not marked to market for you] This channel fires on your own activity and on settlements, not on price moves. Valuing an open position against the current book is your job, from [`orderbook`](/websockets/channels/market-data/#orderbook). ::: --- # Settlements Channel Source: https://docs.stxapp.io/websockets/channels/settlements/ Topic: `settlements:{user_id}` Delivers a settlement recorded against your account whenever a trade closes or expires. Money arrives as dollar strings and contract counts as quantity strings; see [Wire format](/websockets/channels/wire-format/). :::tip[No snapshot on join] This is a change feed. Joining tells you nothing about settlements that already happened. Pull those from `GET /api/v1/portfolio/settlements` and keep them current from here. ::: ## Settlement object ### Identity - `id` : The unique id of the settlement. - `account_id` : The id of the account the settlement is for. - `market_id` : The id of the market the settlement is on. - `opening_trade_id` : The id of the trade that opened the position being settled. - `closing_trade_id` : The id of the trade that closed the position. Null when the position was closed by the market settling rather than by a closing trade. - `type` : What caused the settlement and the position beforehand: `closed_short`, `closed_long`, `expired_short` or `expired_long`. - `inserted_at` : When the settlement was recorded, as an integer of Unix microseconds. ### Amounts - `opening_price` : The price the opening trade traded at. - `closing_price` : The price the closing trade traded at, or the market's settlement price for an `expired_*` settlement. - `quantity` : The number of contracts settled, as a quantity string. Can be fractional. - `fee` : The fee charged for the settlement. - `gross_pnl` : The profit or loss before fees. - `realized_pnl` : The profit or loss after fees. - `settled_premium` : The amount of premium settled. - `settled_risk` : The amount of risk settled. ### Pre-start flags - `opening_placed_pre_start` : Whether the opening trade's order was placed before the event started. - `closing_placed_pre_start` : Whether the closing trade's order was placed before the event started. Null when there is no closing trade. - `opening_traded_pre_start` : Whether the opening trade executed before the event started. - `closing_traded_pre_start` : Whether the closing trade executed before the event started. Null when there is no closing trade. - `pre_start` : Whether the settlement itself was created before the event started. ## Use Cases | Use case | Message to send | | --- | --- | | Join and start receiving settlements | `["3","3","settlements:","phx_join",{}]` | | Join filtered to one market (see [filtering](/websockets/channels/wire-format/#filtering)) | `["3","3","settlements:","phx_join",{"market_ids":[""]}]` | | Change the market filter without rejoining | `["3","4","settlements:","select_market_ids",{"market_ids":null}]` | | Check the connection is alive | `["3","5","settlements:","ping",{}]` | ### Joining ```json ["3","3","settlements:","phx_join",{}] ``` ```json {"status":"ok","response":{"selected_market_ids":null}} ``` Nothing follows until a settlement is recorded. ### Pushed when a settlement is recorded ```json [null, null, "settlements:a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "new_settlements", { "settlements": [Settlement]}] ``` Settlements are batched into one frame. This event is a **delta**: when a `market_ids` filter is active and none of the settlements match it, no frame is sent at all rather than one carrying an empty list. ```json [ null, null, "settlements:a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "new_settlements", { "settlements": [ { "id": "801d773e-fecc-418a-b377-155605595178", "account_id": "c1c3614c-32ae-4bea-bae4-fc3f52047790", "market_id": "687e9cdb-a391-4118-aa80-1122bb14779f", "opening_trade_id": "17aa760d-50c0-4c1a-a5c6-fe4af4adb4bb", "closing_trade_id": "7f959093-6a30-4b48-9a59-d1e09f004340", "type": "closed_short", "inserted_at": 1793559748125460, "opening_price": "0.2000", "closing_price": "0.1000", "quantity": "100.00", "fee": "0.5000", "gross_pnl": "10.0000", "realized_pnl": "9.5000", "settled_premium": "20.0000", "settled_risk": "80.0000", "opening_placed_pre_start": false, "closing_placed_pre_start": false, "opening_traded_pre_start": false, "closing_traded_pre_start": false, "pre_start": false } ] } ] ``` --- # User Info Channel Source: https://docs.stxapp.io/websockets/channels/user-info/ Topic: `user_info:{user_id}` Delivers your profile information, and pushes an update whenever it changes. `user_id` is the UUID that `GET /api/v1/me` returns as `user_id`, the same id every account channel topic takes. It is not `userUid`. ## UserInfo object - `userId` : Your user id: the UUID in this channel's topic, and `user_id` from `GET /api/v1/me`. - `userUid` : A second, external identifier for the user. It can differ from `userId`; never use it in a topic. - `userStatus` : The user's account status, one of `archived`, `pending_approval`, `active`, `suspended`, `banned`, `self_excluded`, `closed`, `rejected`, `dormant`, `cool_off` or `overdrawn`. - `firstName` : The user's first name. - `lastName` : The user's last name. - `middleName` : The user's middle name, if any. - `preferredName` : The user's preferred name, if any. - `countryCode` : The country code for `phoneNumber`, if any. - `phoneNumber` : The user's phone number, if any. - `businessCountryCode` : The country code for `businessPhone`, if any. - `businessPhone` : The user's business phone number, if any. - `dateOfBirth` : The user's date of birth, if on file. - `address1` : Address line 1 of the user's address, if on file. - `address2` : Address line 2 of the user's address, if on file. - `city` : The city of the user's address, if on file. - `state` : The state of the user's address, if on file. - `zipCode` : The zip code of the user's address, if on file. - `jobTitle` : The user's job title, if any. - `industry` : The industry the user works in, if any. - `password_changed` : `true` immediately after the user changes their password. - `logout` : `true` when this update should force the client to log the user out (for example, after a password change). - `test_account` : Whether the account is marked as a test account. - `country` : The country of the user's address, if any. - `citizenshipCountry` : The user's country of citizenship, if any. - `employerName` : The user's employer's name, if any. - `employerAddress` : The user's employer's address, if any. - `optInMarketing` : Whether the user has opted in to marketing communications. - `allowMultipleLogins` : Whether the user may be logged in from more than one place at once. - `whitelistIpAddresses` : The user's whitelisted IP addresses, as a comma-separated string. Null if none are set. - `referralCode` : The user's own referral code. ## Use Cases | Use case | Message to send | | --- | --- | | Join and receive your profile | `["3","3","user_info:","phx_join",{}]` | | Check the connection is alive | `["3","4","user_info:","ping",{}]` | ### Joining ```json ["3","3","user_info:","phx_join",{}] ``` ```json [ "1", "1", "user_info:7fb55544-3a7e-4f6b-bd66-fc89bd3a1087", "phx_reply", { "response": {}, "status": "ok" } ] ``` Immediately after joining, expect a `user_updated` event carrying the current snapshot; the join reply itself carries nothing. ### Pushed when the user's information changes ```json [null, null, "user_info:7fb55544-3a7e-4f6b-bd66-fc89bd3a1087", "user_updated", UserInfo] ``` ```json [ null, null, "user_info:7fb55544-3a7e-4f6b-bd66-fc89bd3a1087", "user_updated", { "userId": "7fb55544-3a7e-4f6b-bd66-fc89bd3a1087", "userUid": "v5yDoWte75hCuhp3ejRMPTWz8Yz1", "userStatus": "active", "firstName": "Jonny", "lastName": "Lu", "middleName": null, "preferredName": null, "countryCode": "+1", "phoneNumber": null, "businessCountryCode": null, "businessPhone": null, "dateOfBirth": "1988-03-05", "address1": "123 Main St", "address2": null, "city": "New York", "state": "NY", "zipCode": "10121", "jobTitle": null, "industry": null, "password_changed": false, "logout": false, "test_account": false, "country": "USA", "citizenshipCountry": null, "employerName": null, "employerAddress": null, "optInMarketing": true, "allowMultipleLogins": false, "whitelistIpAddresses": null, "referralCode": "JLU4754" } ] ``` --- # Wire format Source: https://docs.stxapp.io/websockets/channels/wire-format/ The [account channels](/websockets/#account-channels) and the three [market-data feeds](/websockets/channels/market-data/) write money and contract counts as **strings**, in the same format as the [REST API](/api/rest/). One parser and one set of fixtures cover both surfaces. The `markets` and `market_updates` metadata feeds are the exception: their prices are JSON numbers in cents, not dollar strings. Their own pages give the units field by field. ## The format **Money is a string of dollars**, with at least four decimal places: ```json { "price": "0.6700", "amount": "39.0000", "max_price": "1.0000" } ``` **Quantities and contract counts are strings**, with at least two decimal places: ```json { "quantity": "100.00", "filled": "50.00" } ``` A `null` stays `null`; it never becomes `"0.0000"`. ### Decimal places are a minimum, not a fixed width Almost every money field is exactly four decimal places. Order `price` is the exception on the channels: it is stored to seven, so `"0.0125432"` is a valid price. **Parse money with a decimal type that accepts a variable scale.** A parser hard-coded to four places breaks on that field and nowhere else. Over REST, `GET /api/v1/fills` carries one more such field, `unrounded_trade_fee`, at up to nine places. ### What is not converted Only amounts of money and numbers of contracts become strings. These stay JSON numbers: - percentages: `filled_percentage`, `price_change24h` - counts of objects: `open_order_count`, `trade_count`, `settlements_count` - fee factors: `base_fee_percent`, `taker_factor`, `maker_factor` - loyalty `points` Timestamps come in two shapes and neither is a money string: `time`, `settled_at` and `timestamp` are ISO 8601, while `inserted_at`, `accepted_at`, `delayed_until`, `expiration_time`, `expires_at` and `timestamp_us` are integers of Unix **microseconds**. :::note[Prices read as probabilities] A market's prices run from 0 to its `max_price` of `"1.0000"`, so `"0.6700"` reads directly as a 67% probability. ::: ## `total_fee` is the all-in fee On both [`fills`](/websockets/channels/fills/) and `GET /api/v1/fills`, `total_fee` is the trade fee plus the settlement fee: what the trade has actually cost you. `trade_fee` is the on-trade component alone, and is also sent, so `total_fee - trade_fee` is the settlement part. The two surfaces agree, so a REST snapshot and a `fills` delta can be mixed freely. ## Filtering by market [`orders`](/websockets/channels/orders/), [`fills`](/websockets/channels/fills/), [`positions`](/websockets/channels/positions/), [`settlements`](/websockets/channels/settlements/) and [`account`](/websockets/channels/account/) take an optional `market_ids` filter in the join payload, so a client watching a few markets is not sent every order, fill, position and settlement on the account. ``` ["1","1","orders:","phx_join",{"market_ids":["",""]}] ``` The reply echoes what was actually applied: ```json {"status":"ok","response":{"selected_market_ids":["",""]}} ``` It applies to both the snapshot you get on join and every push afterwards. Omit `market_ids`, or send `null` or `[]`, and nothing is filtered; `selected_market_ids` comes back `null`. Ids you send that are not valid UUIDs are dropped rather than rejected, and **if none of them are valid you get no filter at all**, not an empty one. That is why the applied set is echoed: compare it against what you sent to catch a typo, rather than silently receiving everything. Ids are case-insensitive. ### Snapshots are filtered differently from deltas A **snapshot** the filter empties is still sent: `{"orders": []}` on join tells you there is nothing in those markets, which is worth knowing. A **delta** the filter empties is not sent at all, rather than arriving as an empty list, because `{"positions": []}` would read as "your positions are gone". `updated_positions` and `new_settlements` are the deltas. ### Changing it without rejoining ``` ["1","2","orders:","select_market_ids",{"market_ids":[""]}] ``` The reply carries the new `selected_market_ids`. Send `null` to clear the filter. [`balances`](/websockets/channels/balances/) does not take this parameter; it is scoped to one account, not to markets. It takes an optional `account_id` instead. The market-data feeds narrow differently again: `orderbook` **requires** `market_ids` and changes it with `select_market_ids`, while `ticker` and `trades` filter on other fields and use `select_filters`. See [Order book, ticker and trades](/websockets/channels/market-data/). --- # Market order book > The per-market channel that delivers one market's aggregated order book together with its full market record. Source: https://docs.stxapp.io/websockets/order-book/ Streams the live order book for one market: two sides, bid and offer, each a ladder of price levels carrying size, liquidity and cumulative depth, so you can read how far the market goes without summing levels yourself. It refreshes roughly every 200 ms, not on every individual order; see [Server pushes](#server-pushes) below for the exact shape of a push. The channels under [Channels](/websockets/) carry *your* orders, fills and positions. This one carries **the market**: the full aggregated book and the market record, one market per topic. :::note[Watching several markets] If you follow more than one market, use the [`orderbook`](/websockets/channels/market-data/#orderbook) topic instead. One join covers every market you name, and it uses the same dollar-string format as the REST API and your account channels. ::: :::tip[See one without writing any code] [Live markets](/explore/live-markets/) streams a real book from this channel in your browser and shows the raw frames beside it. Useful for checking your own rendering against, or for seeing current spreads before you build. ::: One channel per market. Join `market:` for each market you are watching or trading, and join [`market_updates`](/websockets/channels/market-updates/) alongside it if you also want lightweight change notifications across many markets at once. ## Joining The topic accepts either the market UUID or its symbol: ```json ["1","1","market:2bc3d8d8-1f4e-4b8a-9c1d-3e5f7a9b1c2d","phx_join",{}] ["1","1","market:NFLSF2025","phx_join",{}] ``` Unlike the `market_updates` channel, **a successful join replies with the current state immediately**: you do not have to ask for a snapshot first: ```json ["1","1","market:2bc3d8d8-…","phx_reply", {"status":"ok","response":{ "market_id":"2bc3d8d8-…", "status":"open", …, "ob":{"b":[…],"o":[…]} }}] ``` The reply is the full market record, the same shape the [`markets`](/websockets/channels/markets/) channel sends, with `ob` added. The two parts use different units: the market record's prices are in cents, while `ob` is in dollars. Read each field's units from its own page. A join is refused with a reason you can act on: | `reason` | Meaning | | --- | --- | | `market_not_found` | No market with that id or symbol | | `market_not_joinable` | The market exists but is not in a joinable status | Joinable statuses are `pre_open`, `open`, `closed` and `cancelled`. `scheduled` markets are hidden and cannot be joined. `resulted` and `voided` are terminal: the socket receives one final `market_update` and is then **dropped by the server**, so treat an unexpected close on this channel as "the market ended", not as a network fault. :::tip `pre_open` is joinable, so you can be on the book before the market opens. See [Market and order status](/concepts/market-status/) for what each status means and how to filter on it. ::: ## Server pushes ### `order_book_update` The aggregated book, pushed **roughly every 200 ms**, not on every individual change. Between pushes many updates are coalesced, so treat each push as the current state of the levels it contains rather than as a single event. ```json ["1","1","market:2bc3d8d8-…","order_book_update", {"ob":{"b":[{"p":0.42,"q":150.0,"l":63.0,"tc":420.0,"tl":176.4}], "o":[{"p":0.43,"q":90.0,"l":38.7,"tc":310.0,"tl":133.3}]}}] ``` `b` is the bid side, `o` the offer side. Each level: | Field | Meaning | | --- | --- | | `p` | Price, in dollars | | `q` | Contracts available at this price | | `l` | Liquidity at this price, in dollars | | `tc` | Total contracts at this price and better | | `tl` | Total liquidity at this price and better, in dollars | These are JSON numbers, not the dollar strings the REST API and the [`orderbook`](/websockets/channels/market-data/#orderbook) topic send. `tc` and `tl` are cumulative, so you can read depth-to-price straight off a level without summing the ones above it. ### `market_update` The full `Market` map, pushed whenever the market changes, status transitions, trading halts, settlement. Same shape the [`markets`](/websockets/channels/markets/) channel delivers. ## Re-syncing after a reconnect Send `request_snapshot` and the server immediately re-pushes both the current `order_book_update` and `market_update`: ```json ["1","2","market:2bc3d8d8-…","request_snapshot",{}] ``` Do this after any reconnect. There is currently **no sequence number** on `order_book_update`, so a client cannot detect a gap while it believes itself connected. If you suspect you have missed data, call `request_snapshot` and replace your book with what comes back rather than trying to reconcile. ## Authentication This channel carries no account-specific data, so it accepts an unsigned socket. The handshake still needs a `User-Agent` header, or it is refused with `403`. Your account channels need a signed socket: if you also want your own orders and fills, sign the handshake as described in [WebSocket channels](/websockets/) and join both on that one connection.