Market-wide scan
https://api.tickerbot.io/ v2/ scanEvery ticker matching a SQL WHERE clause. Right now, or with asof, as of any past date.
Body parameters
stringrequiredSQL WHERE expression. Max 4000 chars; semicolons, comments and write keywords are rejected. Your custom signals are valid here — each expands to its SQL at run time. See the signals catalog for columns + flags you can compose.
stringOptional. Target moment as YYYY-MM-DD (that day's close) or an ISO timestamp (that intraday moment) — the same read as it stood then, unlimited depth. Full contract under As of a past date.
enumdefault autoGrain the past state is reconstructed at: 1m, 1h, 1d, or auto (default). Only valid alongside asof — a live read with interval is a 400. Details under As of a past date.
1m | One minute. The finest stored tier; carries the intraday column subset and ~1,100 of ~13,700 tickers. |
1h | One hour. Stored tier, intraday column subset, full universe. |
1d | One day. The full-history, full-column tier — the only grain that carries daily-only columns (SMAs, RSI, fundamentals). |
autodefault | Let the server pick the finest grain whose columns and ticker coverage both work out, resolving upward on a miss. Reported in _meta.interval. |
stringdefault change_1d_pctSignal to sort by. In aggregate mode the default is the count alias tickers — or, with a custom select, the last item's alias — sorted NULLS LAST with the group keys as tiebreak.
enumdefault descSort direction.
asc | Ascending — smallest or earliest first. |
descdefault | Descending — largest or most recent first. |
integerdefault 50Page size. Max 100. Aggregate mode does not paginate — it sets truncated: true when groups were cut, so sort with order to keep the ones you want.
stringOpaque cursor from the previous response's next_cursor. Row mode only.
string[]Extra signals per row, ADDITIVE — the defaults are always present (ticker, name, asset_class, asset_type, price, change_1d_pct, gap_pct, relative_volume, market_cap). fields accepted as an alias.
booleandefault falseReturn every signal instead of the default set. Mutually exclusive with columns — passing both is a 400.
stringSlug of a system universe (top_10, top_100) or one of your own. Omitted, the scan runs across all ~21,082 tracked tickers.
string[]One or more asset classes — slug or comma-separated list (stocks, rates, crypto, fx). Validated for shape, not against a fixed list, so a well-formed class we don't track simply matches nothing. Echoed in query.
string[]AGGREGATE MODE: 1–6 group keys (signals, expressions, or one of your custom signals as a boolean key). Results become rollup rows. Name a key with AS to choose its JSON key (market_cap > 1e11 AS mega); an un-named expression is named for you rather than returned as ?column?. Incompatible with columns/full/cursor; works with asof.
string[]Aggregate output items (requires group_by). Default: the group keys + COUNT(*) AS tickers. Supports count/avg/sum/min/max/stddev/string_agg/bool_and/bool_or plus FILTER (WHERE …), and your custom signals inside expressions. Alias with AS; a last item without one is a 400.
stringFilter the aggregate rows (requires group_by). Custom signals are valid here too.
Returns
as_ofstringServer time this response was assembled (ISO 8601).
queryobjectYour query, echoed — `q`, `order`, `dir`, `limit`, and any scope.
_metaobject`null_coverage` reports, per signal in the predicate, how many in-scope rows are NULL and therefore never evaluated — absence from `results` means "no value", not "did not match". `scope` additionally describes an explicit `universe`. `deprecated_columns` lists any column you named under its pre-2026-09-07 spelling that was served under its current name (`requested`, `use`, `note`).
countnumberRows in this page.
next_cursorstringOpaque token for the next page; `null` on the last page. Pass it back as `cursor`.
truncatedbooleanAggregate mode only (`group_by`): `true` when the rollup stopped at its row cap. Aggregate responses are unpaged, so `next_cursor` is absent there.
resultsarrayOne row per match — every signal on the [schema page](/docs/schema), plus any you named.
Status codes
200400bad_request — malformed q, unknown signal, invalid order/dir/columns, columns together with full, or an invalid cursor.404universe does not exist.Notes
- Send
qand the rest in a JSON body — no URL-encoding, no length limits, the same shape on every paging request. GET with query-string params also works, for short queries and quick testing. Signal names are the customer-facing ones on the schema page; there is no translation layer. - Aggregate mode: pass
group_byand results become rollup rows instead of tickers — breadth by sector, counts of new highs, average RSI per group — shaped byselectand filtered byhaving. Same grammar as/v2/news/scan. For the same rollups as of a past date, see Market-wide scan (as-of). _meta.null_coverageis the honesty block: for each signal yourqtouches it reportsnull_rowsvsevaluable_rowsout ofin_scope_rows. A row that is NULL on a signal in the predicate is never evaluated, so it cannot match — on a market-wide scan that is routinely thousands of tickers (market_cap alone is NULL on ~7,900 of ~13,800), and it is the difference between "nothing qualified" and "most of the universe had no value to test".- Custom signals:
qmay reference your custom signal names — each is expanded to its SQL before the scan runs, mixed freely with built-in signals (q = my_squeeze AND market_cap > 1e9). See Create a custom signal. - Pair with
asof: "YYYY-MM-DD"to run the same WHERE against historical state — see Market-wide scan (as-of). - The response echoes your projection as
fields, whichever spelling you sent —columnsis the canonical request name,fieldsthe legacy alias, and the echo uses the old one. Anullin an added signal means that ticker has no value for it (CRWV has nope_ratio), not that the signal was dropped. - Cursors carry the (order-value, ticker) pair from the last row. Sort stability is guaranteed (
ticker ASCis the tie-breaker). Treat them as opaque — pass back thenext_cursoryou received, in the same field you sent the rest of the query.
More examples
curl -X POST "https://api.tickerbot.io/v2/scan" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"q":"above_sma_200 AND market_cap > 1e9","group_by":"sector","having":"COUNT(*) >= 10"}'{
"as_of": "2026-08-11T22:04:00.000Z",
"query": { "q": "above_sma_200 AND market_cap > 1e9", "group_by": "sector", "having": "COUNT(*) >= 10", "limit": 50, "order": "tickers", "dir": "desc", "universe": null, "asset_class": null },
"count": 12,
"results": [
{ "sector": "Financial Services", "tickers": 348 },
{ "sector": "Healthcare", "tickers": 275 },
{ "sector": "Technology", "tickers": 258 },
"… (one row per sector)"
]
}curl -X POST "https://api.tickerbot.io/v2/scan" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"q":"above_sma_200 AND market_cap > 1e10","columns":"pe_ratio","limit":1}'{
"as_of": "2026-08-11T23:22:44.604Z",
"query": { "q": "above_sma_200 AND market_cap > 1e10", "limit": 1, "order": "change_1d_pct", "dir": "desc", "fields": ["pe_ratio"], "full": false, "universe": null, "asset_class": null },
"_meta": { "null_coverage": { "…": "see the sample response above" } },
"count": 1,
"next_cursor": "eyJhZnRlcl9vcmRlcl92YWx1ZSI6MC4xNzY0LCJhZnRlcl90aWNrZXIiOiJDUldWIn0",
"results": [
{ "ticker": "CRWV", "name": "CoreWeave, Inc. Class A Common Stock", "asset_class": "stocks", "asset_type": "CS", "price": 103.748, "change_1d_pct": 0.1764, "gap_pct": 0.0312, "relative_volume": 0.9153, "market_cap": 49466863271, "pe_ratio": null }
]
}curl -X POST "https://api.tickerbot.io/v2/scan" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"q":"market_cap>300000000","order":"ticker","dir":"asc","limit":100,"cursor":"<next_cursor from the previous page>"}'// Next page of the same query — same envelope, with the row after the cursor.Market-wide scan (as-of)
https://api.tickerbot.io/ v2/ scanFilter the ticker universe as of a past date — the same SQL WHERE clause, rewound.
Body parameters
stringrequiredSQL WHERE expression. Same grammar as live scan, including your custom signals (inlined at run time). Max 4000 chars. See the signals catalog for columns + flags you can compose.
stringrequiredTarget moment — YYYY-MM-DD (that close) or YYYY-MM-DDTHH:MM[:SS]Z (that intraday moment; UTC Z only, numeric offsets are a 400). Unlimited depth.
enumdefault autoGrain the past state is reconstructed at. Not every signal exists at every grain (rsi_14, fundamentals and valuation ratios are 1d-only) and a cross-section must resolve to ONE grain — without this, a single 1d-only signal silently re-dated everything else. Set, that case is a 400 interval_unavailable naming the signal and the grains that carry it. auto picks the finest servable grain and reports it in _meta.interval.
1m | One minute. The finest stored tier; carries the intraday column subset and ~1,100 of ~13,700 tickers. |
1h | One hour. Stored tier, intraday column subset, full universe. |
1d | One day. The full-history, full-column tier — the only grain that carries daily-only columns (SMAs, RSI, fundamentals). |
autodefault | Let the server pick the finest grain whose columns and ticker coverage both work out, resolving upward on a miss. Reported in _meta.interval. |
stringdefault change_1d_pctSignal to sort by. In aggregate mode the default is the count alias tickers — or, with a custom select, the last item's alias.
enumdefault descSort direction.
asc | Ascending — smallest or earliest first. |
descdefault | Descending — largest or most recent first. |
integerdefault 50Page size. Max 100.
stringOpaque cursor from the previous response's next_cursor. Row mode only.
string[]Extra signals per row — additive; the defaults are always present. fields accepted as an alias.
booleandefault falseReturn every signal instead of the default set. Mutually exclusive with columns (400). With asof it requires a grain carrying every signal, so full=true with 1m/1h is a 400 interval_unavailable.
stringSlug of a system or caller-owned universe. Scopes the scan to those tickers as of the same date.
string[]One or more asset classes — slug or comma-separated list (stocks, rates, crypto, fx). Validated for shape, not against a fixed list. Echoed in query.
string[]AGGREGATE MODE: 1–6 group keys — works with asof too (breadth as of any past date). Incompatible with columns/full/cursor. See Market-wide scan (live) for the full aggregate reference.
string[]Aggregate output items (requires group_by). Default: group keys + COUNT(*) AS tickers. With a custom select, pass order naming an item alias or group key; a last item without AS is a 400.
stringFilter the aggregate rows (requires group_by).
Returns
as_ofstringThe moment you asked about — the date's close, or the timestamp you passed.
queryobjectYour query, echoed.
_metaobjectGrain served (`interval`), why (`interval_reason`), `coverage_gap` for tickers with no row at that grain, `frozen_fields` for signals taken from current state, and `null_coverage` per signal in the predicate. `resolution` is a deprecated alias of `interval`. `deprecated_columns` lists any column you named under its pre-2026-09-07 spelling that was served under its current name (`requested`, `use`, `note`).
countnumberRows in this page.
next_cursorstringOpaque token for the next page; `null` on the last page. Pass it back as `cursor`.
resultsarrayOne row per match as it stood at that instant.
Status codes
200400asof, malformed q, unknown signal, invalid order/dir/columns/interval, or invalid cursor. interval_unavailable when the requested grain does not store a referenced signal (or when full=true is combined with 1m/1h, which carry a subset of signals).404universe does not exist.Notes
- This is
POST /v2/scanwithasofin the body — the same endpoint as the live scan, the second tense of the same page because point-in-time is its own question. It is as-of at whole-market scope, with the same grammar and the same response envelope. GET with query-string params works here too. - A
YYYY-MM-DDdate evaluatesqagainst that day's close; a full ISO timestamp evaluates it at that intraday moment. Pick the grain withinterval(1m/1h/1d/auto) — a cross-section resolves to ONE grain, and1mis only servable for a scopeduniverse, since the minute tier carries ~1,100 of ~13,700 tickers. Whole-market intraday reads use1hor1d. _meta.intervalreports the grain served,_meta.interval_reasonwhy, and_meta.frozen_fieldslists the signals taken from current state rather than reconstructed.- Custom signals:
qmay reference your custom signal names — each is expanded to its SQL before the scan runs, mixed freely with built-in signals (q = my_squeeze AND market_cap > 1e9). See Create a custom signal. - The same endpoint also accepts GET with parameters in the query string — handy for short queries and quick interactive testing.
- Static fields aren't historized —
name,sector,industry,asset_type, andexchangereflect their *current* values._meta.frozen_fieldslists exactly which ones. - For per-ticker time series across many bars (rather than a single point-in-time snapshot), use Series or Ticker state (as-of).
- Unlimited depth.
- A date-only
asofresolves to the close of that day. A full ISO timestamp (…T15:30:00Z) resolves intraday at the grain you name withinterval(1m/1h/1d/auto)._meta.intervalreports the grain served and_meta.interval_reasonwhy; tickers with no row at that grain are disclosed in_meta.coverage_gapwith a sample. Illiquid instruments (SPAC warrants, units, rights) routinely have no intraday print, so a smallcoverage_gapat1m/1his normal rather than a fault. If there is no state exactly at the requested moment (weekend, holiday, or a gap), the server carries forward the most-recent row within the lookback window (14 days at daily resolution, 5 days for intraday granularities) — a Saturdayasofreturns Friday's values, not an empty set. Beyond the window you get an emptyresultsarray with the echoedasof. (/v2/tickers/{ticker}?asof=applies the identical staleness rule since 2026-07-27, and 404s withlast_availablewhen nothing is fresh enough.)
More examples
curl -X POST "https://api.tickerbot.io/v2/scan" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"q": "above_sma_50 AND pe_ratio < 25 AND market_cap > 10000000000",
"asof": "2024-02-23",
"columns": "pe_ratio"
}'{
"as_of": "2024-02-23",
"query": { "q": "above_sma_50 AND pe_ratio < 25 AND market_cap > 10000000000", "asof": "2024-02-23", "interval": "auto", "limit": 50, "order": "change_1d_pct", "dir": "desc", "fields": ["pe_ratio"], "full": false, "universe": null, "asset_class": null },
"_meta": {
"interval": "1d",
"interval_requested": "auto",
"interval_reason": "date-only asof resolves to the daily close",
"resolution": "daily",
"frozen_fields": ["name", "sector", "industry", "asset_type", "exchange", "asset_class", "ticker_category"],
"null_coverage": { "in_scope_rows": 8539, "columns": { "pe_ratio": { "null_rows": 5127, "evaluable_rows": 3412 } }, "note": "…" }
},
"count": 18,
"next_cursor": null,
"results": [
{ "ticker": "JPM", "name": "JPMorgan Chase & Co.", "asset_class": "stocks", "asset_type": "CS", "price": 183.97, "change_1d_pct": 0.53, "gap_pct": 0.11, "relative_volume": 1.05, "market_cap": 531000000000, "pe_ratio": 11.4 },
"… (one row per match)"
]
}