Qualify a contract
Every market-data request and order identifies an instrument with a Contract. Treat a partially specified contract as a search key, not as an instrument identity. Qualification succeeds only when the search resolves to one contract and the application accepts its returned identity and routing fields.
Complete connection and readiness and account scope first. The examples below use a paper port, a read-only API connection, and no market-data or order operation.
Minimal qualification
This runnable script supplies a primary exchange to distinguish a stock from same-symbol listings, then requires exactly one qualified result.
import asyncio
from ib_async import Contract, IB, Stock
async def main() -> None:
ib = IB()
try:
await ib.connectAsync(
host="127.0.0.1",
port=4002,
clientId=9,
timeout=10,
readonly=True,
raiseSyncErrors=True,
)
search = Stock(
"AAPL", "SMART", "USD", primaryExchange="NASDAQ"
)
results = await ib.qualifyContractsAsync(search)
if len(results) != 1 or not isinstance(results[0], Contract):
raise LookupError("contract did not resolve uniquely")
qualified = results[0]
if not qualified.conId:
raise LookupError("qualified contract has no contract ID")
finally:
ib.disconnect()
asyncio.run(main())
Pinned ib_async 2.1.0 mutates a successfully qualified input object in place. In the example, qualified is search. Do not share a mutable search object across concurrent workflows; create it, qualify it, then pass the resolved object to one owner.
Search key versus resolved identity
The official contract request accepts either a contract ID plus exchange or descriptive fields such as symbol, security type, exchange, primary exchange, and currency. Derivatives require their instrument-specific fields. More fields narrow a search, but only the completed response proves how many contracts matched.
| State | Meaning | Safe use |
|---|---|---|
Partial Contract | Search criteria that may match zero, one, or many instruments. | Submit to contract-details lookup or qualification. Do not persist it as a confirmed instrument. |
One ContractDetails result | A uniquely resolved match for this request. | Validate the returned contract, identity, route, and product metadata. |
| Zero results | No known contract matched the supplied fields. | Fail closed; correct permissions or search fields. |
| Multiple results | The search is ambiguous. | Inspect candidates, add explicit discriminators, and request again. Do not select the first result. |
Common discriminators include primaryExchange for duplicate stock symbols and exact expiry, multiplier, trading class, exchange, strike, and right for derivatives. The contract API reference lists the available contract classes and fields.
Details lookup and qualification semantics
IB.reqContractDetailsAsync exposes the zero/one/many result directly. At protocol level, every matching details record is associated with a request ID and contractDetailsEnd marks that request's completion. Concurrent lookups may interleave, so a result is complete only when its own future resolves; a callback observed before that boundary is not a finished candidate set.
IB.qualifyContractsAsync performs one details lookup per input and preserves input positions in its result:
| Details result for one input | Default result slot | Pinned 2.1.0 behavior |
|---|---|---|
| No match | None | Logs the unknown contract; input remains unqualified. |
| One match | The original input object | Copies resolved fields into that object. If SMART was requested, it preserves exchange="SMART". |
| Multiple matches | None | Filters candidates to the requested secType; if exactly one remains, it qualifies that match. Otherwise it logs ambiguity. |
Multiple matches with returnAll=True | Candidate list | Diagnostic escape hatch after the same security-type filter; the application must still refine and retry. |
The blocking IB.qualifyContracts delegates to this async implementation in pinned 2.1.0, so its runtime result retains the same positional None or diagnostic-list slots even though its return annotation and docstring describe a list of successful contracts. Prefer the async form inside an asyncio application and narrow every result slot before use.
Production-pattern unique-match gate
When the application needs details metadata as well as the contract, request the candidates explicitly and reject every cardinality except one. This helper is deliberately separate from transport code so its fail-closed behavior is tested offline.
import asyncio
from ib_async import Contract, ContractDetails, IB, Stock
def require_unique_contract(details: list[ContractDetails]) -> Contract:
if len(details) != 1:
raise ValueError(f"expected one contract match, got {len(details)}")
contract = details[0].contract
if contract is None:
raise ValueError("contract details is missing a contract")
if not contract.conId:
raise ValueError("resolved contract is missing a contract ID")
return contract
async def main() -> None:
ib = IB()
try:
await ib.connectAsync(
host="127.0.0.1",
port=4002,
clientId=9,
timeout=10,
readonly=True,
raiseSyncErrors=True,
)
search = Stock(
"AAPL", "SMART", "USD", primaryExchange="NASDAQ"
)
details = await ib.reqContractDetailsAsync(search)
contract = require_unique_contract(details)
# Copy plain identity values; do not persist the mutable details object.
identity = {
"conId": contract.conId,
"secType": contract.secType,
"exchange": contract.exchange,
"primaryExchange": contract.primaryExchange,
"currency": contract.currency,
"localSymbol": contract.localSymbol,
"tradingClass": contract.tradingClass,
}
if not details[0].timeZoneId:
raise LookupError("contract schedule timezone is unavailable")
_ = identity
finally:
ib.disconnect()
if __name__ == "__main__":
asyncio.run(main())
The returned ContractDetails also carries minimum tick, valid exchanges, market-rule IDs, supported order types, timezone, trading hours, liquid hours, and product metadata. Treat those fields as a point-in-time response. Interpret schedules with timeZoneId, and refresh routing, increments, permissions, and session metadata rather than freezing them into a permanent instrument record.
Security-type discriminators
| Type | Fields commonly needed to make the search specific |
|---|---|
STK | symbol, secType, exchange, currency, and often primaryExchange. |
OPT | Underlying symbol, exact expiry, strike, right, multiplier, exchange, and currency. Enumerate a chain with reqSecDefOptParamsAsync, then qualify intended candidates. |
FUT / FOP | Exact expiry, exchange, currency, multiplier, trading class, plus strike and right for a futures option. |
CASH | Base symbol, quote currency, and exchange; Forex("EURUSD") constructs these fields. |
BAG | Combo shell plus legs; every leg needs a resolved conId, ratio, action, and exchange. Qualify legs before constructing the combo. |
The fields above are search guidance, not a guarantee of uniqueness. Product definitions and permissions vary; the zero/one/many result remains the gate.
Signals and failure handling
| Signal or condition | Interpretation | Required response |
|---|---|---|
| One details record followed by request completion | The search resolved uniquely for this request. | Validate returned identity and metadata before publishing contract readiness. |
| Empty result or error 200 reporting no security definition | No contract matched, or the session cannot resolve it. | Fail closed; check fields, exchange, product availability, and permissions. |
| Multiple results or error 200 reporting ambiguity | The search does not identify one instrument. | Add explicit discriminators and retry; never take the first candidate. |
returnAll=True candidate list | Diagnostic candidates from the pinned helper. | Present or log only non-sensitive identifiers, refine the search, and requalify. |
| Timeout, disconnect, or cancelled await before completion | Candidate collection may be incomplete. | Discard the partial attempt, end the connection epoch, and repeat after readiness is restored. |
| Reconnect after prior qualification | The old identity may still correlate with the instrument, but session metadata and permissions are not current. | Revalidate before starting dependent data or order workflows. |
Contract-details lookup is a finite request; its completion callback, not a cancellation pair, closes normal collection. On timeout or connection loss, discard any partially observed records. Never promote partial callback state into a qualified identity.
Persistence boundary
Persist only the plain identifiers the application needs for correlation, together with enough descriptive fields to detect a mismatch. Do not serialize a mutable Contract or ContractDetails instance as the authoritative product definition. On reconnect or before a sensitive dependent workflow, resolve the stored identity again and compare the returned security type, currency, route, local symbol, trading class, and product-specific fields.
Qualification proves unique resolution for one request. It does not prove market-data entitlement, trading permission, account authorization, current session hours, or that an order is suitable. Those gates belong to later workflow steps.
Sources and applicability
- IBKR Campus: contract object —
official-current; retrieved2026-07-14T19:13:54.602937Z; supports contract search identity and derivative-specific fields. - IBKR Campus: request contract details, receive contract details, and contract-details end —
official-current; retrieved2026-07-14T19:13:54.602937Z; support all-match delivery, request correlation, returned metadata, and the completion boundary. IBblocking contract helpers andreqContractDetails—library-source,ib_async2.1.0; retrieved2026-07-15T03:42:42Z; support blocking-wrapper behavior and zero/one/many interpretation.IB.qualifyContractsAsyncandreqContractDetailsAsync—library-source, 2.1.0; retrieved2026-07-15T03:42:42Z; support in-place mutation, positional results, ambiguity filtering,SMARTpreservation, request allocation, and future completion.ContractandContractDetails—library-source, 2.1.0; retrieved2026-07-15T03:42:42Z; support identity, routing, schedule, increment, and market-rule fields.
No live contract lookup is required for this page. CI compiles both examples as ordinary scripts, binds their ib_async calls to 2.1.0 signatures, executes the unique-match gate against offline details, and verifies pinned qualification mutation and ambiguity behavior with deterministic responses.