Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

HTTP sidecar (bezant-server)

A thin axum binary that exposes the CPAPI as plain REST+JSON. Most of its handlers are deliberately pass-through — they forward the Gateway’s JSON body verbatim — so any language can consume CPAPI without linking Rust.

Endpoints

REST passthrough

MethodPathUpstream
GET/healthPOST /iserver/auth/status (projected)
GET/accountsGET /portfolio/accounts
GET/accounts/:id/summaryGET /portfolio/{id}/summary
GET/accounts/:id/positions?page=NGET /portfolio/{id}/positions/{N}
GET/accounts/:id/ledgerGET /portfolio/{id}/ledger
GET / POST/accounts/:id/ordersGET / POST /iserver/account/{id}/orders
DELETE/accounts/:id/orders/:order_idDELETE /iserver/account/{id}/order/{oid}
GET/contracts/search?symbol=XPOST /iserver/secdef/search
GET/market/snapshot?conids=A,B&fields=…GET /iserver/marketdata/snapshot?…
fallbackany other pathverbatim passthrough (drives /sso/Login etc.)

Events capture (opt-in via BEZANT_EVENTS_ENABLED)

The server can optionally run an internal CPAPI WebSocket consumer that buffers order, PnL, and (lazily per-conid) market-data frames into per-topic ring buffers. Consumers poll cursor-paginated REST endpoints instead of opening their own WebSocket — events are captured server-side the moment they arrive, regardless of whether anyone is currently listening.

MethodPathReturns
GET/events/orders?since=N&limit=Norder lifecycle frames (CPAPI sor)
GET/events/pnl?since=N&limit=NPnL frames (CPAPI spl)
GET/events/marketdata?conid=N&since=N&limit=NL1 market data; lazy upstream subscribe per conid
GET/events/gap?since=N&limit=Nsynthetic gap markers (WS reconnect, process restart)
GET/events/_statusconnector liveness + per-topic buffer sizes
GET/events/{topic}/history?since_ts=…&limit=Nsqlite history (when BEZANT_EVENTS_DB_PATH is set)

Wire semantics:

  • 200{events, next_cursor, reset_epoch}. Use next_cursor as the next since=.
  • 204 — caller is caught up; cursor stays put.
  • 412{head_cursor, reset_epoch, code: "cursor_expired"}. The caller’s cursor is older than the ring buffer’s head; reset to head_cursor - 1 and emit a synthetic gap on the consumer side.
  • 503{code: "events_disabled"} when capture is off.

reset_epoch bumps on every WS reconnect or process restart. Any change in epoch is the consumer’s signal that “you missed something” — the connector also injects a synthetic event into every active topic ring so a polling consumer sees the gap on its next read.

Error envelope

Non-success responses come back as:

{ "code": "not_authenticated", "message": "gateway is not authenticated …" }

Status codes map:

VariantHTTP
not_authenticated401
no_session503
upstream_http_error502
upstream_api_error502
invalid_base_url400
internal500

Configuration

Env-first, clap-exposed. See bezant-server --help.

VariableDefault
IBKR_GATEWAY_URLhttps://localhost:5000/v1/api
BEZANT_BIND0.0.0.0:8080
BEZANT_KEEPALIVE_SECS60
BEZANT_VERIFY_TLSfalse (accepts the Gateway’s self-signed cert)
BEZANT_DEBUG_TOKENunset (/debug/* 404s without it)
BEZANT_EVENTS_ENABLEDfalse
BEZANT_EVENTS_DB_PATHunset (sqlite history disabled)
BEZANT_EVENTS_ORDERS_CAP1000
BEZANT_EVENTS_PNL_CAP5000
BEZANT_EVENTS_MARKETDATA_CAP2000 per conid

Deployment shape

The Docker compose file in the repo root is the canonical shape:

┌────────────┐  stdin/stdout   ┌──────────────┐  HTTPS + cookie   ┌──────┐
│ your app   │ ──────────────► │ bezant-server│ ────────────────► │ IBKR │
│ (any lang) │ ◄────────────── │              │ ◄──────────────── │  GW  │
└────────────┘    HTTP/JSON    └──────────────┘                   └──────┘

Tip: keep the sidecar on 127.0.0.1 in production. It holds a live IBKR session cookie — anyone who reaches its port can make trades.