View as markdown
Endpoints · Scan

Market-wide scan (live)

POSThttps://api.tickerbot.io/v2/scan

Filter the live ticker universe with a SQL WHERE clause.

stringrequired

SQL 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_pct

Column 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 desc

Sort direction.

ascAscending — smallest or earliest first.
descdefaultDescending — largest or most recent first.
integerdefault 50

Page 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.

string

Opaque 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 false

Return every column instead of the default set. Mutually exclusive with columns — passing both is a 400.

string

Slug 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.

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.

string

Filter the aggregate rows (requires group_by). Custom signals are valid here too.

as_ofstring

Server time this response was assembled (ISO 8601).

queryobject

Your 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`.

countnumber

Rows in this page.

next_cursorstring

Opaque token for the next page; `null` on the last page. Pass it back as `cursor`.

resultsarray

One row per match — every column on the [schema page](/docs/schema), plus any you named.

200
Success — the response shape is documented under Returns above.
400
bad_request — malformed q, unknown column, invalid order/dir/columns, columns together with full, or an invalid cursor.
404
Referenced universe does not exist.
  • Send q and 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_by and results become rollup rows instead of tickers — breadth by sector, counts of new highs, average RSI per group — shaped by select and filtered by having. Same grammar as /v2/news/scan. For the same rollups at a past moment, see Market-wide scan (as-of).
  • _meta.null_coverage is the honesty block: for each column your q touches it reports null_rows vs evaluable_rows out of in_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: q may 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 — columns is the canonical request name, fields the legacy alias, and the echo uses the old one. A null in an added column means that ticker has no value for it (CRWV has no pe_ratio), not that the column was dropped.
  • Cursors carry the (order-value, ticker) pair from the last row. Sort stability is guaranteed (ticker ASC is the tie-breaker). Treat them as opaque — pass back the next_cursor you received, in the same field you sent the rest of the query.
Market breadth by sector (aggregate mode)
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"}'
Response
{
  "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)"
  ]
}
Gap-up small-caps without earnings this week
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}'
Response
{
  "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 }
  ]
}
Pagination — pass next_cursor back in the body
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"}'
Response
// Next page of the same query — same envelope, with the row after the cursor.