# IBKR API scanner: reqScannerSubscription in practice

> IBKR API scanner in practice: scan codes from reqScannerParameters, filters, 50 rows and 10 scans, errors 165, 322 and 10360, and a live pre-market run.

Source: https://scalpeltrade.com/blog/tws-api-scanner
By Scalpel Team. Published 2026-06-05, last changed 2026-10-22. 9 min read.

**In short:**

The IBKR API scanner is TWS's Advanced Market Scanner over the socket. Call `reqScannerParameters()` once to get the XML list of scan codes and filters, then `reqScannerSubscription(reqId, subscription, [], filters)` with an instrument, a location such as `STK.US.MAJOR`, a scan code such as `TOP_PERC_GAIN` and filter tags such as `priceAbove`. Results arrive through `scannerData` and `scannerDataEnd` as ranked contracts with no prices, refreshed about every 30 seconds, at most 50 rows a scan and 10 scans at once. Float cannot be used as a filter, and you need a quote request, and a market data line, for every symbol you want live numbers on.

IBKR's scanner is the cheapest serious scanner a day trader on Interactive Brokers can get: it costs nothing extra and it runs in the pre-market. We built Scalpel's scanner on it, so this is the practical version of IBKR's documentation: how to find valid scan codes and filters, a working Python subscription, the limits and error codes you will meet, and what we do with the results. API details were checked against IBKR's reference and a live TWS session on October 22, 2026; the Scalpel details come from its source code. Scalpel is independent and not affiliated with or endorsed by Interactive Brokers.

## How the IBKR API scanner works

IBKR's [market scanner introduction](https://www.interactivebrokers.com/docs/tws-api/doc/market-scanner/introduction) describes the whole thing in a few lines, and every line has a consequence:

| IBKR says | What it means for your code |
| --- | --- |
| Results "simply consist of a list of contracts" | No price, change or volume. Request quotes yourself |
| A subscription keeps sending "periodic updates until the request is cancelled" | You get a fresh ranked list on each pass, each one closed by `scannerDataEnd` |
| "a maximum result of 50 results per scan code" | Ask for more rows and you still get 50 |
| "only 10 API scans can be active at a time" | Shared by every API program on the same login |
| Filters "generally" need market data subscriptions | A filter on a market you do not subscribe to can come back empty |

Three calls do the work. `reqScannerParameters()` returns the XML catalogue of everything the scanner understands. `reqScannerSubscription(reqId, subscription, scannerSubscriptionOptions, scannerSubscriptionFilterOptions)` starts a scan; the options list is for IBKR's internal use and stays empty, and the filters are a list of tag and value pairs. `cancelScannerSubscription(reqId)` stops it.

## Find scan codes and filters with reqScannerParameters

You cannot guess scan codes or filter names: they come from the parameters XML, which TWS returns through the `scannerParameters` callback. On our live session on October 22, 2026, that XML was 2,087,485 characters, about 2 MB. It listed 649 scan types, 242 of them for US stocks, and 763 filter codes. A short script turns it into something you can read:

```python
import xml.etree.ElementTree as ET

root = ET.parse("scanner_params.xml").getroot()

stock_scans = [
    (scan.findtext("scanCode"), scan.findtext("displayName"))
    for scan in root.iter("ScanType")
    if "STK" in scan.findtext("instruments", "").split(",")
]
print(len(stock_scans), "scans for US stocks, for example:", stock_scans[:5])

filters = {f.findtext("code") for f in root.iter("AbstractField") if f.findtext("code")}
print(len(filters), "filter codes")
print("floatSharesBelow listed:", "floatSharesBelow" in filters)
```

Its last line is the trap. The XML lists `floatSharesBelow`, and the API refuses it: in our live tests it came back as error 10360, "Scan filter floatSharesBelow is not allowed". IBKR's [parameters reference](https://www.interactivebrokers.com/docs/tws-api/doc/market-scanner/market-scanner-parameters/receive-market-scanner-parameters) does warn that these are the parameters of TWS's scanners, "not all available in API". So treat the XML as a list of candidates, and test each filter before you build on it.

The codes most momentum traders want, all present in our copy of the XML:

| Scan code | Display name in TWS | Before the open |
| --- | --- | --- |
| `TOP_PERC_GAIN` | Top % Gainers | Works, change measured from yesterday's close |
| `SCAN_stVolumeVsAvg3min_DESC` | 3min Volume vs Average Volume Desc | Works |
| `HOT_BY_VOLUME` | Hot Contracts by Volume | Works |
| `HALTED` | Halted | Works |
| `HIGH_OPEN_GAP` | Top Close-to-Open % Gainers | Empty until 09:30 ET: there is no open yet |
| `TOP_OPEN_PERC_GAIN` | Top % Gainers Since Open | Measures from the open, so it is for the regular session |

And the filters that matter for small caps: `priceAbove` and `priceBelow`, `changePercAbove`, `volumeAbove`, `numSharesBelow` (shares outstanding, the closest allowed stand-in for float) and `stkTypes`, which takes values such as `exc:ETF`. IBKR's advice when a scan misbehaves is to build the same scan in TWS's [Advanced Market Scanner](https://ibkrguides.com/tws/usersguidebook/mosaic/advancedscanner.htm) first: if TWS cannot do it, the API cannot either.

IBKR's own lesson covers requesting the parameters and running a first scan in Python, and is a good companion to the code below:

*Video: [TWS Python API Market Parameters and Scanners](https://www.youtube.com/watch?v=bNlv5xcmObA) (Interactive Brokers, 9:22)*

## reqScannerSubscription in Python

This script saves the parameters XML, then runs a pre-market gainers scan for $1 to $20 stocks up 10% or more and prints the top of each pass for 70 seconds:

```python
import threading
import time

from ibapi.client import EClient
from ibapi.scanner import ScannerSubscription
from ibapi.tag_value import TagValue
from ibapi.wrapper import EWrapper

SCAN_ID = 7001

class ScanApp(EWrapper, EClient):
    def __init__(self):
        EClient.__init__(self, self)
        self.ready = threading.Event()
        self.rows = {}

    def nextValidId(self, orderId: int):
        self.ready.set()

    def scannerParameters(self, xml: str):
        with open("scanner_params.xml", "w", encoding="utf-8") as f:
            f.write(xml)
        print(f"Saved {len(xml):,} characters of scanner parameters")

    def scannerData(self, reqId, rank, contractDetails, distance, benchmark, projection, legsStr):
        self.rows[rank] = contractDetails.contract.symbol

    def scannerDataEnd(self, reqId: int):
        ranked = [self.rows[r] for r in sorted(self.rows)]
        print(time.strftime("%H:%M:%S"), len(ranked), "rows:", ranked[:10])
        self.rows.clear()

    def error(self, reqId, errorTime, errorCode, errorString, advancedOrderRejectJson=""):
        print(f"[{errorCode}] req {reqId}: {errorString}")

app = ScanApp()
app.connect("127.0.0.1", 7497, 46)
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?")

app.reqScannerParameters()  # once, to see every scan code and filter

sub = ScannerSubscription()
sub.instrument = "STK"
sub.locationCode = "STK.US.MAJOR"
sub.scanCode = "TOP_PERC_GAIN"
sub.numberOfRows = 50

filters = [
    TagValue("priceAbove", "1"),
    TagValue("priceBelow", "20"),
    TagValue("changePercAbove", "10"),
]
app.reqScannerSubscription(SCAN_ID, sub, [], filters)
time.sleep(70)  # long enough to see IBKR refresh the scan
app.cancelScannerSubscription(SCAN_ID)
app.disconnect()
```

We ran it at 08:21 ET on October 22, 2026, against a live TWS login. Three passes arrived, at 08:21:26, 08:21:55 and 08:22:31, with 21, 20 and 20 names: 29 and 36 seconds apart, and well short of 50 rows, because only that many stocks passed the filters before the open. Each pass is the complete list again, not a change list, which is why the script clears its rows after every `scannerDataEnd`.

![A timeline of a live IBKR API scan on October 22, 2026: passes at 08:21:26, 08:21:55 and 08:22:31 ET with 21, 20 and 20 names, 29 and 36 seconds apart, with quotes streaming in between.](https://scalpeltrade.com/images/blog/tws-api-scanner/refresh-timeline.webp)

*Our live top gainers scan before the open on October 22, 2026, times in ET. IBKR sends a complete ranked list on each pass; prices only move between passes if you stream quotes yourself.*

`STK.US.MAJOR` covers the major US exchanges. Use `STK.US.MINOR` for OTC stocks, or `STK.US` for both; all three are in the XML. The rank starts at 0 for the top name.

With `ib_async`, one call runs a single pass and cancels the subscription for you:

```python
from ib_async import IB, ScannerSubscription, TagValue

ib = IB()
ib.connect("127.0.0.1", 7497, clientId=47)

sub = ScannerSubscription(instrument="STK", locationCode="STK.US.MAJOR", scanCode="TOP_PERC_GAIN", numberOfRows=50)
filters = [TagValue("priceAbove", "1"), TagValue("priceBelow", "20"), TagValue("changePercAbove", "10")]

rows = ib.reqScannerData(sub, [], filters)  # one pass, then the subscription is cancelled
for row in rows[:10]:
    print(row.rank, row.contractDetails.contract.symbol)
ib.disconnect()
```

For a list that keeps updating, use `ib.reqScannerSubscription` instead and read its `updateEvent`. If you are new to either library, our [IBKR API Python starter](https://scalpeltrade.com/blog/ibkr-api-python) covers connecting and client ids first.

## Interactive Brokers API scanner limits and errors

| What you hit | What happens | What to do |
| --- | --- | --- |
| More than 50 rows | You get 50 | Run more scans and merge them |
| An 11th scan | Error 322, IBKR's generic server error, not a clear limit message | Count your scans across every program on the login |
| A filter the API refuses | Error 10360, for example on `floatSharesBelow` | Use `numSharesBelow`, and read float from filings |
| Too many filters | An empty scan: IBKR's [request reference](https://www.interactivebrokers.com/docs/tws-api/doc/market-scanner/market-scanner-subscription/request-market-scanner-subscription) warns that over-filtering can return nothing | Loosen one filter at a time |
| Error 165 with results | "Historical market Data Service query message" in IBKR's [error code list](https://www.interactivebrokers.com/docs/tws-api/doc/error-handling/error-codes), sent with scanner results | Treat it as a notice, never as the end of the stream |
| Live numbers for 50 names | 50 quote requests, 50 of your 100 default [market data lines](https://scalpeltrade.com/blog/ibkr-market-data-lines) | Cap the list you quote |

The 322 and 165 rows come from our own work, written up in the pillar on [what the TWS API can and cannot do for scalping](https://scalpeltrade.com/blog/tws-api-scalping). Error 165 cost us the most. TWS sends it next to scanner results as an "N items retrieved" notice, often before the data, and the Rust client we use ended the scan on it, so two of our five scans never delivered a row. Our Python run above received no 165 at all, and `ib_async` 2.1.0 responds to one by clearing its result list and waiting for the next pass rather than ending. Whatever your library, check what it does with 165 before you trust a scan that has gone quiet.

The line cost is the one people underestimate. The scan itself needs no quote subscription, but a list of symbols with no prices is not much use while trading, and each quote holds one line for as long as you stream it. Fifty names on a default account leaves 50 lines for everything else, including TWS's own watchlists.

## What Scalpel does with the IBKR scanner

Scalpel's scanner is IBKR's scanner, run by Scalpel on its own API connection. From the source:

- **Five scans, side by side.** `TOP_PERC_GAIN`, `SCAN_stVolumeVsAvg3min_DESC`, `HALTED`, `HOT_BY_VOLUME` and `HIGH_OPEN_GAP`, all on `STK.US.MAJOR` at 50 rows, all with the same filters. That uses half of the 10-scan allowance and leaves the rest for anything else you run.
- **Your settings become filter tags.** **Price min** and **Price max** become `priceAbove` and `priceBelow`, **Min change %** becomes `changePercAbove`, **Min volume** becomes `volumeAbove`, **Max shares out (M)** becomes `numSharesBelow`, and **Stocks only (no ETFs)** becomes `stkTypes` with `exc:ETF`. Filters left empty are not sent. Changing any filter cancels and resubscribes all five scans.
- **Defaults that do not hide runners.** $0.10 to $100, up 10% or more, 30 names. The shares-out cap is off by default because a name IBKR has no share count for would be dropped, and those are often the fresh small caps that move.
- **One merged list.** Every scan's top name first, then every scan's second, and so on, so the top of any scan gets in at once. A name on two scans keeps its better place, and a name that drops off every scan keeps its slot for 3 minutes while there is room, so a stock at the edge of a scan does not flicker in and out.
- **Retries that match the errors.** A scan that ends after it has delivered data is resubscribed 3 seconds later; one that fails without delivering (a refused filter, a pacing refusal) waits 30 seconds, and the failure is logged once rather than every time.
- **Prices from a second connection.** IBKR only names the symbols. A separate quote connection streams Level 1 for each listed name, one line each, and the panel's change, ranking and relative volume update tick by tick between IBKR's passes. Float, which the scan cannot filter on, is replaced by shares outstanding read from SEC EDGAR filings.

*Screenshot: Settings, Connection: the IBKR scanner block at its defaults. Each field maps to one IBKR filter tag.*

The [IBKR scanner setup guide](https://scalpeltrade.com/guides/set-up-ibkr-scanner) walks through every field, [scan types](https://scalpeltrade.com/guides/scan-types) explains the panels built on the list, [pre-market scanning](https://scalpeltrade.com/guides/premarket-scanning) covers the hours before the open, and [fix the scanner when it is off or empty](https://scalpeltrade.com/guides/fix-scanner-off) covers the empty states. For the trading side of scanning rather than the code, see [momentum trading on IBKR: scanner setup for gappers](https://scalpeltrade.com/blog/ibkr-momentum-scanner). Once a name is on a desk, the next request is its book, which we cover in [Level 2 through the TWS API](https://scalpeltrade.com/blog/tws-api-market-depth).

Our scanner section video shows the list, the merge and the desk loading in two minutes:

*Video: [Scanner | Scalpel guide 8 of 15](https://www.youtube.com/watch?v=9EFWeGhzpFE) (Scalpel Trade, 1:50)*

A scanner finds stocks that are moving; it does not find trades. Small-cap momentum is fast and most people who try it lose money.

*Trading carries a significant risk of loss. Nothing here is financial advice.*

## Questions

**Does the IBKR API have a stock scanner?**

Yes. reqScannerSubscription runs the same scans as TWS's Advanced Market Scanner and returns up to 50 ranked contracts per scan, refreshed about every 30 seconds, with up to 10 scans active at once.

**How do I get the list of IBKR scanner parameters?**

Call reqScannerParameters once. TWS returns an XML document, about 2 MB on our session, through the scannerParameters callback, listing every scan code, location code and filter. Not every filter in it is allowed through the API.

**Can the IBKR API scanner filter by float?**

No. floatSharesBelow is listed in the parameters XML but the API refuses it with error 10360. numSharesBelow, a cap on shares outstanding, is the closest filter that works.

**Why does the IBKR API scanner return no prices?**

The scanner only returns contracts and their rank. For price, change or volume you request market data for each symbol separately, and each streaming quote uses one market data line.

**Does the IBKR API scanner work in the pre-market?**

Yes. Top percent gainers, volume and halt scans return pre-market movers. Scans that measure from the open, such as the close-to-open gap scan, stay empty until 09:30 ET.

**How many scanner subscriptions can I run through the IBKR API?**

Ten at a time, shared by every API program on the same login. In our tests an eleventh failed with error 322, a generic server error rather than a clear limit message.
