Integrate with STX
Every command on this page was run against https://demo.stxapp.io and its output
checked before publishing. Where something does not yet work, it says so.
0. The environment
Section titled “0. The environment”There is one integration environment you can reach today:
| Base URL | https://demo.stxapp.io |
| Money | None. Play balances. |
| Shared | Yes — other developers’ orders are in the books, which makes it a realistic test target |
Other hostnames you may have seen in older docs or SDK READMEs — in-api-staging.stxapp.io,
api-staging.on.sportsxapp.com, api.on.stxapp.ca — either do not resolve publicly or do
not serve /api/v1. Use demo.stxapp.io.
1. Create an API key
Section titled “1. Create an API key”- Log in to the exchange and open Account → API Keys
- Create API Key, give it a label, Generate
- Copy the Key ID and the Private Key PEM. The PEM is shown once and never stored by the server.
Key ID: bcea87eb0e67e126490ea541e3534f66PEM: -----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIH... -----END PRIVATE KEY-----One key authenticates all three surfaces: REST, GraphQL and the private WebSocket channels. You do not need a session token or a login call.
2. Sign a request
Section titled “2. Sign a request”The message is three values joined with no separator:
timestamp_ms + HTTP_METHOD_UPPERCASE + pathimport base64, time, requestsfrom cryptography.hazmat.primitives.serialization import load_pem_private_key
HOST = "https://demo.stxapp.io"KEY_ID = "your-key-id"PRIVATE = load_pem_private_key(open("stx.pem", "rb").read(), password=None)
def headers(method, path): """path must include the query string, if there is one.""" ts = str(int(time.time() * 1000)) sig = PRIVATE.sign((ts + method.upper() + path).encode()) return { "STX-ACCESS-KEY": KEY_ID, "STX-ACCESS-TIMESTAMP": ts, "STX-ACCESS-SIGNATURE": base64.b64encode(sig).decode(), }
def get(path): return requests.get(HOST + path, headers=headers("GET", path)).json()import crypto from 'node:crypto'import { readFileSync } from 'node:fs'
const HOST = 'https://demo.stxapp.io'const KEY_ID = 'your-key-id'const PRIVATE = crypto.createPrivateKey(readFileSync('stx.pem'))
export function headers(method, path) { const ts = Date.now().toString() const sig = crypto.sign(null, Buffer.from(ts + method.toUpperCase() + path), PRIVATE) return { 'STX-ACCESS-KEY': KEY_ID, 'STX-ACCESS-TIMESTAMP': ts, 'STX-ACCESS-SIGNATURE': sig.toString('base64'), }}Four rules. Each one is a real 401 we have seen:
| Rule | Gets you |
|---|---|
| The path includes the query string | Signing /api/v1/markets then requesting ?status=open fails |
| Timestamp within 30 seconds of server time | Generate per request; never reuse a signature |
| Method uppercase | GET, not get |
| The body is not signed | Only timestamp, method and path |
Full scheme and a test vector: Request signing.
3. Get your user_id
Section titled “3. Get your user_id”Do this first. You need user_id to subscribe to any private WebSocket channel, and
/me is the only place it comes from.
me = get("/api/v1/me")["me"]print(me["user_id"], me["account_id"], me["scope"]){ "me": { "key_id": "bcea87eb0e67e126490ea541e3534f66", "user_id": "a501cce1-aadc-4db5-b8bc-bbdf1c15e86b", "account_id": "4021a4f3-de26-4743-8db7-c8be7c9d8eaf", "scope": "read_write", "method": "api_key" }}4. Read market data
Section titled “4. Read market data”Market data is public — no key required, on REST’s GraphQL sibling and on the socket:
curl -s -X POST https://demo.stxapp.io/api/graphql \ -H 'Content-Type: application/json' \ -d '{"query":"{ marketInfosWithCount(input:{status:[OPEN]}) { count marketInfos { marketId title trading } } }"}'5. Place and cancel an order
Section titled “5. Place and cancel an order”import requestspath = "/api/v1/orders"body = { "market_id": "2bc3d8d8-fca3-432d-8540-e01e11835fae", "order_type": "limit", "action": "buy", "price": 42, # whole cents, 1-99 "quantity": 5,}r = requests.post(HOST + path, json=body, headers=headers("POST", path)).json()order_id = r["order"]["id"]
path = f"/api/v1/orders/{order_id}"requests.delete(HOST + path, headers=headers("DELETE", path)).json()Create returns 200 (not 201) with {"order": {...}}. Cancel returns
{"status": "cancelled", "order_id": "..."}.
6. Stream public market data
Section titled “6. Stream public market data”No credentials. Connect, join, then send a watch with market ids — without it the
channel is silent.
import json, websocket # pip install websocket-client
ws = websocket.create_connection("wss://demo.stxapp.io/socket/websocket?vsn=2.0.0")ws.send(json.dumps(["1", "1", "market_updates", "phx_join", {}]))ws.send(json.dumps(["2", "2", "market_updates", "watch", ["<market-id>", "<market-id>"]]))while True: print(ws.recv())Frames are JSON arrays, not objects:
[join_ref, ref, topic, event, payload]Keep the connection alive with ["3","3","phoenix","heartbeat",{}] every ~30 seconds.
7. Stream your own orders, trades and positions
Section titled “7. Stream your own orders, trades and positions”Private channels need the key and your user_id from step 3.
import base64, json, time, websocketfrom cryptography.hazmat.primitives.serialization import load_pem_private_key
PRIVATE = load_pem_private_key(open("stx.pem", "rb").read(), password=None)KEY_ID, USER_ID = "your-key-id", "your-user-id-from-me"
ts = str(int(time.time() * 1000))sig = base64.b64encode(PRIVATE.sign((ts + "GET" + "/socket/websocket").encode())).decode()
ws = websocket.create_connection( "wss://demo.stxapp.io/socket/websocket?vsn=2.0.0", header=[ f"X-STX-ACCESS-KEY: {KEY_ID}", f"X-STX-ACCESS-TIMESTAMP: {ts}", f"X-STX-ACCESS-SIGNATURE: {sig}", ],)
for i, topic in enumerate([ f"active_orders:{USER_ID}", f"active_trades:{USER_ID}", f"active_positions:{USER_ID}", f"portfolio:{USER_ID}",]): ws.send(json.dumps([str(i), str(i), topic, "phx_join", {}]))
while True: print(ws.recv())Each private channel replies {"status":"ok"} and then pushes a snapshot:
| Channel | Snapshot event |
|---|---|
active_orders:{user_id} |
all_orders |
active_trades:{user_id} |
all_trades |
active_positions:{user_id} |
all_positions |
portfolio:{user_id} |
summary |
user_info:{user_id} |
user_updated |
That differs from market_updates, which sends nothing until you watch.
8. Protect resting orders from a dropped connection
Section titled “8. Protect resting orders from a dropped connection”Join active_orders with two extra params and the exchange cancels your flagged orders
if your client stops heartbeating:
ws.send(json.dumps(["9", "9", f"active_orders:{USER_ID}", "phx_join", {"cancel_on_disconnect": True, "ping_timeout": 5000}]))Reply: {"ping_timeout": 5000, "cancel_on_disconnect": true}. Then send
["n","n",f"active_orders:{USER_ID}","ping",{}] inside every ping_timeout window.
ping_timeout is clamped to 5000–20000 ms and must be an integer. Only orders that
set cancel_on_disconnect: true themselves are cancelled, and there is a 20-second grace
period after the channel drops.
Try it interactively — kill the socket and watch an order leave the book.
- Postman collection — all 16 endpoints with the signing script wired in
- REST API reference — every endpoint, request and response
- Try it in the console
- WebSocket channels — all eight channels
- Runnable examples on GitHub — Python quickstart and a live watcher
Known issues
Section titled “Known issues”Verified against demo.stxapp.io. Being fixed; listed so you do not lose an afternoon.
| Issue | Workaround |
|---|---|
GET /api/v1/markets?status=open returns suspended markets; trading=true is ignored |
Find open markets via the GraphQL query in step 4 |
?status=OPEN returns 400 |
Lowercase the value |
The market_updates channel doc elsewhere shows the topic as market_update |
The topic is market_updates, plural |

