View as markdown
Endpoints · Scan

Market-wide scan (as-of)

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

Filter the ticker universe as it stood at a past moment — the same SQL WHERE clause, rewound.

stringrequired

SQL WHERE expression. Same grammar as live scan, including your custom signals (inlined at run time). Max 4000 chars. See the schema for columns + flags you can compose.

stringrequired

Target 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 auto

Grain the past state is reconstructed at. Not every column 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 column silently re-dated everything else. Set, that case is a 400 interval_unavailable naming the column and the grains that carry it. auto picks the finest servable grain and reports it in _meta.interval.

1mOne minute. The finest stored tier; carries the intraday column subset and ~1,100 of ~13,700 tickers.
1hOne hour. Stored tier, intraday column subset, full universe.
1dOne day. The full-history, full-column tier — the only grain that carries daily-only columns (SMAs, RSI, fundamentals).
autodefaultLet the server pick the finest grain whose columns and ticker coverage both work out, resolving upward on a miss. Reported in _meta.interval.
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.

enumdefault desc

Sort direction.

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

Page size. Max 100.

string

Opaque cursor from the previous response's next_cursor. Row mode only.

string[]

Extra columns per row — additive; the defaults are always present. fields accepted as an alias.

booleandefault false

Return every column instead of the default set. Mutually exclusive with columns (400). With asof it requires a grain carrying every column, so full=true with 1m/1h is a 400 interval_unavailable.

string

Slug 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 at any past moment). 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.

string

Filter the aggregate rows (requires group_by).

as_ofstring

The moment you asked about — the date's close, or the timestamp you passed.

queryobject

Your query, echoed.

_metaobject

Grain served (`interval`), why (`interval_reason`), `coverage_gap` for tickers with no row at that grain, `frozen_fields` for columns taken from current state, and `null_coverage` per predicate column. `resolution` is a deprecated alias of `interval`.

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 as it stood at that instant.

200
Success — the response shape is documented under Returns above.
400
Missing/malformed asof, malformed q, unknown column, invalid order/dir/columns/interval, or invalid cursor. interval_unavailable when the requested grain does not store a referenced column (or when full=true is combined with 1m/1h, which carry a subset of columns).
404
Referenced universe does not exist.
  • This is POST /v2/scan with asof in the body — the same endpoint as the live scan, on its own 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-DD date evaluates q against that day's close; a full ISO timestamp evaluates it at that intraday moment. Pick the grain with interval (1m/1h/1d/auto) — a cross-section resolves to ONE grain, and 1m is only servable for a scoped universe, since the minute tier carries ~1,100 of ~13,700 tickers. Whole-market intraday reads use 1h or 1d.
  • _meta.interval reports the grain served, _meta.interval_reason why, and _meta.frozen_fields lists the columns taken from current state rather than reconstructed.
  • 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.
  • Static fields aren't historized — name, sector, industry, asset_type, and exchange reflect their *current* values. _meta.frozen_fields lists exactly which ones.
  • For per-ticker time series across many bars (rather than a single point-in-time snapshot), use Series or Ticker as-of date.
  • Unlimited depth.
  • A date-only asof resolves to the close of that day. A full ISO timestamp (…T15:30:00Z) resolves intraday at the grain you name with interval (1m/1h/1d/auto). _meta.interval reports the grain served and _meta.interval_reason why; tickers with no row at that grain are disclosed in _meta.coverage_gap with a sample. Illiquid instruments (SPAC warrants, units, rights) routinely have no intraday print, so a small coverage_gap at 1m/1h is 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 Saturday asof returns Friday's values, not an empty set. Beyond the window you get an empty results array with the echoed asof. (/v2/tickers/{ticker}?asof= applies the identical staleness rule since 2026-07-27, and 404s with last_available when nothing is fresh enough.)
Add a column to a past-quarter fundamental scan
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"
  }'
Response
{
  "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": "day_change_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, "day_change_pct": 0.53, "gap_pct": 0.11, "relative_volume": 1.05, "market_cap": 531000000000, "pe_ratio": 11.4 },
    "… (one row per match)"
  ]
}