@huskly/ibkr-client
v0.20.0
Published
Interactive Brokers Web API client with OAuth 1.0a authentication
Readme
IBKR Client (@huskly/ibkr-client)
A reusable TypeScript client for the Interactive Brokers Web API, authenticating over OAuth 1.0a without the Client Portal Gateway. This package contains no command-line interface; terminal commands and presentation belong exclusively in huskly/cli.
The OAuth 1.0a live-session-token handshake is performed by the
ibkr-client package. See the
ibind OAuth 1.0a wiki for how
the keys below are generated and registered in the IBKR self-service portal.
Layout
The repository contains only the reusable client library:
| Path | Purpose |
| -------------------------------- | --------------------------------------------------------------------------- |
| src/types.ts | Broker-neutral BrokerClient interface and normalized domain types |
| src/ibkr/ibkrClient.ts | IbkrClient — typed wrapper over ibkr-client implementing BrokerClient |
| src/ibkr/oauthConfig.ts | Builds the OAuth config from .pem files + env vars |
| src/ibkr/dhPrime.ts | Extracts the DH prime (hex) from dhparam.pem |
| src/ibkr/optionContract.ts | Canonical OSI parsing and formatting for IBKR option contracts |
| src/ibkr/derivativeContract.ts | Normalizes OPT/FOP identity and market-data availability |
The *.pem files (private_signature.pem, private_encryption.pem,
dhparam.pem, plus the public keys) are the cryptographic material from the
wiki setup step and are git-ignored.
Setup
yarn install --frozen-lockfileProvide the account-specific secrets, either by copying the template:
cp .env.example .env
# then edit .envor by exporting them in your shell:
export IBIND_OAUTH1A_CONSUMER_KEY=... # 9-char consumer key from IBKR
export IBIND_OAUTH1A_ACCESS_TOKEN=... # access token from the portal
export IBIND_OAUTH1A_ACCESS_TOKEN_SECRET=... # access token secret from the portalOptional environment variables:
IBIND_OAUTH1A_REALM— OAuth realm (defaults tolimited_poafor the individual self-service flow).IBKR_KEYS_DIR— directory holding the.pemfiles (defaults to the current working directory).IBKR_ACCOUNT_ID— target a specific account (otherwise the first is used).IBKR_TRANSACTION_CURRENCY— transaction-query currency (defaults toUSD).
Library API
IbkrClient owns IBKR authentication, requests, raw response types, and
normalization. Consumers such as huskly-cli provide presentation, command
routing, and caching. Public request types are the caller contract: this package assumes consumers
use TypeScript and pass values accepted by those types. Provider responses remain untrusted and are
validated at runtime. Its broker-neutral account API includes:
getAccountBalances()andgetPositions()for account state. Account balances include typedmargin.total,margin.securities, andmargin.commoditiessnapshots with IBKR's available funds, buying power, excess liquidity, cushion, SMA, equity-with-loan, Reg-T, initial- and maintenance-margin, full, look-ahead, and leverage values. Margin values arenullwhen IBKR omits or returns an invalid value; numeric zero remains0.getQuotes()andsearchInstruments()for equity/ETF discovery and quotes.fetchTransactionHistory()for normalized portfolio transactions.fetchOrders()for normalized live orders, including aggregateWORKINGmatching across IBKR's active order states.
Strategy market data
The reusable IbkrClient also exposes typed, read-only strategy data:
getPriceHistory(...)returns normalized OHLCV bars.getOptionExpiries(...)discovers weekly and monthly maturities across month buckets.getOptionChain(...)returns an exact-expiry chain with canonical OSI symbols, conids, bid/ask/mid prices, delta, session volume, and open interest.getOptionQuote(...)resolves and prices one exact contract with the same market-data shape.getOptionContract(conid)maps a broker conid back to durable OSI identity.
Broker-neutral derivative discovery
IbkrClient implements the capability-specific DerivativeDiscoveryClient without adding
derivative operations to the smaller account-oriented BrokerClient:
getDerivativeExpiries(...)lists exact series identity over a calendar range.getDerivativeContracts(...)discovers all matching contracts for one expiration.resolveDerivativeContract(...)returns exactly one contract and rejects ambiguous trading classes.getDerivativeChain(...)prices one exact expiration and fails when no usable bid/ask exists.getDerivativeReferenceQuote(...)follows IBKR'sundConidto quote the actual linked underlying contract, such as the September NQ future behind an August QN3 option.
Both OPT and FOP use the stateful secdef/search -> secdef/strikes -> secdef/info
sequence. FOP discovery derives a unique exchange such as CME from the search result when the
caller does not provide one. Index-option callers can select a venue explicitly, such as SMART.
const nq = await client.resolveDerivativeContract({
assetClass: "FOP",
underlying: "NQ",
expiration: "2026-08-21",
strike: 26600,
right: "P",
tradingClass: "QN3",
});
// nq.multiplier === 20; nq.exchange === "CME"
const ndxp = await client.resolveDerivativeContract({
assetClass: "OPT",
underlying: "NDX",
expiration: "2026-08-20",
strike: 26600,
right: "P",
tradingClass: "NDXP",
exchange: "SMART",
});Semantic identity consists of asset class, underlying, expiration, strike, right, trading class,
exchange, and multiplier. conid is returned only because this package is the IBKR boundary; it
is broker-local, can change, and must not be persisted as durable strategy identity. Optional
settlement and exercise-style fields are preserved when IBKR supplies them.
Derivative quotes use nullable values for missing prices and Greeks and normalize field 6509
to live, delayed, frozen, frozen-delayed, or unavailable. A missing subscription is
never reported as live data. All discovery APIs are read-only and do not call preview, order,
warning-reply, or cancellation endpoints.
Contract discovery always calls secdef/search before secdef/strikes, because IBKR keeps
that priming state in the authenticated session. Empty post-prime strikes and incomplete
bid/ask/delta snapshots throw instead of looking like a valid chain with no candidates.
Request shaping is resilient by design: option discovery normalizes the requested symbol,
applies bounded batching for secondary-definition and market-data calls, and retries read-only
requests on transient 429 responses with capped exponential backoff (including Retry-After
headers when available). If every returned contract is unusable (missing bid/ask/delta), the
client now fails noisily so callers can handle that condition explicitly.
Option volume and open interest are required nullable fields: numeric zero remains zero, while
missing, unsupported, or non-finite provider values are returned as null.
Conids are broker-boundary identifiers; consumers should persist the returned OSI symbol.
Explicit derivative What-If previews
IbkrClient also implements DerivativePreviewClient, a deliberately narrow capability with
no placement, reply-confirmation, modification, or cancellation method. Callers must provide an
exact account ID; the client never selects the first account for preview work.
getTradingDiagnostics(accountId)reports authentication, selected account, paper/live state, market-data access, and advisory asset permissions. It does not switch accounts or preview an order.previewDerivativeCombo(...)accepts two exact contracts with signed ratios and a positive user-facing credit/debit. It requests the required market-data snapshot, constructs one atomicconidex, and calls only/orders/whatif.
For a BUY-oriented combo, IBKR encodes a net credit as a negative limit price. That provider
detail remains inside this package: callers send { priceEffect: "CREDIT", limit: 39 }. The
normalized result includes paper/live environment, commission, initial and maintenance margin,
warnings, rejection reasons, and submitted: false. An incomplete nominal success fails closed.
Permission metadata is diagnostic only; the What-If response remains authoritative.
Guarded derivative order execution
IbkrClient implements a separate DerivativeExecutionClient capability for callers that have
already enforced their own reviewed-preview workflow. It does not persist previews or decide
whether live execution is allowed.
Submission rejection is authoritative only when IBKR returns its documented top-level error object
with a meaningful message, code, or failure status. Array-contained or non-diagnostic error fields
return recovery_required, because they do not prove that every submitted ticket failed.
submitDerivativeSingleOrder(...)places one single-leg LIMIT or STOP option order with exact contract, side, quantity, TIF, and session. Standalone orders require a unique client order ID; sequentially attached child orders instead require the parent's exact ID and omit their owncOID, as required by IBKR. LIMIT and STOP requests are discriminated: LIMIT orders require only a positivelimit, while STOP orders require only a positivestopPrice. Equity-option (OPT) orders omit CME-only fields; futures-option (FOP) orders require the caller's exactextOperatorandmanualIndicator. Mixed, multiple, unknown, or malformed response evidence returnsrecovery_required; pending-cancel acknowledgements also require recovery. Callers must reconcile the retained broker order IDs before another write.submitDerivativeCombo(...)places one atomic combo with the exact legs, signed ratios, quantity, price effect, limit, TIF, and session supplied by the caller. The request requires a unique client order ID. Futures-option (FOP) writes also require the caller's exact CMEextOperatorand manual/automated-originmanualIndicator; equity-option (OPT) writes omit both CME-only fields. As with single orders, ambiguous response evidence returnsrecovery_requiredand must block blind resubmission.submitDerivativeContingentOrders(...)places a general parent/child LIMIT-or-STOP pair in one bracket request. The parent carries the caller's unique client order ID; the client derives the child'sparentIdfrom it and deliberately omits a childcOID. Parent and child must target the same account, but may reference the same or different contracts. IBKR does not promise an all-or-none response:acceptedtherefore requires exactly two non-failure acknowledgements, while mixed, incomplete, pending-cancel, canceled, rejected, unknown, or malformed evidence is returned asrecovery_requiredwith every observed broker order ID retained.acknowledgeOrderWarning(...)replies once to an exact broker warning ID. Warnings with a non-empty array of string message IDs are markedknown; callers must still approve exact IDs, and malformed or missing IDs remain unknown.- A warning from
submitDerivativeContingentOrders(...)includes a typedcontinuationcontaining the exact reply ID and parent client ID. Pass that object unchanged toacknowledgeContingentOrderWarning(...); its result retains both broker acknowledgements or returns all partial evidence asrecovery_required. Do not route contingent warnings through the single-order acknowledgement method. getDerivativeOrderStatus(...)uses IBKR's exact order-ID status endpoint so fast terminal orders remain visible after live-list eviction. It normalizes pending, working, partial-fill, fill, canceled, and rejected lifecycle states with leg ratios and order economics, and fails closed on identity mismatch, unknown status, or missing aggregate quantities.findDerivativeOrder(...)accepts exactly one broker order ID or caller-supplied customer order ID (cOID/order_ref). Broker IDs go directly to the exact endpoint; a customer ID is resolved through the live list and then read by exact broker ID.listActiveDerivativeOrders(accountId)is the pre-placement risk view for one exact account. It preserves every signed USDconidexmember, including exchange-qualified spreads, and applies the outer order side to each ratio (or preserves a single conid/side). Caller and broker IDs, including echoed clientparentIdownership, remain distinct alongside graph role, quantities, lifecycle, pricing, TIF, session, timestamps, and unambiguous single-leg OSI option identity. Combo identities are not inferred from description order without a conid correlation. Malformed legs, aggregate-only rows, unknown statuses or directions, missing or ambiguous parents, and duplicate graph members are returned with explicituncertaintyrather than silently discarded. An account mismatch or an incomplete IBKR snapshot rejects the entire read, preventing a partial collection from being used as the pre-placement risk view. This active collection is not terminal history: after an order leaves it, usegetDerivativeOrderStatus(...)with its broker ID as the authoritative exact lookup.getDerivativeExecutions(...)reads up to seven calendar days from IBKR's trade history and returns individual leg fills with execution ID, conid, side, quantity, price, commission, commission currency, net amount, venue, and execution time. Customer order IDs allow fills to be reconciled back to the atomic combo. Missing provider values remainnullrather than estimated.reconcileDerivativeComboExecution(...)polls delayed trade publication to a bounded deadline, correlates by the unique client order reference, rejects duplicate or mismatched evidence, and validates each expected leg's side and ratio-derived quantity. Its sanitized result separates gross option points, multiplier-adjusted gross dollars, commission, and net dollars without exposing account or execution IDs.cancelDerivativeOrder(...)sends one exact cancellation request and returns only a typedrequestedacknowledgement. Its requiredassetClasslets the client apply the same product-aware CME metadata rule without guessing from an order ID. Callers remain responsible for reading until a terminal state and verifying that cancellation reachedCANCELED.
Placement, warning replies, and cancellation deliberately use single-attempt HTTP writes so a
transport retry cannot duplicate a broker action. A BUY-oriented net credit remains negative at
the IBKR boundary, matching the What-If ticket exactly. Every write requires the exact account ID;
the library never falls back to the first account. Broker-declared order rejections retain their
structured response in BrokerErrorDetail.details; transport exceptions are rethrown intact.
Request pacing and temporary blocks
Every authenticated request runs through one priority scheduler per IbkrClient. The default
limits allow at most ten requests globally and one exploratory security-definition request at a
time. Order preview, status, warning, cancellation, and immediate-trade requests take priority
over queued discovery. Multi-month derivative discovery also primes each month serially, while an
exact expiry/strike/right request expands only that requested contract.
A 429 pauses the shared queue behind one Retry-After-aware exponential backoff with jitter;
individual queued reads do not start independent retry loops. Exhausted throttling throws
IbkrRequestSchedulerError with code IBKR_THROTTLED. A broker temporary-block response opens a
bounded circuit, rejects queued work, and throws code IBKR_TEMPORARILY_BLOCKED without retrying.
IbkrClient accepts optional scheduler limits and an onRequestTelemetry callback. Telemetry
contains only a sanitized endpoint category, event, attempt, and delay—never account IDs, order
IDs, credentials, or request payloads.
Authorized read-only smoke test
Run this only after the account owner authorizes a read-only brokerage request and the OAuth environment from Setup is present. Supply an explicit calendar window; the client does not source strategy time from IBKR. This calls account, security-definition, history, and market-data endpoints only—never preview, placement, reply-confirmation, or cancellation.
IBKR_SMOKE_SYMBOL=MSTR \
IBKR_SMOKE_FROM=2026-08-01 \
IBKR_SMOKE_TO=2026-08-31 \
node --input-type=module <<'NODE'
import { IbkrClient, buildOauthConfig } from "./dist/index.js";
const client = new IbkrClient(buildOauthConfig());
await client.init();
const symbol = process.env.IBKR_SMOKE_SYMBOL;
const from = process.env.IBKR_SMOKE_FROM;
const to = process.env.IBKR_SMOKE_TO;
if (!symbol || !from || !to) throw new Error("Smoke symbol/from/to are required");
const [balances, history, expiries] = await Promise.all([
client.getAccountBalances(),
client.getPriceHistory({ symbol, days: 5 }),
client.getOptionExpiries(symbol, "C", from, to),
]);
const expiry = expiries[0];
if (!expiry) throw new Error(`No listed expiries for ${symbol} in ${from}..${to}`);
const chain = await client.getOptionChain(symbol, expiry);
console.log({ equityRead: Number.isFinite(balances.netLiquidation), historyBars: history.length,
expiry, contracts: chain.length, first: chain[0]?.symbol });
NODEDevelopment
yarn lint # eslint
yarn format # prettier --write
yarn typecheck # tsc --noEmit
yarn test # typecheck + native node:test suite
yarn build # tsc -> dist/
yarn run check # lint + format:check + typecheckCI (.github/workflows/ci.yml) runs lint, format check, typecheck, tests, and build on
every push and pull request, plus gitleaks
to guard against committed secrets.
Security notes
- The private keys and
.envare git-ignored — do not commit them. - Credentials are read from the environment, never hardcoded.
- CI scans every change with gitleaks to catch accidentally committed secrets.
Resumable derivative order graphs
submitDerivativeOrderGraph(...) is the bounded (one through eight member) bracket API for
broker-hosted derivative protection. A graph may combine atomic two-leg LIMIT combos with
single-option LIMIT, STOP, and MARKET members. Every node has a caller-stable memberId; exactly
one root carries rootClientOrderId. Each later node uses parentMemberId to name a member that
comes before it. IBKR receives a deterministic cOID for every node. A non-root node also receives
the deterministic parentId of its exact parent. This means that a graph can contain children,
grandchildren, and deeper descendants.
Accepted and fail-closed results retain each node's immutable request evidence (contracts/conids,
signed ratios, side, quantity, TIF, session, and prices), stable depth role, parent member/broker
IDs, and every broker order ID. Conids remain correlation evidence only; callers still own durable
semantic option identity.
Warnings are never acknowledged automatically. A warning result contains a JSON-safe
continuation with the exact reply ID, full graph request, and all correlation accumulated so far;
persist it before calling acknowledgeDerivativeOrderGraphWarning(...). Each placement or reply is
attempted once after revalidating the account's authentication and competing-session safety;
chained and unknown warnings remain pending for the caller, and mixed, partial,
duplicated, terminal, malformed, or ambiguous acknowledgements return recovery_required. Broker
IDs from ambiguous acknowledgements are retained as uncorrelated responses rather than assigned to
nodes by position. A failed synchronous response retains a readable text or warning_message in
errors and recovery reasons when IBKR supplies one.
recoverDerivativeOrderGraph(...) reconstructs the exact graph from its root client ID, any
known member broker ID, and terminal evidence keyed by the durable root client ID. It searches
filtered filled, canceled, and inactive order snapshots before using recent execution evidence;
an exact broker ID is still queried even if execution history is unavailable.
It requires complete active and filtered terminal snapshot markers, traverses both nested child
collection aliases, and rejects contradictory aliases within a broker response before correlation.
It correlates the root by the transmitted client ID and its complete ticket. It correlates each
descendant by the deterministic client order ID of its exact parent, plus the contract or complete
combo legs, order type, side, quantity, applicable signed limit or stop price, TIF, and
regular/overnight session. Recovery accepts terminal members when the evidence is
non-ambiguous and complete, preserves broker terminal states for each member, and fails closed when
evidence is partial, duplicated, ambiguous, unknown, includes an unexpected attached order, or cannot
prove required account, broker ID, or parent identity links.
No recoveries involve writes.
Failed terminal snapshot lookups force recovery_required; trade-linked members whose exact status
lookup fails remain preserved as uncorrelated evidence instead of being discarded.
This recovery API is the safety boundary: consumers should not bypass it with the private raw request
client.
