Venues & balances

GET /flow/v1/partners · GET /flow/v1/auth/account/partners · GET /flow/v1/auth/account/balances
Live

Read a user's connected venues and balances through the API. Venues are connected by the user in the hosted flow — you never collect or send exchange credentials through the API.

Connecting a venue is not an API operation

Your code never handles exchange credentials. Venue connection — entering an API key, a password or a private key and validating it — happens on OpenMarkets' hosted page, on our infrastructure. There is no documented endpoint for a third party to submit a venue credential, and the hosted page is the supported path. This keeps you out of scope for handling exchange secrets.

The hosted page opens three ways, depending on whose account it is — see Connect:

  • Your own account — from the console's Connect page.
  • Your app's users — your app opens it for the signed-in person with the account:link scope.
  • Your own users (an organization) — your backend mints a one-time link; the user enters credentials on connect.openmarkets.ai, never through you. See the Connect integration guide.

Minting a hosted link for one of your own users (the only "connect" call an organization's backend makes):

curl -X POST https://api.openmarkets.ai/flow/v1/connect/link-sessions \
  -H 'X-API-Key: om_..._live_...' \
  -H 'Content-Type: application/json' \
  -d '{ "external_user_id": "your-user-123",
        "return_url": "https://app.yourco.com/openmarkets/done" }'

# → { "hosted_url": "https://connect.openmarkets.ai/start?token=lt_live_..." }
# Redirect the user to hosted_url. They connect their venues there.

What the API gives you

Once a user has connected venues in the hosted flow, the API is read-only over them:

  • GET /flow/v1/partners — the catalog of venues OpenMarkets supports.
  • GET /flow/v1/auth/account/partners — a user's connected venues + balances.

The shape:

  • partner — a supported venue (Kalshi, BettorEdge, Polymarket, …).
  • account_partner — a user's connection to that venue (status, balance).
  • credential_status — pending (linked, not yet validated), connected (validated), failed — all set by the hosted flow, never by you.

See supported venues

Read-only catalog of every venue OpenMarkets supports. (The response also carries the credential schema each venue expects — that's what the hosted flow renders; you don't build a connect form.)

curl https://api.openmarkets.ai/flow/v1/partners \
  -H 'X-API-Key: om_data_live_...'
{
  "data": [
    {
      "partner_id": "p_kalshi",
      "name": "Kalshi",
      "status": "active",
      "orders_allowed": true,
      "connectable": true,
      "internal_book": false,
      "order_config": {
        "orders_allowed": true,
        "credentials_allowed": true,
        "min_order_amount": 1,
        "max_order_amount": null,
        "amount_increment": null,
        "whole_contracts": true,          // amount / price must be a whole number of contracts
        "full_fill_always_allowed": false,
        "price_submitted": true,          // we submit a price
        "price_format": "cents",
        "price_tick": 1,                  // whole cents
        "min_price": 1,
        "max_price": 99,
        "cancel_supported": true,
        "supported_order_types": ["market", "limit"]  // this venue rests limit orders
      }
    },
    {
      "partner_id": "p_bettoredge",
      "name": "BettorEdge",
      "status": "active",
      "orders_allowed": true,
      "connectable": true,
      "internal_book": false,
      "order_config": {
        "orders_allowed": true,
        "credentials_allowed": true,
        "min_order_amount": 1,
        "max_order_amount": null,
        "amount_increment": 0.01,         // any cent stake
        "whole_contracts": false,
        "full_fill_always_allowed": true, // accept-by-id: full take waives the minimum
        "price_submitted": false,         // you take the offer's price; nothing to snap
        "price_format": "american",
        "price_tick": 1,
        "min_price": null,
        "max_price": null,
        "cancel_supported": true,
        "supported_order_types": ["market"]           // immediate-only: limit 400s here
      }
    }
  ]
}

Order constraints (the venue grid)

Each venue's order_config is the grid an order on that venue must satisfy. It is per-venue (not per-market), so read it once from GET /flow/v1/partners, cache it, and join it to a price by partner_id — every partner_liquidities[] entry in the liquidity feed carries the partner_id to join on.

  • orders_allowed — false ⇒ read-only / quotes-only; don't offer an order ticket for that venue.
  • min_order_amount / max_order_amount — USD bounds (max_order_amount: null ⇒ only the position's available caps the order).
  • full_fill_always_allowed — accept-by-id venues waive the minimum when you take the whole resting offer.
  • whole_contracts — amount / price must be a whole number of contracts; size the stake so it doesn't floor to zero.
  • amount_increment — USD step when not whole-contract (e.g. 0.01).
  • price_submitted — false ⇒ accept-by-id (you take the offer's price; there's nothing to validate); true ⇒ we submit a price on the grid below.
  • price_format / price_tick / min_price / max_price — the venue's native price grid (e.g. Kalshi: whole cents, 1–99). These are in price_format units, not the 0–1 probability the liquidity feed quotes.
  • cancel_supported — whether a resting order can be cancelled.
  • supported_order_types — which order_type values /orders/buy accepts for this venue. Every venue supports "market"; "limit" is present only where the venue rests orders. Requesting a type absent here returns 400 unsupported_order_type.
Using the grid to size the order form is an optimization, not a requirement — the server enforces the same rules on POST /orders/buy and returns a per-leg rejected_code (below_min_order, above_max_order, unfillable, price_out_of_band, orders_not_supported) if an order violates them. Pre-validating just gives the user the feedback before they submit.

Read a user's connected venues + balances

Returns the acting account's connected venues with cached balances. On a player JWT it's the user's own account; for Connect, scope to one of your users with X-OpenMarkets-Account.

curl https://api.openmarkets.ai/flow/v1/auth/account/partners \
  -H 'X-API-Key: om_..._live_...' \
  -H 'X-OpenMarkets-Account: your-user-123'
{
  "data": {
    "router_account_id": "ra_...",
    "total_balance": 1234.56,
    "account_partners": [
      {
        "account_partner_id": "ap_...",
        "partner_id":          "p_kalshi",
        "partner_name":        "Kalshi",
        "status":              "active",
        "credential_status":   "connected",
        "balance":             892.10,   // what Kalshi last told us
        "expected_balance":    842.10,   // minus $50 committed to an open order
        "pending":             -50.00,
        "balance_as_of":       "2026-08-24T16:04:11Z",
        "is_stale":            false
      },
      {
        "account_partner_id": "ap_...",
        "partner_id":          "p_bettoredge",
        "partner_name":        "BettorEdge",
        "status":              "active",
        "credential_status":   "connected",
        "balance":             342.46,
        "expected_balance":    342.46,   // nothing committed
        "pending":             0,
        "balance_as_of":       "2026-08-24T16:03:52Z",
        "is_stale":            false
      }
    ],
    "expected_total_balance": 1184.56
  }
}

balance is a cached snapshot of what the venue last reported. It refreshes on a schedule, whenever the user connects in the hosted flow, and when you call the refresh endpoint below. expected_balance is that snapshot net of orders placed since — see the next section.

balance vs expected_balance — which one to show

Every venue reports two numbers, and picking the wrong one is the most common way to render a balance that is quietly wrong.

  • balance — the venue's own last word, taken at balance_as_of. This is the figure that reconciles against a statement from the venue. It does not know about orders placed since.
  • expected_balance — that snapshot minus funds committed to orders we have placed for the user since. This is what to display anywhere the user is deciding what they can spend.
  • pending — the difference. Negative means funds held against open orders; positive means settlements landed that the snapshot predates.

Concretely: a user with $1,000 at Kalshi places a $50 order. balance still reads 1000 until the next refresh; expected_balance reads 950 immediately. Showing balance there invites them to spend money they have already committed.

Check is_stale before you trust either figure. It is true when the venue snapshot is older than our refresh SLA, or was never taken at all — which means expected_balance is anchored to a number that may have moved. Treat it as advisory and consider calling the refresh endpoint, or say "as of {balance_as_of}" in your UI rather than presenting it as current.

balance_source tells you where the figure came from: venue_api (the venue told us), om_wallet (our own practice ledger — always exact, never stale), or null for a venue that exposes no balance endpoint at all, in which case there is no honest expected figure for it.

We can only see what we routed. If the user trades directly at the venue's own app, nothing in expected_balance reflects it until the next refresh reads the venue again. That is what balance_as_of and is_stale are for: they bound how blind the number can be, rather than pretending it is live.

Refresh balances

Re-reads live balances from the user's already-connected venues and returns the same envelope as GET /account/partners with a refreshed_partner_ids field added. This is a balance read — it touches no credentials and connects nothing.

curl -X POST https://api.openmarkets.ai/flow/v1/auth/account/partners/balance/update \
  -H 'X-API-Key: om_..._live_...' \
  -H 'X-OpenMarkets-Account: your-user-123' \
  -H 'Content-Type: application/json' \
  -d '{}'
This fans out to every connected venue, so it's slower than the read endpoint (typically 1–3 seconds). Don't poll it — call on user action or a long timer (e.g. 30s).

Real money vs. play money (currency buckets)

An account can hold two kinds of money, and they are never summed together: real money (USD, held across the connected venues above) and play money (ATLAS — practice "atlas coins" on the OpenMarkets internal book, auto-provisioned at signup). Real trading and paper trading (see Placing orders → Real vs. paper) draw down the matching bucket.

GET /flow/v1/auth/account/balances returns balances partitioned by currency — one bucket per currency, plus convenience totals. Use this (rather than /account/partners) when you need to show real and practice balances side by side without blending them.

curl https://api.openmarkets.ai/flow/v1/auth/account/balances \
  -H 'X-API-Key: om_..._live_...' \
  -H 'X-OpenMarkets-Account: your-user-123'
{
  "data": {
    "router_account_id": "ra_...",
    "by_currency": [
      {
        "currency": "USD",
        "is_real_money": true,
        "symbol": "$",
        "label": "US Dollars",
        "total": 1234.56,                 // sum of partners[].balance in this currency
        "expected_total": 1184.56,        // sum of partners[].expected_balance — spendable now
        "partners": [
          { "partner_id": "p_kalshi",     "partner_name": "Kalshi",     "balance": 892.10,
            "expected_balance": 842.10, "pending": -50.00, "is_stale": false, "internal_book": false },
          { "partner_id": "p_bettoredge", "partner_name": "BettorEdge", "balance": 342.46,
            "expected_balance": 342.46, "pending": 0, "is_stale": false, "internal_book": false }
        ]
      },
      {
        "currency": "ATLAS",
        "is_real_money": false,
        "symbol": "🪙",
        "label": "Atlas Coins",
        "total": 1000,
        "partners": [
          { "partner_id": "p_openmarkets", "partner_name": "OpenMarkets (Practice)", "balance": 1000,
            // the practice wallet is our own ledger: expected always equals balance, never stale
            "expected_balance": 1000, "pending": 0, "is_stale": false, "internal_book": true }
        ]
      }
    ],
    "real_money_total": 1234.56,          // convenience: total USD across connected venues
    "real_money_expected_total": 1184.56, // real money spendable now, net of holds
    "atlas_balance": 1000                 // convenience: the ATLAS practice wallet
  }
}
Never add a USD balance to an ATLAS balance. They are different currencies in separate buckets — a combined "total balance" across them is meaningless. Render (and reconcile) each currency on its own.

Not available through the API

By design, none of the following are operations your code performs — they all live on the hosted page (Connect for your users; the console's Connect page for your own account):

  • Submitting or storing exchange credentials.
  • Connecting, reconnecting, or disconnecting a venue.
  • Testing/validating credentials.

If you need any of these, send the user through the hosted Connect flow. Users also set their own execution limits, pause venues, and disconnect there — you're read-only over all of it.