Market-wide scan (live)
https://api.tickerbot.io/ v2/ scanFilter the live ticker universe with a SQL WHERE clause.
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 schema for columns + flags you can compose.
stringdefault day_change_pctColumn 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 columns per row, ADDITIVE — the defaults are always present (ticker, name, asset_class, asset_type, price, day_change_pct, gap_pct, relative_volume, market_cap). fields accepted as an alias.
booleandefault falseReturn every column 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 ~14,602 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 (columns, 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.
stringAggregate 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 predicate column, 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`.
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 — every column on the [schema page](/docs/schema), plus any you named.
Status codes
200400bad_request — malformed q, unknown column, 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. Column 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 at a past moment, see Market-wide scan (as-of). _meta.null_coverageis the honesty block: for each column yourqtouches it reportsnull_rowsvsevaluable_rowsout ofin_scope_rows. A row that is NULL on a predicate column 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 columns (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.
- 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 column means that ticker has no value for it (CRWV has nope_ratio), not that the column 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": "day_change_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, "day_change_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":"eyJhZnRlcl9vcmRlcl92YWx1ZSI6bnVsbCwiYWZ0ZXJfdGlja2VyIjoiQVNCIn0"}'// Next page of the same query — same envelope, with the row after the cursor.