REST reference
The read API — leagues, participants, contests, markets, liquidity, orderbook depth, and live game state, normalized across every connected venue. Authenticate with an API key. Use the “Try it” panel under each endpoint to send live requests right from this page.
Base URL
https://api.openmarkets.ai/flow/v1The catalog, liquidity, depth, and live-state endpoints on this page are read-only GETs (with one batch POST for fan-out reads). Pass your key as the X-API-Key header on every request.
The API is not read-only overall. Account, balance, order, and execution endpoints live under /flow/v1/auth/* and work with the same key — see Venues & balances and Placing orders. Your key reaches its own account with no X-OpenMarkets-Account header; GET /auth/me (below) reports what it can do.
lg_nfl, ct_abc123, p_kalshi, the 6-part position hash) exist only for readability. In production every id and hash is an opaque string — typically pg-<digits>-<rand>-<n>, and a few partners are bare UUIDs. Treat them all as opaque: store verbatim, never construct one, split a hash, or pattern-match on a prefix like p_kalshi.Response envelope
Every successful response uses the same top-level structure: data, optional pagination, and meta.
{
"data": [...] | {...},
"pagination": {
"next_cursor": "2026-04-17T22:00:00.000Z" | null,
"has_more": true,
"limit": 50
},
"meta": {
"timestamp": "2026-04-17T22:00:00Z",
"api_version": "v1"
}
}Catalog
Leagues, participants, contests, and markets.
/flow/v1/leaguesList leagues you have access to.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | no | Max items (default 50, max 200) |
/flow/v1/leaguesMax items (default 50, max 200)
/flow/v1/leagues/:idRetrieve a single league. Returns 404 with not_found if the league ID does not exist. participant_type describes the league's competitors: 'team', 'athlete' (e.g. tennis, golf), or null when the league doesn't declare one — don't assume it's always 'team'.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | yes | League ID returned from /leagues |
/flow/v1/leagues/lg_nflLeague ID returned from /leagues
{
"id": "lg_nba",
"name": "NBA",
"participant_type": "team", // "team" | "athlete" | null
"status": "active"
}/flow/v1/participantsList participants (teams or athletes) in a league. Requires league_id. Returns a slim shape suited to dropdowns and lookups.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| league_id | query | string | yes | League to scope to (required) |
| limit | query | integer | no | Max items (default 50, max 200) |
/flow/v1/participantsLeague to scope to (required)
Max items (default 50, max 200)
/flow/v1/participants/:idRetrieve a single participant. Returns 404 not_found if the participant ID does not exist.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | yes |
/flow/v1/participants/p_lakers/flow/v1/contestsList contests, filtered and paginated by scheduled start. Each list item carries a has_markets boolean — true when the contest has markets defined — so you can hide contests with nothing to price without a second call.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| league_id | query | string | no | Scope to a specific league |
| status | query | enum | no | Default: open |
| start_after | query | iso8601 | no | Only contests starting at or after this timestamp |
| start_before | query | iso8601 | no | Only contests starting at or before this timestamp |
| parent_contest_id | query | string | no | Children of a parent (e.g. matches in a tournament) |
| cursor | query | iso8601 | no | Pass the previous pagination.next_cursor to fetch the next page |
| limit | query | integer | no | Max items (default 50, max 200) |
/flow/v1/contests?status=openScope to a specific league
Default: open
Only contests starting at or after this timestamp
Only contests starting at or before this timestamp
Children of a parent (e.g. matches in a tournament)
Pass the previous pagination.next_cursor to fetch the next page
Max items (default 50, max 200)
/flow/v1/contests/:idRetrieve a single contest.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | yes |
/flow/v1/contests/ct_abc123{
"id": "ct_abc123",
"league_id": "lg_nfl",
"title": "Kansas City Chiefs vs. San Francisco 49ers",
"type": "match",
"starts_at": "2026-09-07T20:20:00Z",
"status": "open",
"has_markets": true,
"tags": { "home_team": "KC", "away_team": "SF" },
"created_at": "2026-08-01T12:00:00Z",
"updated_at": "2026-08-15T18:30:00Z"
}/flow/v1/contests/:id/marketsList all markets (moneyline, spread, total, etc.) available on a contest.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | yes |
/flow/v1/contests/ct_abc123/markets/flow/v1/markets/:idRetrieve a single market definition (not specific to any contest).
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | yes |
/flow/v1/markets/mk_mlLiquidity & depth
Best-available prices per position, and full per-partner orderbook depth.
/flow/v1/contests/:id/liquidityCurrent aggregated liquidity positions for the contest, with per-partner prices and distribution-model fair values. available is a float — notional size in USD, not a whole-share/contract count — so don't parse it as an integer.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | yes |
/flow/v1/contests/ct_abc123/liquidity{
"position_hash": "ct_abc123:mk_ml:side_home:var_0:p_kc:tf_full",
"contest_id": "ct_abc123",
"title": "Kansas City Chiefs — Moneyline",
"market_key": "moneyline",
"side_key": "home",
"participant_id": "p_kc",
"consensus_price": 0.52,
"partner_liquidities": [
{
"partner_id": "kalshi",
"partner_name": "Kalshi",
"price": 0.49,
"american_odds": "+104",
"available": 5400.0,
"liquidity_hash": "kalshi:ct_abc123:mk_ml:side_home:var_0:p_kc:tf_full"
}
]
}/flow/v1/depth/:position_hashFull orderbook depth across all partners for a single 6-part position hash. Bulk depth lookup is planned but not yet available — query one hash at a time.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| position_hash | path | string | yes | Same value returned by /contests/:id/liquidity |
/flow/v1/depth/ct_abc123%3Amk_ml%3Aside_home%3Avar_0%3Ap_kc%3Atf_fullSame value returned by /contests/:id/liquidity
{
"position_hash": "ct_abc123:mk_ml:side_home:var_0:p_kc:tf_full",
"partners": [
{
"partner_id": "kalshi",
"partner_name": "Kalshi",
"levels": [
{ "price": 0.49, "available": 2100 },
{ "price": 0.48, "available": 3300 }
],
"total_available": 5400,
"best_price": 0.49,
"fetched_at": "2026-04-17T22:00:00Z"
}
],
"fetched_at": "2026-04-17T22:00:00Z"
}/flow/v1/contests/:id/price-historyHistorical price / liquidity series for a contest's positions, from live tick-by-tick out to all-time. Paid capability — requires the price_history entitlement (403 without it). With resolution=auto (the default) the bucket size is chosen from the window width, so you can request any range without tuning it. Always read resolution off the response: it reports what was actually served, which may be coarser than requested. Caveats: an unknown contest id returns 200 with an empty points array, not a 404; and series_truncated appears when the window held more series than limit could cover.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | yes | |
| from | query | integer | no | Range start, unix ms (default: 1h before to) |
| to | query | integer | no | Range end, unix ms (default: now) |
| resolution | query | enum | no | Bucket size. auto picks from the window: ≤6h → tick, ≤24h → 5m, ≤7d → 1h, ≤90d → 4h, beyond → 1d. Explicit values are honoured where the data supports them — 1m is served from ticks inside the 6h window and degrades to 5m beyond it (the response says which). |
| position_hash | query | string | no | Comma-separated position hashes to scope to. Strongly recommended: a contest can carry hundreds of series, and scoping is both far faster and avoids series_truncated. |
| partner_id | query | string | no | Scope to one venue |
| limit | query | integer | no | Max points returned, budgeted evenly ACROSS series so each keeps a plottable shape. Max 50000. |
/flow/v1/contests/ct_abc123/price-history?resolution=auto&limit=5000Range start, unix ms (default: 1h before to)
Range end, unix ms (default: now)
Bucket size. auto picks from the window: ≤6h → tick, ≤24h → 5m, ≤7d → 1h, ≤90d → 4h, beyond → 1d. Explicit values are honoured where the data supports them — 1m is served from ticks inside the 6h window and degrades to 5m beyond it (the response says which).
Comma-separated position hashes to scope to. Strongly recommended: a contest can carry hundreds of series, and scoping is both far faster and avoids series_truncated.
Scope to one venue
Max points returned, budgeted evenly ACROSS series so each keeps a plottable shape. Max 50000.
{
"contest_id": "ct_abc123",
"resolution": "5m",
"from": 1751050800000,
"to": 1751054400000,
"points": [
{ "t": 1751050800000, "position_hash": "ct_abc123:mk_ml:side_home:var_0:p_kc:tf_full", "price": 0.51 }
]
}Arbitrage
Cross-venue arbitrage: two mutually exclusive positions whose combined price is under 1.0, so both legs together lock a profit regardless of the result. We detect these continuously across every connected venue as liquidity changes, and expose the live set here. Paid capability — requires the arbitrage:read entitlement (403 without it).
after_fee_roi_pct and profitable_after_fees computed from each venue's own fee formula, and the legs we surface are the pair that is cheapest after fees — not the pair that looks cheapest gross. Trade off the after-fee numbers, not roi_pct.Both fields are null when a venue didn't publish a fee packet for one of the legs. That means “fees unknown”, not “fee-free” — so after_fee=true excludes those arbs rather than assuming the best case.
Arbs are ephemeral: the underlying detection cache holds each one for at most 120 seconds and it is rewritten on every liquidity change. Treat a response as a snapshot of this instant. For anything latency-sensitive, use the arbitrage WebSocket channel instead of polling.
/flow/v1/arbitragesLive cross-venue arbitrage opportunities. Filters compose as AND. Scope with contest_id or league_id where you can — an unfiltered call returns every open arb across every league. Each arb carries both legs with every venue quoting them; options[0] on each side is the selected, cross-venue pair the metrics describe.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| contest_id | query | string | no | Comma-separated contest ids |
| league_id | query | string | no | Comma-separated league ids |
| market_id | query | string | no | Comma-separated market ids (e.g. moneyline only) |
| partner_id | query | string | no | Only arbs where this venue quotes at least one leg |
| min_roi | query | string | no | Minimum GROSS guaranteed profit %, before fees |
| after_fee | query | enum | no | true → only arbs still profitable after taker fees on both legs. Arbs with unknown fees are excluded. |
| min_after_fee_roi | query | string | no | Minimum AFTER-FEE profit %. Implies after_fee=true. |
| min_investment | query | string | no | Minimum deployable USD across both legs |
| sort | query | enum | no | Descending. Under after_fee_roi, arbs with unknown fees sort last. |
| limit | query | integer | no | Max arbs returned. Max 500. meta.total reports the pre-truncation count. |
/flow/v1/arbitrages?sort=roi&limit=100Comma-separated contest ids
Comma-separated league ids
Comma-separated market ids (e.g. moneyline only)
Only arbs where this venue quotes at least one leg
Minimum GROSS guaranteed profit %, before fees
true → only arbs still profitable after taker fees on both legs. Arbs with unknown fees are excluded.
Minimum AFTER-FEE profit %. Implies after_fee=true.
Minimum deployable USD across both legs
Descending. Under after_fee_roi, arbs with unknown fees sort last.
Max arbs returned. Max 500. meta.total reports the pre-truncation count.
[
{
"arbitrage_id": "3f2a91c4-8b17-4d02-9e55-7ac1b0d3e884",
"contest": {
"id": "ct_abc123",
"label": "Lakers @ Rockets",
"league_id": "lg_nba"
},
"market": { "id": "mk_ml", "variable_id": "var_0" },
"combined_price": 0.968,
"roi_pct": 3.31,
"after_fee_roi_pct": 1.18,
"profitable_after_fees": true,
"max_investment": 412.5,
"sides": [
{
"market_side_id": "side_home",
"title": "Lakers",
"options": [
{
"partner_id": "p_kalshi",
"partner_name": "Kalshi",
"price": 0.478,
"after_fee_price": 0.492,
"available": 900,
"title": "Lakers",
"position_hash": "ct_abc123:mk_ml:side_home:var_0:p_lal:tf_full",
"source": "streaming"
}
]
},
{
"market_side_id": "side_away",
"title": "Rockets",
"options": [
{
"partner_id": "p_prophetx",
"partner_name": "ProphetX",
"price": 0.49,
"after_fee_price": 0.496,
"available": 640,
"title": "Rockets",
"position_hash": "ct_abc123:mk_ml:side_away:var_0:p_hou:tf_full",
"source": "polling"
}
]
}
],
"detected_at": "2026-04-17T22:00:00.000Z"
}
]Acting on an arb
- Both legs or neither. The profit only exists as a pair. A filled first leg with a failed second is a naked position, not an arb — size to what both venues can actually absorb (
max_investmentis the binding constraint across the two) and have a plan for a one-sided fill. - They close fast. Between detection and your order reaching a venue, the price that created the edge may already be gone. The narrower the edge, the more this matters.
- Verify depth before sizing.
availableis size at the best price only. For the full ladder, pass a leg'sposition_hashtoGET /depth/:position_hash(requiresdepth:read). - Detection is not advice. We report a priced relationship between two venues. Venue rules, settlement-source differences, and your own account limits are yours to check.
Live contest state
Live game state — score, clock, period, box score — churns on a different cadence than contest metadata, so it lives on its own endpoints. Available where we have a sports-data feed for the league. For real-time updates, subscribe to the contest_state channel on the WebSocket stream instead of polling.
/flow/v1/contests/:id/stateLive game state for a contest — game_status, clock, period, scores or leaderboard, and a sport-specific situation block. Returns 404 if no state has been cached for the contest yet. For team games, scores is { home_score, away_score, period_scores[] } (per-period breakdowns) — not { home, away }. For individual-sport contests the block is a leaderboard instead.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | yes |
/flow/v1/contests/ct_abc123/state{
"contest_id": "ct_abc123",
"state": {
"game_status": "in_progress",
"clock": { "display": "7:42", "remaining_seconds": 462, "is_running": true },
"period": { "number": 3, "label": "3rd Quarter", "type": "quarter" },
"contest_result": "pending",
"winner_participant_id": null,
"scores": {
"home_score": 71,
"away_score": 68,
"period_scores": [
{ "home_score": 24, "away_score": 22 },
{ "home_score": 25, "away_score": 20 },
{ "home_score": 22, "away_score": 26 }
]
},
"situation": null
},
"refreshed_at": "2026-05-26T02:14:30Z",
"cached_at": "2026-05-26T02:14:31Z"
}/flow/v1/contest-state/batchFan-out read: live state for many contests in one request (max 200). Body is { contest_ids: string[] }. Returns a map keyed by contest_id — contests with no cached state are simply absent (no 404). Use this to hydrate a multi-game scoreboard without N round-trips.
/flow/v1/contest-state/batch// Request body
{ "contest_ids": ["ct_abc123", "ct_def456"] }
// Response.data — keyed by contest_id; misses are omitted
{
"ct_abc123": {
"contest_id": "ct_abc123",
"state": { "game_status": "in_progress", "scores": { "home_score": 71, "away_score": 68, "period_scores": [] } },
"refreshed_at": "2026-05-26T02:14:30Z"
}
}Partners catalog
The exchange venues OpenMarkets aggregates. Connecting a venue is done by the user in the hosted flow, not through the API — see /developers/partners-and-balances.
/flow/v1/partnersThe catalog of exchange venues OpenMarkets supports. The response also carries the credential schema each venue expects — that is what OpenMarkets' hosted connect flow renders; you never collect credentials yourself. New venues appear without a client release.
/flow/v1/partners{
"data": [
{
"partner_id": "p_kalshi",
"name": "Kalshi",
"status": "active",
"credential_fields": [
{ "key": "api_key", "label": "API Key", "type": "text", "required": true },
{ "key": "private_key_pem", "label": "Private Key (PEM)", "type": "textarea", "required": true }
]
}
]
}/flow/v1/partners/:partner_idRetrieve a single partner descriptor (same shape as an item from /flow/v1/partners). Returns 404 not_found if the partner ID does not exist.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| partner_id | path | string | yes |
/flow/v1/partners/p_kalshiYour key & entitlements
GET /flow/v1/auth/me reports what the calling key (or JWT) is and what it can do — key type, whether it can place orders, your effective rate limits (which may exceed the documented defaults), recent usage, and your reasoning-credit balance. This is the entitlements endpoint: read can_execute before offering trading, and router_account_id to know which account your reads and writes land on.
/flow/v1/auth/meThe caller's own context. Works with an API key (its own account — no X-OpenMarkets-Account header needed) or a player JWT. can_execute:true means this key may post fills via POST /auth/orders/buy (see Placing orders); a data key is always false.
/flow/v1/auth/me{
"auth_method": "api_key", // "api_key" | "jwt"
"router_account_id": "ra_...", // the account your /auth/* reads + orders hit
"can_execute": true, // gates POST /auth/orders/buy
"current_key": {
"key_type": "execution", // "data" | "execution"
"rate_limit_per_minute": 300, // YOUR real limits — can exceed the 60/min default
"rate_limit_per_day": 50000
},
"usage": { "requests_last_24h": 1204, "requests_last_7d": 8130, "errors_last_24h": 3 },
"credits": { // reasoning-credit wallet (fuels the MCP "ask" tool)
"allowance": 1293, // monthly plan credits remaining (resets each period)
"purchased": 0, // bought credits (persist across periods)
"total": 1293, // allowance + purchased
"included_per_month": 5000, // meter denominator
"used_this_period": 3707 // consumed this period
}
}Performance & P&L
GET /flow/v1/auth/performance returns realized profit-and-loss for the calling account over a date window, computed server-side from settled orders — so you don't roll up order history on the client. Results are always partitioned by currency (real USD vs. practice ATLAS — never blended), and can optionally be broken down per day, league, venue, model, or opportunity type via group_by.
/flow/v1/auth/performanceCurrency-partitioned P&L for the authed account, aggregated from settled orders over a window (default: last 90 days). Optionally grouped with group_by. Uses the key's own account — no X-OpenMarkets-Account header needed; returns 403 scope_required if the key has no account.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| from | query | iso8601 | no | Window start (ISO datetime). Default: 90 days ago. |
| to | query | iso8601 | no | Window end (ISO datetime). Default: now. |
| group_by | query | enum | no | Per-group breakdown inside each currency bucket. Default: none. |
/flow/v1/auth/performance?group_by=noneWindow start (ISO datetime). Default: 90 days ago.
Window end (ISO datetime). Default: now.
Per-group breakdown inside each currency bucket. Default: none.
{
"by_currency": [
{
"funding": {
"currency": "USD",
"is_real_money": true,
"symbol": "$",
"precision": 2,
"label": "Real money"
},
"staked": 1250.0, // SUM(amount) over settled win/lose orders
"net_pl": 143.75, // SUM(net_winnings) - SUM(amount)
"roi": 0.115, // net_pl / staked (0 when staked == 0)
"settled_count": 42,
"pending_count": 3, // still-open orders in this currency
"wins": 25,
"losses": 15,
"pushes": 2,
"cancelled": 0,
"groups": [ // present when group_by != "none"
{ "key": "lg_nba", "label": "NBA", "staked": 800.0, "net_pl": 96.0,
"roi": 0.12, "settled_count": 28, "wins": 17, "losses": 10, "pushes": 1 }
]
}
],
"from": "2026-04-16T00:00:00.000Z",
"to": "2026-07-15T00:00:00.000Z",
"group_by": "league"
}Account, orders & execution
Reading a user's connected venues, balances, and order history — and placing orders — all live under /flow/v1/auth/* and are documented on their own pages:
- Venues & balances —
GET /auth/account/partners,GET /auth/account/balances. - Placing orders —
POST /auth/orders/buy,GET /auth/orders, resting, cancel, and resolution.
- Rolled-up positions over REST:
GET /auth/account/positions(cursor-paginated, default 50 / max 200) — one row per (venue, market, side) withcost_basis,payout,net_pnl(nullwhile open),outcome, andstatus. Filter with?status=openor?status=settled(server-side, so finding your open positions doesn't page the whole lifetime book) and?partner_id. The responsedata.sourcetells you which book answered:- Default (
source: "orders") — rolls up your OpenMarkets order book (everything placed through FlowPOST /auth/orders/buy). These settle the moment we resolve the market, so a finished game never lingers asopen. This is what you want for "what did I bet through OpenMarkets, and how did it settle?" ?history=1(source: "history") — rolls up the connected account's full venue history (partner_account_history), a separate ledger that includes bets placed outside OpenMarkets, keyed on the venue's ownposition_key. Retroactive coverage varies by venue. Use this for "everything this connection has ever done." Same MCPlist_account_positionstool, same row shape.
GET /auth/account/history(always the connect-history ledger). - Default (
- Venue
logo_urlis onGET /partners(catalog),/auth/orders,/auth/opportunities, and now/auth/account/partners+/auth/account/balances.
Pagination
List endpoints return a pagination object. When has_more is true, pass next_cursor back as the cursor query param to fetch the next page. Cursors are opaque but stable — don't construct them yourself.
Coming soon
Planned but not yet available:
- Batch depth lookup — multiple position hashes in one request
- Webhooks for liquidity and settlement events