Venues & balances

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

You never handle exchange credentials. There is no API to submit an API key, username/password, or private key to OpenMarkets. Venue connection — entering credentials and validating them — happens only in OpenMarkets' hosted UI, on our infrastructure. This keeps you out of scope for handling exchange secrets.

Connection happens in one of two hosted surfaces, depending on who the account belongs to:

  • Your users (Connect) — send each user through the hosted Connect flow. Your backend mints a one-time link; the user enters credentials on connect.openmarkets.ai, never through you. See the Connect integration guide.
  • Your own account (first-party) — connect venues yourself on the Venues screen at app.openmarkets.ai.

Minting a hosted Connect link (the only "connect" call your backend makes):

bash
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_statuspending (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.)

bash
curl https://api.openmarkets.ai/flow/v1/partners \
  -H 'X-API-Key: om_data_live_...'
json
{
  "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_allowedfalse ⇒ 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_contractsamount / 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_submittedfalse ⇒ 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.

bash
curl https://api.openmarkets.ai/flow/v1/auth/account/partners \
  -H 'X-API-Key: om_..._live_...' \
  -H 'X-OpenMarkets-Account: your-user-123'
json
{
  "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
      },
      {
        "account_partner_id": "ap_...",
        "partner_id":          "p_bettoredge",
        "partner_name":        "BettorEdge",
        "status":              "active",
        "credential_status":   "connected",
        "balance":             342.46
      }
    ]
  }
}

Balances here are cached. They refresh whenever the user connects/reconnects in the hosted flow, or when you call the refresh endpoint below.

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.

bash
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.

bash
curl https://api.openmarkets.ai/flow/v1/auth/account/balances \
  -H 'X-API-Key: om_..._live_...' \
  -H 'X-OpenMarkets-Account: your-user-123'
json
{
  "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
        "partners": [
          { "partner_id": "p_kalshi",     "partner_name": "Kalshi",     "balance": 892.10, "internal_book": false },
          { "partner_id": "p_bettoredge", "partner_name": "BettorEdge", "balance": 342.46, "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, "internal_book": true }
        ]
      }
    ],
    "real_money_total": 1234.56,          // convenience: total USD across connected venues
    "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 API operations — they all live in the hosted flow (Connect for your users, the Venues screen 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.