Skip to content

Market data

Market data access lets your product read STX markets, prices and order-book depth in real time, with credentials of your own. It is for odds comparison sites, media, research and trading tools. It reads public market data only: it never touches a member’s account, so there is no consent screen and no member data.

Trading on your own account instead? You don’t need data credentials: a signed API key reads the same data and trades too.

Data REST WebSocket
Market catalogue: every listed market, its status, rules and details GET /api/v1/markets markets, market_updates
Prices and depth: best bid and offer, the aggregated book, recent trades bids, offers and recent_trades on each market orderbook, ticker and trades
Price history market_stats
Events and competitions, the catalogue markets hang off GET /api/v1/events events

The market_data and events scopes cover all of it. The full list of what each scope opens is under App scopes.

  1. Go to apps.stxapp.io/data and choose Get data access.
  2. Create your account and name your organization. You land on Market data access in STX Apps.
  3. Confirm your email: open the link STX Apps sends you. Until you do, the page shows Confirm your email to get demo keys. with a Send the link again link instead of the key form.
  4. Choose Get data access keys. You get an access key ID and a secret key for the US demo exchange. In OAuth terms they are your client ID and client secret.
  5. Copy the secret key: it is shown once. You can get a new one from STX Apps at any time, and the old one stops working.

No redirect URI or server IP list is needed for data keys.

Owners, admins and data developers manage data access and its keys. See STX Apps.

Data credentials are a Server-to-server OAuth client: they hold app scopes only and use the client_credentials grant, with no member involved.

$STX is the exchange’s base URL. On the US demo exchange that is https://demo.stxapp.io; every host is on Environments.

Send the access key ID and secret key as HTTP Basic (-u). A secret in the form body is refused with invalid_client.

curl -s -u "$STX_CLIENT_ID:$STX_CLIENT_SECRET" "$STX/oauth/token" \
-d grant_type=client_credentials \
-d "scope=market_data events"
{ "access_token": "stxapp_at_…", "token_type": "bearer", "expires_in": 3600, "scope": "market_data events" }

scope is optional. Leave it out and the request still succeeds with 200: the token carries every app scope your client holds. Captured on US demo with a client that can also create markets:

curl -s -u "$STX_CLIENT_ID:$STX_CLIENT_SECRET" "$STX/oauth/token" \
-d grant_type=client_credentials
{ "scope": "market_data events markets.write", "expires_in": 3600, "access_token": "stxapp_at_…", "token_type": "bearer" }

Read scope in the response for what the token can do, and name the scopes you need when you want a narrower token. A scope your client does not hold is invalid_scope.

The token lasts an hour and has no refresh token: request a new one when it expires. Keep the secret and the token on your server. See App tokens and Tokens and security.

Send the token as a bearer. This reads open markets that are taking orders:

curl -s -H "Authorization: Bearer $STX_APP_TOKEN" \
"$STX/api/v1/markets?status=open&trading=true&competitions=NBA"
{
"cursor": "g2gDbQAAABCiuxfKlv2VBukIZG7zFzCAbgUB0pA2N4ZoA2EAbgcAANzktGFeBmIAAJzc",
"markets": [
{
"market_id": "1eb49edd-9bf0-4e86-8971-f8a19ff44b7d",
"symbol": "STXNBA-26OCT211900ATLORL-PRAORLNALEXANDERWALKER493112-25.5",
"title": "N. Alexander-Walker 25.5 PTS + REB + AST",
"question": "Will Nickeil Alexander-Walker have more than 25.5 Points + Rebounds + Assists?",
"status": "open",
"trading": true,
"price": "0.4850",
"last_traded_price": null,
"max_price": "1.0000",
"bids": [
{ "quantity": "652.00", "price": "0.4600" },
{ "quantity": "1111.00", "price": "0.4500" },
{ "quantity": "1704.00", "price": "0.4400" },
{ "quantity": "2439.00", "price": "0.4100" },
{ "quantity": "3076.00", "price": "0.3900" }
],
"offers": [
{ "quantity": "2926.00", "price": "0.5900" },
{ "quantity": "2272.00", "price": "0.5600" },
{ "quantity": "1630.00", "price": "0.5400" },
{ "quantity": "1063.00", "price": "0.5300" },
{ "quantity": "612.00", "price": "0.5100" }
],
"recent_trades": [],
"open_interest": "0.00",
"event_id": "776f2c7f-afc8-43fa-8a90-3cf581294143",
"event_title": "Atlanta Hawks at Orlando Magic",
"event_start": "2026-10-21T23:00:00.000000Z",
"competition": "NBA",
"sport": "Basketball"
}
]
}

The response is trimmed to one market and a few fields; List markets documents every field and filter. Pass cursor back to read the next page; it is null on the last one. Prices are dollar strings below the market’s max_price.

A token without the scope an endpoint needs gets 403, naming the missing scope in the challenge and the body. This is a token minted with events only, calling GET /api/v1/markets:

HTTP/2 403
www-authenticate: Bearer error="insufficient_scope", scope="market_data"
{"error":"insufficient scope for this operation","scope":"market_data"}

GET /api/v1/events with a market_data-only token is refused the same way, with scope="events".

Times: microseconds on events, ISO strings on markets

Section titled “Times: microseconds on events, ISO strings on markets”

An event’s start and its markets’ copy of it arrive in different formats. The same game, read both ways on US demo:

Where Field Value
GET /api/v1/events start_time 1792623600000000, an integer of Unix microseconds
GET /api/v1/events start_time_iso "2026-10-21T23:00:00.000000Z", the same instant as an ISO 8601 string
GET /api/v1/markets event_start "2026-10-21T23:00:00.000000Z", an ISO 8601 string
market:<market_id> join reply and market_update event_start 1792623600000000, Unix microseconds again

Divide a microsecond value by 1,000,000 for Unix seconds, or by 1,000 for the milliseconds JavaScript’s Date takes. Read start_time_iso when you would rather not convert.

Connect to /socket/websocket?vsn=2.0.0 with the token in the x-stx-oauth-token handshake header and a User-Agent, then join the topics you want. The socket runs Phoenix channels, so connecting subscribes you to nothing until you join; see WebSockets.

Join the book for the markets you show:

["1","1","orderbook","phx_join",{"market_ids":["0eae64df-90bb-48d6-92f1-37bebc90ddad"]}]
["1","1","orderbook","phx_reply",{"status":"ok","response":{"selected_market_ids":["0eae64df-90bb-48d6-92f1-37bebc90ddad"]}}]

What arrives next depends on whether the book has orders in it.

  • A market with resting orders sends one book push straight after the join reply, carrying the current book. This is the first push from the join above, on the market from the REST example:

    ["1",null,"orderbook","book",{"timestamp":"2026-10-21T06:12:18.095898Z","market_id":"1eb49edd-9bf0-4e86-8971-f8a19ff44b7d","timestamp_us":1792563138095898,"offers":[{"quantity":"612.00","price":"0.5100","total_liquidity":"312.1200","liquidity":"312.1200","total_quantity":"612.00"},{"quantity":"1063.00","price":"0.5300","total_liquidity":"875.5100","liquidity":"563.3900","total_quantity":"1675.00"},{"quantity":"1630.00","price":"0.5400","total_liquidity":"1755.7100","liquidity":"880.2000","total_quantity":"3305.00"},{"quantity":"2272.00","price":"0.5600","total_liquidity":"3028.0300","liquidity":"1272.3200","total_quantity":"5577.00"},{"quantity":"2926.00","price":"0.5900","total_liquidity":"4754.3700","liquidity":"1726.3400","total_quantity":"8503.00"}],"bids":[{"quantity":"652.00","price":"0.4600","total_liquidity":"299.9200","liquidity":"299.9200","total_quantity":"652.00"},{"quantity":"1111.00","price":"0.4500","total_liquidity":"799.8700","liquidity":"499.9500","total_quantity":"1763.00"},{"quantity":"1704.00","price":"0.4400","total_liquidity":"1549.6300","liquidity":"749.7600","total_quantity":"3467.00"},{"quantity":"2439.00","price":"0.4100","total_liquidity":"2549.6200","liquidity":"999.9900","total_quantity":"5906.00"},{"quantity":"3076.00","price":"0.3900","total_liquidity":"3749.2600","liquidity":"1199.6400","total_quantity":"8982.00"}]}]

    On the socket both sides are best first: offers starts at the lowest price, unlike REST.

  • A market with an empty book sends nothing after the join reply. There is no empty snapshot: silence means no orders. The first book with levels in it comes when an order rests. A push that arrives while the book is still empty has "bids": [] and "offers": [].

After that, a market pushes when its book changes, at most once every 200 ms however many orders change in that time. It is not a timer. On US demo over five minutes, the live market above sent its join snapshot, one push at 32 seconds, then six pushes within three seconds while the house maker re-quoted; an empty market sent nothing in six minutes; four new crypto markets sent nothing for 23 seconds, then their first books as the maker quoted them. Each push is the whole book for that market and can repeat the levels you already hold, so replace what you hold rather than applying it as a change.

Join trades the same way for executed trades, and ticker for a price summary per market. The pushes and their fields are under Order book, ticker and trades.

Take a snapshot over REST when you start, then keep it current from the socket. A market that sent no book after your join has an empty book. Limits on both are under Rate limits.

Live data terms, including how STX prices are shown and credited on your site, come with live access: STX shares them before you go live. Building and testing on demo credentials is free.

Showing STX prices on your site? Link each price to STX with your own affiliate links, and earn on the players you send. Use to= to land them on the event you show, for example https://demo.stxapp.io/r/ip7owude?to=/events/06f19033-c427-4cef-9647-794204e4e2e9.

STX is live for trading in Ontario. Market data access for Ontario is waitlisted in STX Apps for now: choose Ontario when you sign up, and STX emails you when Ontario demo access opens. In Ontario an app reads public market data only (market_data and events) and does not act for members.