Gotchas
Things that have caught real users.
`BTC` is not bitcoin
Symbols are namespaced by asset class. We don’t auto-resolve.
Passing BTC to a stock endpoint returns the Grayscale Bitcoin Mini Trust ETF (a real US-listed equity with that ticker), not bitcoin spot. If you want the crypto, use X:BTCUSD. Same rule for GOLD (Barrick Gold equity) vs X:XAUUSD (gold spot). See Asset symbols for the full prefix table.
Crypto and FX have no `session_open` / `session_high` / `session_low`
Markets without a US regular session don’t populate session columns.
For equities those columns reflect today's regular session. On a live crypto or FX read they come back null — not 0 — so a filter like session_high > 0 silently drops every one of those symbols. Reach for price and the daily-tier columns instead. Corporate-event flags behave the same way in spirit: earnings_this_week and friends are always false off the equity universe.
Edge-triggered flags stay true all session
One firing, sticky until the next reset.
Flags like sticky_breakout, gap_up, and golden_cross are edge-triggered: they flip true the moment the condition is met and stay true for the rest of the trading session even if the underlying condition reverses. They reset at the start of the next session. If you're asking “is this happening right now,” use the continuous flavor of the flag (e.g. above_yesterday_high); if you're asking “did this happen today,” the edge flag is what you want.
Daily-only signals can’t be pulled at 1m or 1h
SMAs, RSI, MACD, fundamentals: daily snapshots only.
Asking /v2/series?ticker=AAPL&columns=rsi_14&interval=1m returns 400 — Column “rsi_14” is daily-only — request it at interval=1d. Request 1d for any slow-moving signal. The schema page labels each column with the intervals it's stored at; the minute tier is price, day_change_pct, gap_pct, volume, relative_volume, day_vwap, and the position-from-extremes columns.
`as_of` means different things on different endpoints
Live time vs requested date.
On live endpoints as_of is the server time at which the response was assembled. On as-of reads (/tickers/{t}?asof=, scan with asof) it is the moment you asked about instead: that day’s close, or the timestamp you passed. An old as_of on those calls is the answer, not stale data.
ticker beats tickers on /v2/series
Series resolves the collision silently. Events rejects it.
/v2/series accepts ticker= and tickers=; pass both and the singular silently wins, so a stray ticker= shrinks a multi-ticker request to one symbol with no error. Check the response's echoed tickers if counts look low. /v2/events resolved the same collision by precedence once and now returns a 400 instead — pass one spelling or the other.
Numeric literals are fully specified
This is an API; we don’t parse suffixes.
Write 1500000000, not 1.5B. Write 0.05 for 5% on decimal columns like day_change_pct, not 5 — scales vary by column, so check the field description on the schema page.
Cursors are opaque
Don’t parse them, don’t cache them across deploys.
List endpoints return next_cursor. Pass it back on the next request and that's it — as ?cursor= in the query string on GET, or in the JSON body on POST. What the token carries is your resume position, not your query: on most endpoints you resend the same filter params on every page, while /v2/events pins the original filters and rejects a conflicting one with a 400. See Pagination for both models.
kind=signal needs the signal named to use the grammar
A q that works on kind=analyst needs two more params on kind=signal.
The firing log is ~175M rows. A predicate on payload sits above the union that builds it, where no index can reach it — so q and join=state require signal=<flag>, which binds the query to an index before the payload is ever built. Named that way, golden_cross enters across the whole market for a week come back in milliseconds; unnamed, the request is refused with a 400 naming the param to add. transition=enter|exit is an ordinary filter here, not a second requirement.
group_by is not available on this kind: an aggregate has no limit to stop at, so it reads every firing in the window. Filter mode needs none of this. The interval shape of the same facts is signal spans, and flips inline on a value series is transitions_only.
The per-ticker event log carries three kinds — and is deprecated
/v2/tickers/{t}/events is the corporate slice, not the full timeline.
For a ticker's timeline use /v2/events?ticker=X — it carries everything: dividends, splits, insider, analyst actions, plus opt-in signal firings and news. The older per-ticker spelling /v2/tickers/{ticker}/events returns dividends, splits, and insider transactions only (kind=analyst there is a 400), and is deprecated with a 2026-10-31 sunset — don't build anything new on it.
1q rows are keyed by calendar quarter
52/53-week fiscal filers don't end quarters on calendar boundaries.
On /v2/series?interval=1q, t is the calendar quarter of the fiscal period end (2026-Q2) so off-calendar fiscal years still align cross-ticker. Read each row's fiscal_period for the exact period-end date — don't assume it's the calendar quarter's last day. Quarterly columns also can't mix with daily ones in a single request (the grids aren't joinable — 400 by design).
System universe slugs are reserved
`top_10` and `top_100` you don’t own.
When you fetch /v2/universes/top_10 you get a system universe, even though you didn't create one. You can't create a universe with a slug that collides with a system one (slug_taken on POST). You can't delete or edit a system universe either; both come back 403.
Webhook deliveries can fire after delete
Queued deliveries capture target + secret at queue time.
If a webhook is in the middle of an evaluation cycle when you call DELETE /v2/webhooks/{id}, any deliveries already enqueued can still fire. They carry the original target URL and signing secret. To pause without this race, stop responding 2xx at your target and the subscription will flip to disabled after consecutive failures.
DELETE endpoints return 204 No Content
Empty body on success.
DELETE /v2/webhooks/{id} and DELETE /v2/universes/{id} both return 204 No Content on success. There's no JSON to parse; check the status code. Errors (404) still come back with the standard JSON envelope.
Deprecated & sunsetting: five legacy reads end 2026-10-31
Each successor is a strict superset — or, for the as-of alias, the identical response at its canonical URL. Don't build anything new on these.
| Deprecated route | Use instead | What changes |
|---|---|---|
GET / | / | Same data plus multi-ticker alignment, OHLCV columns, 1w/1q intervals, and transitions_only. |
GET / | / | One signal is just a column pick on series — same values, and you can pull several signals in one call. |
GET / | / | The full timeline: adds analyst actions plus opt-in signal firings and news, queryable by payload. |
GET / | / | Same rows from the unified stream; firm and action are real filters there. |
GET / | / | Nothing — it was a second spelling of the as-of read, and the canonical URL returns the byte-identical response. |
All five routes are sunset on 2026-10-31 — they keep serving byte-identical responses until then, carrying Deprecation/Sunset/Link headers that name the successor. On or after that date they may return 410 Gone with a pointer at the successor. Existing callers: migration notes live on Ticker history and Signal history; for the as-of alias the migration is the one-line URL swap above.