# As-of queries

An as-of query rewinds the table to a past date: the state as it actually stood then, not as it looks now. One parameter, `asof`, and one grain control, `interval`.

## What asof answers

Pass `asof` and you get the row (or the match set) as it stood at that instant: one ticker's whole row (`/v2/tickers/{ticker}`), who matched this signal (`/v2/signals/{signal}`), who matched this scan (`/v2/scan`), one row per ticker on Series (`/v2/series`), or the last closed bar (`/v2/tickers/{t}/bars/{interval}`). A `YYYY-MM-DD` date returns the state at that day's close; a timestamp returns it at that intraday moment. Timestamps are the strict ISO subset `YYYY-MM-DDTHH:MM[:SS]Z` — UTC `Z` only, numeric offsets are a `400`.

State is stored at three grains — `1m`, `1h`, `1d` — and they do not all carry the same signals, so a past date carries a second question: *at what grain?* `interval` answers it; `_meta.interval` reports what was actually served.

## Which company did the symbol mean?

Symbols get reassigned. `SHLD` was Sears Holdings until 2018, then a VictoryShares ETF, and today it is the Global X Defense Tech ETF — three unrelated companies, one symbol. Without `asof` a symbol always means the company holding it *now*. With it, the symbol resolves to whoever held it at that instant, and you get that company's identity and its prices:

```shell
# Sears Holdings — the company that traded under SHLD in 2010
curl "https://api.tickerbot.io/v2/tickers/SHLD?asof=2010-06-30" \
  -H "Authorization: Bearer $TICKERBOT_API_KEY"

# the Global X ETF that holds the symbol today
curl "https://api.tickerbot.io/v2/tickers/SHLD" \
  -H "Authorization: Bearer $TICKERBOT_API_KEY"
```

The ticker record carries the ownership history directly: `tenure_start` and `tenure_end` are the dates the company on the record has held the symbol, `symbol_reused` is `true` when an earlier unrelated company held it, `predecessors[]` lists those earlier holders with their own `cik` and `composite_figi`, and `prior_symbols[]` lists the same company's earlier tickers — so `META` reports `FB`. This is what makes a backtest over a past universe safe: the symbol you screen on resolves to the company that actually traded it.

## Choosing the grain: interval

`interval` takes `1m`, `1h`, `1d`, or `auto` (the default) — the same values as [Series](/docs/endpoints/series) and [Bars](/docs/endpoints/bars/get), one vocabulary for "which grain" across the API.

```shell
# Pin the grain. Every signal comes from the 1h tier, or you get a 400.
curl -X POST "https://api.tickerbot.io/v2/scan" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"q": "above_vwap", "asof": "2026-06-28T17:45:00Z", "interval": "1h"}'
```

**Why pin it.** Not every signal exists at every grain — `rsi_14`, fundamentals and valuation ratios are `1d`-only — and a cross-section has to resolve to one grain, so a single `1d`-only signal decides the grain for *every* signal in the request. With `interval` set that is a `400 interval_unavailable` naming the signal and the grains that do carry it, instead of the daily close quietly standing in for everything else.

`auto` picks the finest grain whose signals and ticker coverage both work out, resolving upward on a miss; `_meta.interval_reason` says which grain ran and why, and `_meta.coverage_gap` lists any tickers with no row there. The grain can legitimately differ between two requests — a different projection, universe, or instant changes what is servable — so pin the interval if you are storing numbers or comparing across dates.

`_meta.resolution` is a deprecated alias of `_meta.interval`, still carrying the old `minute`/`hourly`/`daily` names — read `interval`.

## The three shapes

One parameter, three shapes: a ticker read merges per signal, while a signal's match set and a market-wide scan are cross-sections that resolve to a single grain.

**One ticker** — [Ticker state (as-of)](/docs/endpoints/tickers/state#as-of). This is the one shape that *merges per signal*: each signal comes from the finest grain that carries it and has a row at or before your instant, so `price` can be the 17:45 value while `rsi_14` is that morning's daily figure. The row is therefore a blend, which is what `_meta.blended`, `_meta.sources` (per grain, with the columns each contributed) and `_meta.column_intervals` (per column) tell you. `_meta.interval` is the *finest* grain present, not the age of the whole row.

```shell
curl "https://api.tickerbot.io/v2/tickers/AAPL?asof=2026-06-28" \
  -H "Authorization: Bearer YOUR_KEY"
```

**One signal** — [Ticker matches (as-of)](/docs/endpoints/signals/state#as-of):

```shell
curl "https://api.tickerbot.io/v2/signals/at_52w_high?asof=2026-06-28" \
  -H "Authorization: Bearer YOUR_KEY"
```

**Many tickers** — [Market-wide scan (as-of)](/docs/endpoints/scan/state#as-of):

```shell
curl -X POST https://api.tickerbot.io/v2/scan \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"q":"above_sma_200","asof":"2026-06-28"}'
```

Both are **cross-sections**: every row comes from ONE grain — comparing tickers captured at different instants is not a cross-section. That is why a `1d`-only signal in the projection sets the grain for the whole request, and why `interval` exists.

Seven identity signals — `name`, `sector`, `industry`, `asset_type`, `exchange`, `asset_class`, `ticker_category` — are not historized: every as-of read serves them from the ticker's current row, as `_meta.frozen_fields` discloses. Group an as-of scan by `sector` and you group by today's sector.

Membership in an as-of set is point-in-time (survivorship-safe): a ticker appears if it was listed and not yet delisted at that instant, so a since-delisted name can legitimately show up in a historical match set — which is what keeps a backtest over these results honest. Live reads of the same endpoints return currently-active tickers only.

A bare `YYYY-MM-DD` means that day's close; add a time (`2026-06-28T17:45:00Z`) for an intraday moment. A date has none to resolve, so a date plus `interval=1m`/`1h` is a `400`.

## Bars and Series take asof too — the last closed row on a grid

[Bars](/docs/endpoints/bars/get) accept `asof` and return the most recent bar whose period had **closed** at or before the moment you asked about. At 10:00 ET on a Wednesday the day's close has not happened, so `interval=1d&asof=`that instant returns *Tuesday's* bar. A bare date still means "that day's close," matching the rest of this page.

[Series](/docs/endpoints/series) works the same way, for up to 50 tickers at once: `asof` turns the range read into a point read — **one row per ticker**, the last row at or before the instant on the requested `interval`'s grid, with the symbol resolved to whoever held it then (the SHLD rule above). At `interval=1q` the anchor is the date the quarter was *reported*, so you get the latest quarter that was public knowledge at the instant — a restatement filed later doesn't leak back.

On bars, `asof` returns a single bar unless you pass `limit`, which gets you the last `limit` closed bars; on series it is always one row per ticker. On both, combining it with `before`/`cursor` or `from`/`to` is a `400`: those page back from the live tail or bound a window, asof reads one point in time.

## Unlimited depth

`asof` is **unlimited-depth**. No endpoint bounds how far back you can look.

Point-in-time is a different promise from [Series](/docs/endpoints/series), which is also all-time: series answers "how did this evolve"; an as-of query rewinds the whole market to a past cross-section.

## Staleness: "the state AT that instant"

The row served must be within the tier's staleness window (14 days for daily) of the instant you asked for. If nothing that fresh exists — a delisted ticker, an instant before it started trading — the ticker `404`s with a `last_available` pointer rather than handing back a stale row. When you want the *last known* state instead of the state at a moment, read `/v2/series?ticker={t}&limit=1`.

## Coverage: a null can mean "not yet backfilled"

Price, technical indicators, and financial-statement signals reconstruct to the exact date. A few vendor-sourced signals — `forward_pe`, `short_interest`, ownership percentages, analyst estimates — are populated forward-only, so an older snapshot may return `null` while backfill is in progress. That `null` means "not yet backfilled," not "no value existed."

## News and events don't take asof

`asof` exists because scan, signals, tickers and bars *reconstruct* the row as of a past date. News and events don't need it: each row already carries the real moment it happened (`time_published`, the event `ts`), so "as of a past date" is a `WHERE` filter on that column (or `since`/`until`) — no `asof` parameter, no staleness window. **Logs filter by their timestamp; reconstructed state takes `asof`.**

The two meet on [All events](/docs/endpoints/events/query): `join=state` evaluates ticker-state signals as of each event's timestamp — point-in-time state per event row, at `1d` (reported as `_meta.state_interval`; the old `state_resolution` key remains as a deprecated alias).

## The sandbox is the same endpoint

The no-key `/v2/sandbox/*` as-of routes run the same handlers as the keyed ones — same `interval`, `_meta`, staleness rule, `as_of` stamp, and the same unlimited depth. The only difference is the anonymous per-IP budget: https://tickerbot.io/docs/errors.md
