Saved Scans & Performance
A one-off scan answers what matches now. A saved scan keeps answering it: scanr.ai re-runs it in the background, records when stocks enter the result, and tracks how those stocks performed afterwards. The saved scans you create from the API are the same ones you see in the scanr.ai app, and the other way round.
Save a scan
POST /api/v2/scan/save-filters stores a payload under a name. It needs a token with the write scope. Saving under an existing name updates that scan.
The response is the saved scan. Its leviathan_id is the scan's id, which the performance and alert endpoints below take as scan_id.
To rename a scan, use POST /api/v2/scan/rename-saved-filter with {"filterName": "old", "newName": "new"}; a name that's already taken returns 409.
Read your saved scans
POST /api/v2/scan/myscans lists your saved scans, 10 per page by default (up to 100):
Each entry has the scan's name, payload and leviathan_id. This is also how you move a scan from the app into code: build it visually in the scanr.ai scanner, save it, then read its payload here and run it with /api/v2/scan.
Run a saved scan
Run the saved payload like any other, and pass the scan's id in save_args to record the run against the saved scan instead of as a one-off:
The run is attached only when the payload matches the saved scan's definition; otherwise it's recorded as a one-off. Either way, the scan runs and you get the results.
Alerts: when stocks enter a scan
While a saved scan runs in the background, every change to its result is recorded as an event. POST /api/v2/scan/paginated-scan-events returns them newest first:
tickers holds the stocks that triggered the event (the new entries); all_tickers is the whole result at that moment. For the next page, send next_page back as-is. Leave out scan_id to get events across all of your saved scans.
An event with event_type: "disarmed" means the scan stopped refreshing in the background because its filters can no longer run, for example because a custom indicator or pattern model it uses was deleted. It has no tickers (tickers and all_tickers are null). Its detail says why, as a code with the values the message is built from:
New codes can appear without notice, so treat a code you do not recognize as "this scan can no longer run". Fix the scan's filters and save it again under the same name (save-filters) to restart background refreshes.
To limit the time range, add after and/or before. Both filter on event_time and include the boundary. Each takes an ISO 8601 date-time with a zone, such as "2026-09-29T13:30:00Z" or "2026-09-29T09:30:00-04:00". A bare date or a time with no zone returns 400. event_type returns one kind of event only, such as "notification".
This is the efficient way to follow a scan through the day: poll the events every few minutes instead of re-running the scan. Set after to the newest event_time you have seen. Since after includes the boundary, that event comes back again, so skip any event_id you already hold.
Scan history
GET /api/v2/scan/myscans/scan-history lists the scans you ran (from the API, the MCP server or the app) in the last 30 days, with each run's payload and results. Filter it with scanName, and page with page (starting at 1) and pageSize:
Performance
Power User and Business plans.
scanr.ai measures every saved scan as if you had bought each stock when it entered the scan and sold it after a fixed holding period. The holding periods are time horizons:
The values are case-sensitive: 1M is a month, and there is no 1m horizon.
For each horizon you get the statistics of those simulated trades: win_rate, avg_percent_return, expectancy, sharpe_ratio, sortino_ratio, calmar_ratio, avg_max_drawdown, total_trades and more. Percentages are fractions (0.0784 is 7.84%). insufficient_history is true when a scan hasn't run long enough to judge.
The main endpoints:
universe selects which trades count: all (the default), listed (exchange-listed only) or otc.
Measure a Scan's Edge walks through reading these numbers without fooling yourself.


