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
POST/events/_reconnect (debug-token-gated)drop the WS and reconnect now, skipping backoff (202)
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=. reset_epoch is the ring’s live epoch. A topic with no events yet answers 200 with no events and this process’s cursor — never the caller’s echoed back.
  • 204 — caller is caught up; cursor stays put. Headers x-bezant-cursor and x-bezant-reset-epoch carry the cursor and the live epoch.
  • 412 — {head_cursor, reset_epoch, code: "cursor_expired"}. The caller’s cursor is not in the ring: older than its head (evicted), or newer than anything it issued (a cursor from a previous process). Reset to head_cursor - 1 and emit a synthetic gap on the consumer side.
  • 503 — {code: "events_disabled"} when capture is off.

Cursors and reset_epoch are seeded from the boot time (epoch = Unix ms, first cursor = Unix ms × 1000, raised above the sqlite log’s high water when persistence is on), so a restart never rewinds them below a client’s; both stay below 2^53 for JavaScript clients. A cursor below the ring’s first one reads from the start. The epoch then moves exactly once per reconnect — on the connect that ends an outage, never on a failed attempt — and every ring reports the new value. Any change in epoch is the consumer’s signal that “you missed something”. The outage itself is recorded as one event on /events/gap only (reason, previous_reset_epoch, new_reset_epoch, disconnected_at, failed_attempts).

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_unreachable503 — the Gateway itself is down (connection refused)
gateway_upstream_failingthe Gateway’s 5xx — /health only: the Gateway is up but api.ibkr.com behind it is failing; a restart will not help
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_BIND127.0.0.1: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
BEZANT_EVENTS_QUIET_AFTER_ROUNDS3 (0 re-asks a silent subscription forever)

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.