Connect — integration guide

X-API-Key + X-OpenMarkets-Account · connect:read · connect:write · connect:trade
Live

For an organization: onboard your own users, have them link venue accounts on the hosted page, and read or trade for them. Execution itself is documented on Placing orders.

Before you start

Console → your organization

Three things, all self-serve in the console under your organization:

  • An organization key — your X-API-Key for every server call. It carries connect:read, connect:write and connect:trade on an organization plan. Shown once at mint.
  • A Connect flow — the venues offered, real money or practice-only, the permissions a user may grant, AI providers, branding, and your allowed return URLs.
  • A webhook endpoint per environment, each with its own secret, if you want to be told when a user connects.
X-API-Key:             om_exec_live_...     every call (identifies your organization)
X-OpenMarkets-Account: your-user-123        scopes the call to one of your users

Base URL https://api.openmarkets.ai. Identity is (your organization, external_user_id) — never an email. Each organization's users are fully isolated: a call naming a user you do not own is 403 account_forbidden, whether or not the user exists.

How it works

  1. 01
    Create a link session· your backend
    POST /flow/v1/connect/link-sessions

    Returns a one-time hosted_url and link_token for this user.

    ↓ redirect the user, or open the button

  2. 02
    The user links their venues→ OpenMarkets · hosted
    connect.openmarkets.ai

    Credentials are entered and validated here, never through you. They also set per-venue permissions and limits.

    ↓ user.partners_connected · signed webhook

  3. 03
    Verify and record· your backend
    X-OpenMarkets-Signature

    Check the HMAC, dedupe on delivery_id, mark the user connected.

    ↓ then, any time

  4. 04
    Read and act· your backend
    GET /flow/v1/auth/account/partners · POST /flow/v1/auth/orders/buy

    Balances, positions and history — or trade on their behalf, within what they granted.

2 · Webhooks

POST {your endpoint}   ·   X-OpenMarkets-Signature: t=1758500000,v1=<hex hmac>
{ "event": "user.partners_connected",
  "delivery_id": "evt_...",
  "external_user_id": "your-user-123",
  "om_account_id": "...",
  "connected_partners": ["kalshi"] }

Three events: user.partners_connected, user.permissions_changed, user.history_synced. Each delivery carries event, delivery_id, external_user_id, om_account_id and the event's own fields.

Verify the signature. The header is t=<unix seconds>,v1=<hex>; v1 is HMAC-SHA256 over `${t}.${rawBody}` with your endpoint's secret. Reject a t more than five minutes from now; retries are re-signed with a fresh timestamp. Compare in constant time. Then dedupe on delivery_id. Trust the webhook, not the redirect.

Verify in Node
import { createHmac, timingSafeEqual } from 'node:crypto'

function verify(rawBody: string, header: string, secret: string) {
  const { t, v1 } = Object.fromEntries(header.split(',').map((kv) => kv.split('=')))
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
  return expected.length === v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
}

You can register several endpoints (production, staging, an internal consumer), each on its own environment. Every endpoint receives every event for its environment, each signed with that endpoint's secret and retried on its own schedule — a staging box being down never delays production. All copies share one delivery_id. Endpoints, secrets, test events, the delivery log and redelivery are in the console under your organization's Connect settings; @openmarketsai/connect-node ships webhooks.verify and constructEvent.

3 · Read accounts

GET /flow/v1/auth/account/partners
X-API-Key: om_exec_live_...   ·   X-OpenMarkets-Account: your-user-123

Connected venues and balances; a venue the user did not grant balance:read for is omitted rather than erroring. The same header works on /auth/account/balances, /auth/account/positions, /auth/orders and the AI pass-through. Shapes are on Venues & balances.

4 · Place a trade

Your user must have granted trade:execute for that venue. They choose this on the hosted page; without it the leg is refused with 403 scope_not_granted naming the venue. Your organization's connect:trade entitlement lets you act for a user — it does not override what that user agreed to, nor their execution limits. Send them back through "mode": "manage" to change either.

POST /flow/v1/auth/orders/buy
X-API-Key: om_exec_live_...   ·   X-OpenMarkets-Account: your-user-123

{ "orders": [
  { "liquidity_hash": "<a venue's price from partner_liquidities[]>",
    "amount": 50,            // stake in USD — or "shares": 100
    "max_price": 0.42 }      // won't fill above this
]}

Name the venue and a price ceiling — pass a liquidity_hash from the liquidity feed (or a position_hash plus partner_id). Add "mode": "paper" for a practice fill. Strict — any other field is 400 unknown_parameter. A control-blocked leg (a venue the user paused, a stake over their cap) comes back 200 with venue_disabled or execution_limit_exceeded in results[i].rejected_code. Placing orders is the source of truth for multi-leg buys, resting orders, cancel and settlement.

Managing your users

connect:read · connect:write

Everything below is read-only over what the user set on the hosted page, except the sync trigger.

GET/flow/v1/connect/users

Your users, by external_user_id, with their om_account_id. A test key lists your test users; a live key, your real ones. GET /flow/v1/connect/users/:external_user_id returns one.

GET/flow/v1/connect/users/:external_user_id/policy

The user's execution policy as they set it: limits (an array of { type, params } rules such as a per-order cap or a venue allowlist) and venues (each with status and credential_status). 404 user_not_found if not provisioned. There is no write route — only the user changes this.

Response.data
{
  "external_user_id": "your-user-123",
  "om_account_id": "...",
  "limits": [ { "type": "max_stake_per_order", "params": { "amount": 250 } } ],
  "venues": [ { "partner_id": "p_kalshi", "status": "active", "credential_status": "connected" } ]
}
GET/flow/v1/connect/users/:external_user_id/history

The user's synced venue history, newest first — only from connections where they granted history:read. Query partner_id, limit (default 100, max 500) and offset. Each record carries the venue's own reference, kind, side, instrument_label, quantity, price, status, cost, payout, outcome, position_key, settled_pnl, occurred_at, and placed_via_openmarkets so you can tell your orders from theirs.

POST/flow/v1/connect/users/:external_user_id/history/sync

Ask for a fresh sync from every venue that supports history. Needs connect:write. Returns { external_user_id, om_account_id, dispatched_partners }; the user.history_synced webhook fires when it lands. Syncs also run on a schedule and whenever the user opens the hosted page.

GET/flow/v1/connect/webhook-config

The organization's legacy single webhook: return_urls, webhook_url and whether a secret is set. POST (connect:write) updates any of them and mints a secret when rotate_secret is true or none exists — the plaintext is returned once. Per-environment endpoints with their own secrets, the delivery log and redelivery live in the console; prefer those for anything new.

Response.data
{ "return_urls": ["https://app.yourco.com/openmarkets/done"],
  "webhook_url": "https://app.yourco.com/hooks/openmarkets",
  "webhook_secret_set": true }

Environments

With a test keyWith a live key
a sandbox session offering one credential-free venue, OpenMarkets Practicethe venues your flow allows
your test users, a separate set under the same idsyour real users
test webhook endpoints and their secretslive endpoints and theirs

Nothing migrates at launch: mint a live key and change the credential. See Test mode.

Users set their own limits, pause venues and disconnect on the hosted page — you are read-only there. Prefer typed calls? @openmarketsai/connect-node wraps every route on this page; @openmarketsai/connect-web is the drop-in launcher.

Next