By Scalpel Team. , updated . 10 min read.

In short
TWS API market depth comes from reqMktDepth(reqId, contract, numRows, isSmartDepth, mktDepthOptions). Set isSmartDepth to True on a SMART-routed stock and IBKR merges every venue you subscribe to into one book, sent as updateMktDepthL2 messages that insert, update or delete a numbered row on the bid or ask. You rebuild the book yourself from those messages, and empty it when error 317 says IBKR reset it. A default account can stream only 3 symbols' depth at once, and you need a depth subscription such as Nasdaq TotalView for rows to arrive.
Level 2 is the reason most traders want the API, and the part of the TWS API with the least forgiving rules. We stream it on every desk in Scalpel, so this is the version of IBKR's documentation we wish we had read first: each parameter, keeping a book correct from a stream of row edits, the limits, and how our montage is built from it. Everything about the API was checked against IBKR's reference and its 10.51.01 Python client on October 22, 2026. Everything about Scalpel comes from its source code. Scalpel is independent and not affiliated with or endorsed by Interactive Brokers.
IBKR's market depth reference defines one request and two callbacks. Python, Java and C++ call the request reqMktDepth; C# and Visual Basic call it reqMarketDepth.
| Parameter | What it does |
|---|---|
reqId | Your number for this book. Every update carries it, and you cancel with it |
contract | The stock. For SMART depth its exchange must be SMART |
numRows | How many rows to keep on each side |
isSmartDepth | True: one book merged across venues. False: one venue's book |
mktDepthOptions | Internal use. Pass an empty list |
Each update arrives as updateMktDepthL2(reqId, position, marketMaker, operation, side, price, size, isSmartDepth). side is 1 for the bid and 0 for the ask. position is the row number, 0 at the inside. operation says what to do with that row: 0 insert, 1 update, 2 delete. With SMART depth, marketMaker is the exchange holding the order, such as NSDQ or ARCA; IBKR's montage shows venues, not firms (MMID). Cancel with cancelMktDepth(reqId, isSmartDepth).
One thing IBKR says plainly and many first versions miss: depth "is sent without sampling or filtering". The ordinary Level 1 quote is the opposite. IBKR's streaming data page lists US stock quotes as snapshots every 250 ms. So the book can show a level being hit before your Level 1 last price moves, and your code must apply every message, because each one edits the row the previous one left.
| SMART depth | Direct depth | |
|---|---|---|
| Request | isSmartDepth=True, contract on SMART | isSmartDepth=False, contract on one exchange |
| What you get | Every venue you subscribe to, in one book | That one exchange's book |
marketMaker | The exchange code | The market maker's MPID |
| Like in TWS | BookTrader | The Market Depth window |
For a scalper, SMART depth is the useful one: it is the whole lit book in one stream, and it costs one depth slot for the symbol however many venues feed it. Which venues appear depends on your subscriptions. Nasdaq TotalView and NYSE ArcaBook are the usual pair for small caps; our guide to IBKR market data for day trading walks through subscribing, and Nasdaq TotalView covers what that feed adds.
Two contradictions in IBKR's own material, so you do not lose an afternoon to them:
updateMktDepth and direct depth with updateMktDepthL2, yet the same page describes updateMktDepth as "used for direct routed requests only" and gives updateMktDepthL2 a marketMaker field that holds "the exchange holding the order if isSmartDepth is True". The safe answer is to implement both callbacks and route them through one function, which is what the code below and Scalpel both do.isSmartDepth flag and the reference page say otherwise, and SMART depth works.To see what direct depth looks like before you code it, IBKR's lesson on the TWS Market Depth window is a nine-minute tour, including the per-exchange buttons that mirror direct requests:

The book is a list per side, best first, and every message edits one numbered row. The rule that matters: an insert at a position pushes the rows below it down one, and a delete pulls them up one. Treat an insert as an overwrite and you lose a row; skip the shift on a delete and the rest of that side is off by one until the next reset.

This Python script keeps both sides correct and prints the top three rows a second for ten seconds:
import threading
import time
from ibapi.client import EClient
from ibapi.contract import Contract
from ibapi.wrapper import EWrapper
REQ_ID, ROWS = 2001, 10
class DepthApp(EWrapper, EClient):
def __init__(self):
EClient.__init__(self, self)
self.ready = threading.Event()
self.bids, self.asks = [], [] # rows of [venue, price, size], best first
def nextValidId(self, orderId: int):
self.ready.set()
def updateMktDepthL2(self, reqId, position, marketMaker, operation, side, price, size, isSmartDepth):
book = self.bids if side == 1 else self.asks # side: 1 = bid, 0 = ask
row = [marketMaker, price, size]
if operation == 0: # insert: the rows below move down one
book.insert(min(position, len(book)), row)
elif operation == 1 and position < len(book): # update this row
book[position] = row
elif operation == 2 and position < len(book): # delete: the rows below move up
del book[position]
def updateMktDepth(self, reqId, position, operation, side, price, size):
self.updateMktDepthL2(reqId, position, "", operation, side, price, size, False)
def error(self, reqId, errorTime, errorCode, errorString, advancedOrderRejectJson=""):
if errorCode == 317: # TWS reset its book: empty ours, the updates rebuild it
self.bids.clear()
self.asks.clear()
else:
print(f"[{errorCode}] req {reqId}: {errorString}")
app = DepthApp()
app.connect("127.0.0.1", 7497, 44)
threading.Thread(target=app.run, daemon=True).start()
if not app.ready.wait(5):
raise SystemExit("No reply: is TWS or IB Gateway running with the API on this port?")
stock = Contract()
stock.symbol, stock.secType, stock.exchange, stock.currency = "ABCD", "STK", "SMART", "USD"
app.reqMktDepth(REQ_ID, stock, ROWS, True, []) # True = SMART depth
for _ in range(10):
time.sleep(1)
bids, asks = list(app.bids), list(app.asks)
print("bids", bids[:3], "asks", asks[:3])
app.cancelMktDepth(REQ_ID, True)
app.disconnect()
ABCD is a placeholder that returns error 200, no security definition: put in a real symbol you have depth for.
Three details matter, two of them from IBKR's error list:
The callbacks run on the API thread, so copy the lists before you draw or compute from them, as the loop does.
ib_async keeps the book for you on a ticker:
from ib_async import IB, Stock
ib = IB()
ib.connect("127.0.0.1", 7497, clientId=45)
stock = Stock("ABCD", "SMART", "USD") # put a real symbol here
if ib.qualifyContracts(stock)[0] is None:
ib.disconnect()
raise SystemExit("IBKR does not know that symbol.")
ticker = ib.reqMktDepth(stock, numRows=10, isSmartDepth=True)
for _ in range(10):
ib.sleep(1)
for level in ticker.domBids[:3]:
print("bid", level.marketMaker, level.price, level.size)
for level in ticker.domAsks[:3]:
print("ask", level.marketMaker, level.price, level.size)
ib.cancelMktDepth(stock, isSmartDepth=True)
ib.disconnect()
Note that isSmartDepth defaults to False in ib_async, so pass it. Also read how a library applies inserts before you trust it. ib_async 2.1.0 clears the book on error 317, but it stores rows by position and treats an insert as "insert or overwrite" rather than shifting the rows below. We shift, because IBKR describes 0 as inserting a new row. If you build on either approach, check your book against TWS's BookTrader on a busy stock for a few minutes.
Depth is the scarcest data IBKR sells through the API. From IBKR's specialized market data lines table, checked on October 22, 2026:
| Market data lines | Level 2 books at once |
|---|---|
| 100 (the default) | 3 |
| 101 to 400 | 3 |
| 401 to 500 | 4 |
| 601 to 700 | 6 |
| 1001 to 1100 | 10 |
So three Quote Booster packs, which take you to 400 lines, still leave you with 3 books. Ask for a fourth symbol and IBKR's error 309 comes back: "Max number (3) of market depth requests has been reached". The same entry adds that API clients "may make multiple market depth requests for the same security", so the cap counts distinct symbols, not requests. IBKR applies the same limit to TWS's own depth windows (BookTrader, Market Depth and the Integrated Stock Window) as to API clients, and we treat the slots as first come, first served: close any depth window in TWS you are not reading before your code asks for its books.
Two gaps to design around:
reqMktDepthExchanges() returns each venue and whether it supports SMART depth or only direct depth. Run it once when a book looks thinner than you expect.Here is what our code does with all this, read from Scalpel's source:
How to read the result, rather than build it, is in our guide to reading Level 2 and the pillar how to read Level 2. Our section video shows the montage, tape and chart working together:

If you are writing your first script, start with our IBKR API Python starter for the connection basics, and see the TWS API scanner for finding which symbols deserve your three books. API access itself is switched on as in our enable TWS API guide.
Trading carries a significant risk of loss. Nothing here is financial advice.