SQL
Compose signals into a SQL WHERE clause and pass it as q: gap_up AND small_cap AND NOT earnings_this_week. Signals are columns of various types — pre-thresholded flags you name directly (gap_up), numerics and strings you compare (rsi_14 < 30, sector = 'Technology') — built-in and custom alike. The same grammar goes everywhere a query goes: q on /scan, /events, and /news, and the q / event_q fields on /webhooks. What changes per surface is only the row it filters. The one exception is custom-signal expr, which compiles against a stricter subset — see the limitations list below.
One grammar, three projections
Same WHERE clause; the identifiers come from the surface you're querying.
- The ticker row — Scan and webhook
q. Identifiers are schema columns of any type — pre-thresholded flags you name directly, numerics, strings, and dates you compare — plus the qualified context tickers below. - The event row — All events and the webhook
event_qcontent filter. Exactly four identifiers:ticker,ts,kind, andpayload, with jsonb operators for payload fields (payload->>'firm' = 'Goldman Sachs'). Withjoin=stateticker-state columns become legal too, evaluated as of each event's timestamp. - The article row — News. Article columns (
time_published,source, sentiment fields) plus thetkalias for per-ticker rollups.
Sending the query
POST a JSON body. No encoding needed.
Send the query in a JSON body via POST /v2/scan — the same pattern holds on /v2/events and /v2/news. GET with q as a URL parameter also works, but then the query must be URL-encoded: spaces become %20, > becomes %3E, single quotes become %27. Most HTTP clients encode automatically.
# JSON body: the query travels as written, no URL encoding
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"}'Examples
Bullish breakouts on volume
Stocks at or above their 20-day high on at least 2× their normal volume.
curl -X POST "https://api.tickerbot.io/v2/scan" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"q": "at_20d_high AND high_volume_alert"
}'Small-cap gappers, avoiding earnings noise
Small caps gapping up, with no earnings announcement this week.
curl -X POST "https://api.tickerbot.io/v2/scan" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"q": "gap_up AND small_cap AND NOT earnings_this_week"
}'Oversold tech with healthy fundamentals
Oversold technology names that are still profitable.
curl -X POST "https://api.tickerbot.io/v2/scan" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"q": "rsi_oversold AND sector = 'Technology' AND pe_ratio BETWEEN 5 AND 30"
}'New 52-week highs with momentum
Stocks making fresh 52-week highs with strong upward momentum.
curl -X POST "https://api.tickerbot.io/v2/scan" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"q": "at_52w_high AND momentum_strong_up"
}'Intraday range breaks
Names that broke above the prior 15-minute high on heavy volume.
curl -X POST "https://api.tickerbot.io/v2/scan" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"q": "range_break_15m_up AND high_volume_alert"
}'Mega-cap consolidation
Mega-cap stocks coiling for a move: tight Bollinger bands, low volatility.
curl -X POST "https://api.tickerbot.io/v2/scan" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"q": "market_cap > 200000000000 AND atr_percent < 1.5"
}'Sector rotation candidates
Healthcare and energy names crossing above their 50-day average.
curl -X POST "https://api.tickerbot.io/v2/scan" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"q": "sector IN ('Healthcare', 'Energy') AND above_sma_50 AND NOT in_downtrend"
}'Macro-conditioned setup (qualified context tickers)
RSI-oversold names, but only when the S&P 500 is holding above its 50-day average.
curl -X POST "https://api.tickerbot.io/v2/scan" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"q": "rsi_oversold AND spy.price > spy.sma_50"
}'Sector-relative breakout
Tech breakouts that are also outpacing the tech sector ETF on the same day.
curl -X POST "https://api.tickerbot.io/v2/scan" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"q": "at_20d_high AND sector = 'Technology' AND day_change_pct > xlk.day_change_pct"
}'Context tickers (qualified syntax)
Reference the market itself inside a query about individual stocks.
A scan or webhook q normally filters each ticker on its own columns. Sometimes the condition you care about lives on a different symbol: “oversold names, but only while the S&P 500 holds its 50-day.” Writing spy.price > spy.sma_50 pulls those columns from SPY into the same query — no second call, no client-side join, and every clause evaluates against the same instant (including under asof). The qualifier works for these fixed macro and sector ETFs:
| context | what it is |
|---|---|
dia | Dow Jones Industrial Average ETF. |
iwm | Russell 2000 ETF — small-cap proxy. |
qqq | NASDAQ 100 ETF — large-cap tech proxy. |
spy | S&P 500 ETF — broad-market proxy. |
xlb | Materials sector ETF. |
xlc | Communication Services sector ETF. |
xle | Energy sector ETF. |
xlf | Financials sector ETF. |
xli | Industrials sector ETF. |
xlk | Technology sector ETF. |
xlp | Consumer Staples sector ETF. |
xlre | Real Estate sector ETF. |
xlu | Utilities sector ETF. |
xlv | Healthcare sector ETF. |
xly | Consumer Discretionary sector ETF. |
Where SQL doesn’t apply
A few reads ask a narrower question than a WHERE clause.
- Signal matches take
condition, notq: the question is one comparator on one named column, so it's a single bound like?condition=>70(operators> >= = != < <=). Boolean flags need no condition at all. The sameconditionshape appears on ticker- and signal-trigger webhooks. - Series and bars take structured params (
columns,interval,from/to) — you're selecting a grid, not filtering rows. - Tickers reads are keyed by symbol; there's nothing to filter.
What we don’t accept
Valid SQL features we explicitly reject. Avoids surprises when an otherwise-correct WHERE clause returns a 400.
- No subqueries or joins. A query filters one row: the wide ticker row on /v2/scan, the (ticker, ts, kind, payload) event row on /v2/events, the article row on /v2/news. The one sanctioned join is `join=state` on /v2/events — a parameter, not SQL you write.
- No statement composition. `UNION` / `EXCEPT` / `INTERSECT`, `WITH`, `SELECT`, any DDL/DML keyword, semicolons, and SQL comments (`--`, `/* */`) are rejected.
- Functions are whitelisted: aggregates (`count`, `avg`, `sum`, `min`, `max`, `string_agg`, ...) in `select`/`having`, plus scalar helpers (`coalesce`, `round`, `abs`, `lower`, `upper`, `length`, `date_trunc`, `extract`, `now`, ...). Anything else — including window functions (`OVER (...)`, `row_number`, `rank`, `lag`) — is a 400 naming the function.
- Everything else you'd expect works: `LIKE`/`ILIKE`, `IN`, `BETWEEN`, `CASE`, computed expressions (`price / sma_50 > 1.05`), and casts to whitelisted types (`::numeric`, `::date`, `::timestamptz`, ...).
- No double-quoted identifiers. Write columns bare (`price`, not `"price"`); single quotes are for string literals.
- Custom-signal `expr` is stricter still: comparisons, `AND`/`OR`/`NOT`, `IN`, `BETWEEN`, `IS [NOT] NULL`, arithmetic, and only `abs`/`coalesce`/`round`/`least`/`greatest`. No `LIKE`/`ILIKE`, no `CASE`, no `::` casts. An expression that works in a scan `q` can still be rejected with `compile_failed`.