Skip to content

List your own markets on STX

An approved app can list its own player-prop markets on STX events. You name the event, the player, the stat and the line; STX builds the market, settles it from its stats feed, and tells you what happened to each one you asked for.

This is separate from adding STX accounts to your app. You can do either without the other.

Adding STX accounts Creating markets
Who acts Your app, for a member who approved it Your app, as itself. No member, no login, no consent screen
Token A member’s access token An app token (client_credentials)
Access Any registered app Granted by STX on request, per app
Where calls come from Your allow-listed addresses, when you hold a write scope Your allow-listed addresses, always

An API key cannot create markets. A member token cannot either.

The market creation flowTwo lanes: your app and STX. Your app requests an app token with client_credentials and the markets.write scope. It lists events to get an event_id, then gets the event's players for a player_id and the allowed stats. It posts the markets; STX checks the player, stat, half line and that the event has not started, builds each market, and answers created, exists or rejected for each one. The new markets then trade like any other.Your app (server)STX1POST /oauth/token (client_credentials, markets.write)app access token2GET /api/v1/eventsevent_id3GET /api/v1/events/{event_id}/playersplayer_id per player, allowed stats4POST /api/v1/markets (player_id, stat, line), up to 25Checks player, stat, .5 line, not startedbuilds title, rules and price rangeper market: created | exists | rejected (reason)Then: ordinary markets. Quote them with your market-making account through the orders endpoints and channels.

Ask your STX contact to enable market creation for your app, on the demo exchange first. STX then:

  • adds the markets.write app scope to your client. Only STX can grant it;
  • sets your client’s IP allow-list to the addresses your servers call from. A call from any other address is refused.

Every call on this page is authenticated the same way: an app token in the Authorization header. It is not a member’s token and not an API key, and the call must come from one of your allow-listed addresses.

Request the token with the client_credentials grant, authenticating with your client_id and client_secret over HTTP Basic. Ask for markets.write, and events for the read calls in steps 2 to 4. $STX in the examples is the base URL of the exchange you are calling. Environments lists them.

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

Keep access_token as $APP_TOKEN for the calls below. There is no refresh token: request a new one when it expires. See App tokens.

curl -s "$STX/api/v1/types" -H "Authorization: Bearer $APP_TOKEN"

The response has one entry per rule. This is the player_stat_line entry, with the other rules, event types and stats left out:

{
"player_stat_line": {
"partner_create": true,
"fields": {
"player_id": { "type": "string", "description": "A player_id from GET /api/v1/events/{event_id}/players." },
"stat": { "type": "string", "description": "One of the event type's stats." },
"line": { "type": "number", "description": "A positive half line, e.g. 19.5." }
},
"event_types": {
"basketball_game": {
"sport": "Basketball",
"stats": [
{ "name": "Assists", "stat": "ASSISTS" },
{ "name": "Points", "stat": "POINTS" },
{ "name": "Steals", "stat": "STEALS" }
]
}
}
},
"spread_r1": { "event_types": ["baseball_game", "football_game", "soccer_game"], "partner_create": false }
}

GET /api/v1/types lists the market rules, keyed by rule. A rule with partner_create: true can be created through the API, and lists the fields an entry takes and, for each event type, the stats on offer.

Today one rule can be created: player_stat_line, over or under a player’s stat. Read /types once and cache it; it changes only when STX adds a rule or a stat.

curl -s "$STX/api/v1/events?status=scheduled" -H "Authorization: Bearer $APP_TOKEN"

GET /api/v1/events lists upcoming events. Take the event’s id. Markets can be created only while the event is scheduled and has not started.

curl -s "$STX/api/v1/events/$EVENT_ID/players" -H "Authorization: Bearer $APP_TOKEN"

Shortened to two players and three stats:

{
"event_id": "25f19747-3398-4333-81c3-2e457f83bda1",
"teams": [
{
"team": "Orlando Magic",
"players": [
{ "name": "Alex Morales", "position": "G", "jersey": "30", "player_id": "4593034" },
{ "name": "Anthony Black", "position": "G", "jersey": "0", "player_id": "4712849" }
]
}
],
"stats": [
{ "name": "Assists", "stat": "ASSISTS" },
{ "name": "Points", "stat": "POINTS" },
{ "name": "Steals", "stat": "STEALS" }
]
}

GET /api/v1/events/{event_id}/players returns each team’s roster, and the stats on offer for this event.

  • Rosters are the teams’ current rosters, not game-day actives, and can be up to 15 minutes old.
  • teams is empty until a roster is published. Try again later.
  • Send player_id back exactly as returned.

POST /api/v1/markets takes one event and 1 to 25 markets. Send it with the app token from step 1, from an allow-listed address:

curl -s -X POST "$STX/api/v1/markets" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event_id": "25f19747-3398-4333-81c3-2e457f83bda1",
"markets": [
{ "rule": "player_stat_line", "player_id": "4593034", "stat": "STEALS", "line": 0.5 }
]
}'
Field Value
event_id The event, from step 3.
dry_run Optional. true checks everything and creates nothing; see Dry run. Defaults to false.
rule Required. player_stat_line.
player_id Required. From step 4.
stat Required. One of the event’s stats, such as STEALS.
line Required. A positive half line, such as 0.5 or 19.5.

The response is 200 with one outcome per entry, in request order:

{
"event_id": "25f19747-3398-4333-81c3-2e457f83bda1",
"markets": [
{ "index": 0, "reason": null, "status": "created", "market_id": "cbcc7ea5-a3c4-4c38-a00b-17bdccd04816" }
]
}

index is the entry’s position in your request, from 0. All four fields are always present.

Send the same request again and the market is not created twice. It answers exists, with the same market_id:

{
"event_id": "25f19747-3398-4333-81c3-2e457f83bda1",
"markets": [
{ "index": 0, "reason": null, "status": "exists", "market_id": "cbcc7ea5-a3c4-4c38-a00b-17bdccd04816" }
]
}
status Meaning market_id
created A new market. STX sets its title, rules and price range. The new market
exists A market for the same player, stat and line is already on the event. Nothing new is created. The existing market
valid Dry run only: the entry passed every check and would be created. null
rejected Not created. reason says why. null
reason Why What to do
invalid_market The entry is malformed: it is not an object, has no rule or no string player_id, or holds a field other than rule, player_id, stat and line. An entry in the older stat_detail form gets this too. Fix the entry.
unsupported_rule rule is not one the API creates. Use a rule with partner_create: true in /types.
unsupported_stat The stat is not on offer for this event. Use one of the event’s stats.
invalid_line The line is not a positive half, such as 19.5. Fix the line.
unknown_player player_id is not on the event’s rosters. Read the players again.
event_not_open The event started while the request ran. Stop; markets cannot be added to it.
not_created The entry was valid, but the save failed. Resend the same request; see below.

A request can mix outcomes. Here the first entry is created, the second already exists, and the next three are rejected: a whole-number line, a player who is not on the event, and a stat the event does not offer.

{
"event_id": "cf1bfed8-106e-44ea-a3f1-351be4765728",
"markets": [
{ "index": 0, "reason": null, "status": "created", "market_id": "a3868943-bc39-477b-9c02-d3496514167d" },
{ "index": 1, "reason": null, "status": "exists", "market_id": "787ec653-a60a-4c1d-8cc1-87523e3d207e" },
{ "index": 2, "reason": "invalid_line", "status": "rejected", "market_id": null },
{ "index": 4, "reason": "unknown_player", "status": "rejected", "market_id": null },
{ "index": 5, "reason": "unsupported_stat", "status": "rejected", "market_id": null }
]
}

The entries at index 3 and 6 to 11 of that response are left out here.

  • A market you already created answers exists with the same market_id, whether you resend it or repeat it within one request. It is never created twice.
  • The valid entries of a request are saved together, all or nothing. If that save fails, every one of them answers rejected with not_created and none is created, so you can send the same request again as it is.

With "dry_run": true, every check runs and nothing is created. An entry that would be created answers valid, and so does a repeat of it in the same request. A market that is already listed answers exists with its market_id. Anything else answers rejected with its reason. Use it to check a batch before you list it.

The request from step 5 with "dry_run": true, before the market existed:

{
"event_id": "25f19747-3398-4333-81c3-2e457f83bda1",
"markets": [
{ "index": 0, "reason": null, "status": "valid", "market_id": null }
]
}
Status Body Meaning
400 {"error": "markets must be a list of 1 to 25 markets"} markets is missing, empty or longer than 25.
400 {"error": "dry_run must be true or false"} dry_run is not a boolean; the string "true" is refused.
400 {"error": "event_id is required"} event_id is missing.
401 {"error": "Missing or invalid API key credentials"} No token was sent.
403 {"error": "insufficient scope for this operation", "scope": "markets.write"} The app token does not carry markets.write.
403 {"error": "creating markets needs an OAuth app token with the markets.write scope"} An API key or a member token was used.
403 {"error": "ip_not_allowed", "error_description": "this app does not allow requests from this IP address"} The call came from an address outside your allow-list.
404 {"error": "event not found"} Unknown event.
422 {"error": "event_not_open"} The event is not scheduled, or has already started.
422 {"error": "unsupported_event"} No player props for this event’s type.
422 {"error": "roster_unavailable"} The event’s rosters are not published yet.

Measured on the demo exchange, from one client, with a few samples each. About 0.14 seconds of every figure is the network round trip.

Request Time to answer
1 market, dry run About 0.16 seconds
1 market, created About 0.17 seconds
1 market that already exists About 0.15 seconds
25 new markets, dry run 0.33 to 0.44 seconds

A created market was returned by GET /api/v1/markets?market_ids=<market_id> on the first request sent after the create answered.

Treat these as what to expect, not as a guarantee: the samples are few, and a first request on a new connection adds the time to connect.

On a busy system a batch can take much longer, and a request that times out or answers a 5xx may still have created its markets. Send the same request again, or send it with dry_run: true: entries that were created answer exists with their market_id, and nothing is created twice. Allow at least 30 seconds before your client gives up on a request.

The new markets behave like any other STX market. Find them with GET /api/v1/markets and the market data channels (to look one up by id, use ?market_ids=<market_id>; there is no /markets/{id} route), and quote them with your market-making account’s API key through the orders endpoints. STX settles them from its stats feed when the game ends.

@stxapp/stx-typescript 0.8.2 and later wraps these calls: npm install @stxapp/stx-typescript.

import { AppScopes, OAuthClient } from "@stxapp/stx-typescript/oauth";
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 markets = [{ rule: "player_stat_line", playerId: player.player_id, stat: roster.stats[0].stat, line: 19.5 }];
// Check first, then create.
const checked = await stx.createMarkets(eventId, markets, { dryRun: true });
if (checked.every((r) => r.status === "valid" || r.status === "exists")) {
const results = await stx.createMarkets(eventId, markets);
for (const r of results) console.log(r.index, r.status, r.market_id ?? r.reason);
}

createMarkets() returns one result per market, in the order you passed them, with the response’s index, status, market_id and reason. { dryRun: true } sends dry_run. { validate: true } checks players, stats and lines on your side before sending. stx.marketTypes() reads GET /api/v1/types.