REST API

Every endpoint lives under /v1 on the address nginx publishes, http://localhost:8889/v1 by default. Requests and responses are JSON. The operations come from the OpenAPI spec in api/v1.yaml, and the running app serves its own schema at /openapi.json with Swagger UI at /docs (both outside /v1, both behind the token).

export IBKR=http://localhost:8889/v1
export API_TOKEN=<your api_token>

curl -s -H "Authorization: Bearer $API_TOKEN" "$IBKR/ping"

The examples below leave out the Authorization header to stay short. Send it on every request when api_token is set.

Authentication

When api_token (or API_TOKEN) is set, every request needs Authorization: Bearer <token>. That includes /v1/ping, /openapi.json, /docs and the MCP endpoint. A missing or wrong token gets 401 with code UNAUTHORIZED. With an empty token there is no auth at all. See Security.

Conventions

Routes by asset class

Prefix Asset class IBKR secType Path parameter
/stocks/{symbol} Equities STK symbol, 1 to 32 characters
/options/{symbol} Options on the underlying symbol OPT underlying symbol
/futures/{symbol} Futures FUT root symbol
/cfd/{symbol} CFDs CFD symbol
/forex/{pair} Currency pairs CASH pair such as EURUSD, 6 to 12 characters
/crypto/{symbol} Crypto CRYPTO symbol such as BTC

Each class has the same five routes, except that historical ticks exist only for stocks:

Method and path Returns
GET /<class>/{symbol} Contract details.
GET /<class>/{symbol}/tick A snapshot quote, with Greeks for options.
GET /<class>/{symbol}/rates Historical OHLC bars.
GET /stocks/{symbol}/ticks Historical trade or quote ticks.
POST /<class>/{symbol}/rates/ta Bars plus technical indicators from wickworks.

Contract selection

These query parameters pick the contract on every route of a class. Leaving one out uses contract_defaults from the configuration.

Class Required Optional
stocks   exchange, currency, primaryExchange
options expiry (YYYYMMDD or YYYYMM), strike, right (C, P, CALL or PUT) exchange, currency, multiplier, tradingClass
futures expiry (YYYYMM or YYYYMMDD), exchange (CME, NYMEX, …) currency, multiplier, tradingClass
cfd   exchange, currency
forex   exchange
crypto   exchange, currency

An option right outside those four values is not caught by validation and ends in a 500.

Bar sizes and data types

barSize takes a short form or IBKR’s own string. Leaving it out means one hour.

Short form IBKR string
1s 5s 10s 15s 30s 1 secs 5 secs 10 secs 15 secs 30 secs
1m 2m 3m 5m 10m 15m 20m 30m 1 min 2 mins 3 mins 5 mins 10 mins 15 mins 20 mins 30 mins
1h 2h 3h 4h 8h 1 hour 2 hours 3 hours 4 hours 8 hours
1d 1w 1mo 1 day 1 week 1 month

Anything else gets 400 BAD_REQUEST with the accepted list in details.accepted.

whatToShow accepts TRADES, MIDPOINT, BID, ASK, BID_ASK, ADJUSTED_LAST, HISTORICAL_VOLATILITY and OPTION_IMPLIED_VOLATILITY, in any case. The default is TRADES, also for forex /rates; IDEALPRO has no trade data, so pass whatToShow=MIDPOINT for forex bars. Only forex /rates/ta defaults to MIDPOINT.

duration is IBKR’s duration string: an integer, a space and a unit (S, D, W, M, Y), such as 60 S, 30 D, 13 W, 6 M, 1 Y. Encode the space as + or %20 in a URL. endDateTime is empty for now or YYYYMMDD HH:MM:SS in UTC.

The refresh parameter

refresh=true is accepted on contract details, /rates, /ticks, /rates/ta, /options/{symbol}/chain, /futures/{symbol}/continuous, /futures/{symbol}/contracts and both /history routes. It only changes anything where a response can come from disk: contract details, the chain and the two futures lists skip their cached copy and fetch again. Bars, ticks and history always come from IBKR. See Pacing and history.

Errors

Every error has the same body:

{
  "code": "NOT_FOUND",
  "message": "Contract not found or not qualifyable",
  "details": {
    "secType": "STK",
    "symbol": "NOPE"
  }
}

details is optional. Switch on code, not on message.

Status code When
400 BAD_REQUEST Unsupported bar size or data type, an order missing a required field, an unknown assetClass, action or exercise action, a chain request for a non-stock underlying without underlyingConId.
401 UNAUTHORIZED Missing or wrong bearer token.
404 NOT_FOUND IBKR could not qualify the contract, returned no contract details or account summary, or the order is not open. Also an unknown path (with FastAPI’s own body).
422 VALIDATION_FAILED The request did not match the schema. details.errors lists each problem.
429 RATE_LIMIT_NEAR The pacing gate refused the call. See below.
500 INTERNAL_SERVER_ERROR An uncaught exception. The message names the exception class; the traceback is in the api log.
502 UPSTREAM_FAILED Wickworks answered with an error or was unreachable, or IBKR returned no ticker for a snapshot.
503 TA_SIDECAR_UNAVAILABLE wickworks.url is empty or not http(s).

When the gateway is disconnected, requests do not fail fast: they wait for the reconnect. See troubleshooting.

Rate limits

Every call that reaches IBKR passes a pacing gate first. When a limit is close, the API answers 429 before IBKR would have rejected the request:

{
  "code": "RATE_LIMIT_NEAR",
  "message": "Approaching IBKR historical pacing (55/60 per 600s). Backing off to protect API access.",
  "details": {
    "rule": "historical_60_per_600s",
    "used": 55,
    "limit": 60,
    "window_sec": 600,
    "retry_after_sec": 312,
    "tier": "historical"
  }
}

Wait retry_after_sec and retry. The per-contract variant has rule: <tier>_per_contract_per_sec, a contract key and retry_after_sec: 1. Limits and tiers are on the pacing page.

System and account

GET /ping

Gateway connection state.

curl -s "$IBKR/ping"
{
  "status": "ok",
  "connected": true,
  "serverVersion": 178,
  "startTime": "2026-06-19T00:00:00+00:00",
  "uptimeSeconds": 3600.2,
  "msgsSent": 512,
  "msgsRecv": 2048
}

/ping connects first if it has to, so it only answers once the gateway socket is up.

GET /accounts

{
  "accounts": [
    "DU1234567"
  ]
}

Paper accounts start with DU, live ones with U.

GET /account and GET /account/values

Both take an optional account query parameter. /account is IBKR’s account summary (accountSummaryAsync), /account/values is the longer account values list. A summary with no rows is a 404.

{
  "accounts": {
    "DU1234567": {
      "NetLiquidation": {
        "value": "1000000.00",
        "currency": "USD",
        "modelCode": ""
      },
      "BuyingPower": {
        "value": "4000000.00",
        "currency": "USD",
        "modelCode": ""
      }
    }
  }
}

Values are the strings IBKR sends.

GET /positions

Optional account filter.

{
  "positions": [
    {
      "account": "DU1234567",
      "contract": {
        "conid": 265598,
        "symbol": "AAPL",
        "secType": "STK",
        "exchange": "NASDAQ",
        "currency": "USD"
      },
      "position": 10.0,
      "avgCost": 180.25
    }
  ]
}

The contract object has more fields than shown here: primaryExchange, lastTradeDateOrContractMonth, strike, right, multiplier, tradingClass, localSymbol.

Market data

GET /{class}/{symbol}: contract details

Contract selection parameters as above, plus refresh. Served from the 7-day contract details cache when present.

curl -s "$IBKR/stocks/AAPL"
curl -s "$IBKR/options/AAPL?expiry=20260619&strike=200&right=C"
curl -s "$IBKR/futures/ES?expiry=202609&exchange=CME"
curl -s "$IBKR/forex/EURUSD"
{
  "contracts": [
    {
      "contract": {
        "conid": 265598,
        "symbol": "AAPL",
        "secType": "STK"
      },
      "marketName": "NMS",
      "minTick": 0.01,
      "orderTypes": [
        "LMT",
        "MKT"
      ],
      "validExchanges": [
        "SMART",
        "NASDAQ"
      ],
      "longName": "APPLE INC",
      "timeZoneId": "US/Eastern",
      "tradingHours": "...",
      "liquidHours": "..."
    }
  ]
}

Other fields: priceMagnifier, industry, category, subcategory, evRule, evMultiplier.

GET /{class}/{symbol}/tick: snapshot quote

Contract selection parameters only. Each call also appends a row to the snapshot history.

curl -s "$IBKR/stocks/AAPL/tick"
curl -s "$IBKR/options/AAPL/tick?expiry=20260619&strike=200&right=C"
curl -s "$IBKR/futures/ES/tick?expiry=202609&exchange=CME"
curl -s "$IBKR/crypto/BTC/tick?currency=USD"

Fields, all nullable: time, bid, bidSize, ask, askSize, last, lastSize, close, open, high, low, volume, vwap, halted, plus contract. Options add modelGreeks, bidGreeks, askGreeks and lastGreeks, each with impliedVol, delta, gamma, theta, vega, optPrice and undPrice; a Greeks object is null when IBKR sent none. Real-time Greeks need an OPRA subscription and market_data_type: 1; with delayed data they are often null.

GET /{class}/{symbol}/rates: historical bars

Parameter Required Default Notes
duration yes   IBKR duration string.
barSize no 1h See bar sizes.
endDateTime no now YYYYMMDD HH:MM:SS UTC.
whatToShow no TRADES Use MIDPOINT for forex.
useRTH no false Regular trading hours only.
refresh no false No effect: bars are always fetched.

Plus the class’s contract selection parameters.

curl -s "$IBKR/stocks/AAPL/rates?duration=1+Y&barSize=1d&whatToShow=ADJUSTED_LAST"
curl -s "$IBKR/options/AAPL/rates?expiry=20260619&strike=200&right=C&duration=10+D&barSize=1h"
curl -s "$IBKR/futures/ES/rates?expiry=202609&exchange=CME&duration=30+D&barSize=1d"
curl -s "$IBKR/forex/EURUSD/rates?duration=30+D&barSize=1h&whatToShow=MIDPOINT"
{
  "symbol": "AAPL",
  "secType": "STK",
  "bars": [
    {
      "time": "2026-06-18T00:00:00",
      "open": 196.1,
      "high": 198.4,
      "low": 195.7,
      "close": 197.9,
      "volume": 51234567.0,
      "average": 197.2,
      "barCount": 312345
    }
  ]
}

Forex responses carry pair instead of symbol; options add expiry, strike and right; futures add expiry. Every response is also merged into the bars CSV on disk. That step currently breaks /rates and /rates/ta with the default config; see troubleshooting.

GET /stocks/{symbol}/ticks: historical ticks

Parameter Default Notes
startDateTime, endDateTime empty Window bounds, YYYYMMDD HH:MM:SS UTC. IBKR wants one of them.
numberOfTicks 1000 1 to 1000.
whatToShow TRADES TRADES, BID_ASK or MIDPOINT.
useRTH false  
exchange, currency, primaryExchange, refresh    
{
  "symbol": "AAPL",
  "secType": "STK",
  "ticks": [
    {
      "time": "2026-06-18T14:30:00.123000+00:00",
      "price": 197.5,
      "size": 100.0,
      "bid": null,
      "ask": null,
      "bidSize": null,
      "askSize": null
    }
  ]
}

Ticks are not stored on disk.

POST /{class}/{symbol}/rates/ta: bars with indicators

A POST that reads only. Contract selection parameters (and refresh) go in the query string; the bar window and indicator spec go in the body. The bars come through the same path as /rates, then the API POSTs them to wickworks.

Body field Required Default Notes
duration yes   IBKR duration string.
barSize no 1h  
endDateTime no now  
whatToShow no TRADES (MIDPOINT for forex)  
useRTH no false  
indicators no {} Wickworks indicator spec, passed through unchanged.
recentBars no   Passed to wickworks as recentBars.
curl -s -X POST "$IBKR/stocks/AAPL/rates/ta" \
  -H "Content-Type: application/json" \
  -d '{
    "duration": "200 D",
    "barSize": "1d",
    "whatToShow": "ADJUSTED_LAST",
    "indicators": {
      "rsi": true,
      "macd": true,
      "bbands": {
        "length": 20,
        "std": 2
      }
    }
  }'

The response is {"symbol", "secType", "bars", "ta", "asOf"}, where ta is the ta object wickworks returned. Indicator names and parameters are wickworks’ own; its indicator list is the reference. Fetch about twice the slowest indicator’s lookback so it has warm-up bars.

503 TA_SIDECAR_UNAVAILABLE means no wickworks URL is configured. 502 UPSTREAM_FAILED carries wickworks’ status and the first 500 bytes of its body in details.

Options

GET /options/{symbol}/chain

Expirations and strikes for each exchange and trading class.

Parameter Default Notes
underlyingSecType STK For any other type you must pass underlyingConId.
futFopExchange empty Exchange for options on futures.
underlyingConId   Skips qualifying the underlying.
refresh false Bypass the 1-day chain cache.
{
  "symbol": "AAPL",
  "underlyingConId": 265598,
  "chains": [
    {
      "exchange": "SMART",
      "tradingClass": "AAPL",
      "multiplier": "100",
      "expirations": [
        "20260619",
        "20260717"
      ],
      "strikes": [
        195.0,
        200.0,
        205.0
      ]
    }
  ]
}

Each call also appends one row per expiry and strike to the chain history on disk, so a large chain writes many rows.

POST /options/combo

Places a real multi-leg (BAG) order. Resolve each leg’s conid first with GET /options/{symbol}.

Field Required Default Notes
symbol yes   Underlying.
legs yes   Array of {conid, ratio, action, exchange}; ratio defaults to 1, exchange to SMART.
action yes   BUY or SELL for the whole combo.
quantity yes   Integer.
orderType no LMT LMT or MKT only.
lmtPrice for LMT    
tif no DAY  
exchange, currency no SMART, USD  
account no    
curl -s -X POST "$IBKR/options/combo" \
  -H "Content-Type: application/json" \
  -d '{
    "symbol": "AAPL",
    "legs": [
      {
        "conid": 681000001,
        "ratio": 1,
        "action": "BUY"
      },
      {
        "conid": 681000002,
        "ratio": 1,
        "action": "SELL"
      }
    ],
    "action": "BUY",
    "quantity": 1,
    "orderType": "LMT",
    "lmtPrice": 1.20
  }'

Returns a Trade.

POST /options/exercise

Exercises or lapses a real option position. The TWS socket API supports this; IBKR’s Client Portal REST API does not.

Field Required Default Notes
conid yes   The option contract.
action yes   EXERCISE or LAPSE.
quantity yes   At least 1.
account yes    
override no false Ask IBKR to override its own exercise decision.
curl -s -X POST "$IBKR/options/exercise" \
  -H "Content-Type: application/json" \
  -d '{
    "conid": 681000001,
    "action": "EXERCISE",
    "quantity": 1,
    "account": "DU1234567"
  }'

Returns {"status": "submitted", "contract": {...}, "action": "EXERCISE"}. A conid IBKR cannot qualify is a 404.

Futures

GET /futures/{symbol}/continuous

The continuous contract (CONTFUT). Requires exchange; takes currency (default USD) and refresh. Cached for the contract details TTL.

GET /futures/{symbol}/contracts

Every listed expiry. Requires exchange; takes currency, includeExpired (default false) and refresh. Cached for one day.

Both return the contract details shape, {"contracts": [...]}.

Orders

Every POST and DELETE here changes a real account.

GET /orders

Open trades as the gateway has reported them to this API connection: {"trades": [Trade, ...]}.

GET /orders/{orderId}

One open trade, or 404 if the order is not open.

POST /orders

Places one order on any asset class.

Field Required Default Notes
assetClass yes   stock/stk, option/opt, future/fut, cfd, forex/cash, crypto.
symbol yes   For forex, the pair.
action yes   BUY or SELL.
quantity yes   Greater than 0.
orderType no MKT See below.
lmtPrice for LMT, STP_LMT    
auxPrice for STP, STP_LMT   The stop trigger.
tif no DAY Time in force, such as GTC.
outsideRth no false  
transmit no true false stages the order in the gateway without sending it.
expiry, strike, right options   Futures need expiry and exchange.
exchange, currency, multiplier, tradingClass, primaryExchange no contract defaults  
conid no   When set and positive, the API builds the contract from it and ignores the other contract fields.
account no gateway default  
goodAfterTime, goodTillDate, ocaGroup, parentId no   Passed to the IBKR order.

Order types: MKT, LMT, STP and STP_LMT (also written STP LMT or STPLMT) are checked for their prices. Any other value is sent to IBKR as the raw orderType with whatever lmtPrice and auxPrice you gave.

curl -s -X POST "$IBKR/orders" \
  -H "Content-Type: application/json" \
  -d '{
    "assetClass": "stock",
    "symbol": "AAPL",
    "action": "BUY",
    "quantity": 10,
    "orderType": "LMT",
    "lmtPrice": 180.00
  }'

curl -s -X POST "$IBKR/orders" \
  -H "Content-Type: application/json" \
  -d '{
    "assetClass": "future",
    "symbol": "ES",
    "expiry": "202609",
    "exchange": "CME",
    "action": "BUY",
    "quantity": 1,
    "orderType": "LMT",
    "lmtPrice": 5500.00
  }'

The API qualifies the contract (a 404 if IBKR does not know it), places the order and returns the Trade right away. Read orderStatus.status: a rejection by IBKR shows up there, not as an HTTP error. There is no modify endpoint; cancel and place again.

DELETE /orders/{orderId}

Cancels one open order: {"status": "cancelling", "orderId": 12345}, or 404 if it is not open.

DELETE /orders

IBKR global cancel: every open order on the account, including ones placed elsewhere. Returns {"status": "global_cancel_requested"}.

Trade object

{
  "contract": {
    "conid": 265598,
    "symbol": "AAPL",
    "secType": "STK"
  },
  "order": {
    "orderId": 12,
    "permId": 123456789,
    "clientId": 1,
    "action": "BUY",
    "totalQuantity": 10.0,
    "orderType": "LMT",
    "lmtPrice": 180.0,
    "tif": "DAY",
    "transmit": true
  },
  "orderStatus": {
    "status": "Submitted",
    "filled": 0.0,
    "remaining": 10.0,
    "avgFillPrice": 0.0
  },
  "fills": [],
  "log": []
}

order also has auxPrice, outsideRth, account, parentId, ocaGroup, goodAfterTime and goodTillDate. orderStatus also has permId, lastFillPrice, whyHeld and mktCapPrice.

History

GET /history/executions

Executions IBKR reports for the current day. Optional filters: account, clientId, secType, symbol, exchange, side, timeAfter (YYYYMMDD HH:MM:SS UTC).

{
  "fills": [
    {
      "contract": {
        "conid": 265598,
        "symbol": "AAPL"
      },
      "execution": {
        "execId": "0000e0d5.65b2c1a1.01.01",
        "side": "BOT",
        "shares": 10.0,
        "price": 180.0
      },
      "commissionReport": {
        "commission": 1.0,
        "currency": "USD"
      },
      "time": "2026-06-18T14:30:01+00:00"
    }
  ]
}

New fills are appended to the daily executions file on disk, deduplicated by execId.

GET /history/completed_orders

Completed orders IBKR still holds. apiOnly=true limits the list to orders placed through the API. Returns {"trades": [{"contract", "order", "orderStatus"}, ...]} and appends new ones to the daily completed orders file.