Scan articles
/v2/news/scanOne endpoint, two shapes. With no `group_by` you get article rows; with `group_by` you get rollups (counts, averages) grouped by your expressions. Auto-joins `UNNEST(tickers) AS tk` whenever any clause references the `tk` alias — that's how you write per-ticker rollups without unnesting yourself. There is no separate "asof" parameter. Historical queries are just WHERE filters on `time_published`: ``` q=time_published >= '2026-01-15' AND time_published < '2026-01-16' q=time_published >= NOW() - INTERVAL '7 days' ``` The grammar is the same SQL whitelist used by `/v2/scan`: comparison and logical operators, casts (`::numeric`, `::date`, …), array operators (`= ANY(tickers)`, `tickers && ARRAY[…]`), JSONB operators (`->`, `->>`, `@>`), and the scalar/aggregate functions listed below. Subqueries and DDL keywords are rejected. **Columns available** on `news_article` (and as `select` / `group_by` items): `id`, `url`, `time_published`, `title`, `summary`, `source`, `source_domain`, `category`, `authors`, `topics`, `banner_image`, `overall_sentiment_score`, `overall_sentiment_label`, `tickers` (text[]), `ticker_data` (jsonb), `created_at`. Plus the synthetic `tk` alias — when referenced, the server auto-joins `UNNEST(tickers) AS tk`. **Aggregates**: `COUNT`, `AVG`, `SUM`, `MIN`, `MAX`, `STRING_AGG`. **Scalar funcs**: `COALESCE`, `NULLIF`, `ROUND`, `GREATEST`, `LEAST`, `TANH`, `ABS`, `DATE_TRUNC`, `EXTRACT`, `NOW`, `LOWER`, `UPPER`, `LENGTH`, `JSONB_ARRAY_LENGTH`. Pagination is opaque cursor — pass `next_cursor` back as `?cursor=…`. For aggregate queries, alias every `group_by` expression so the cursor can resolve the next page (e.g. `group_by=time_published::date AS day`).
Plan access
Pro and above. Hobby + Free return 403 news_tier_required for the archive — the live `news_volume` and `news_volume_weighted_sentiment` columns on the ticker object remain available on every paid plan.
Rate limit
Hobby 60/min · Pro 2,000/min · Scale 10,000/min.
Per-call cap
Pro: 200 rows. Scale/Enterprise: 1000. History back to 2015.
Query parameters
stringrequiredWHERE clause. Required. 4000-char max.
stringComma-separated SELECT items. Default for article queries: `id, time_published, title, source, tickers, overall_sentiment_score, overall_sentiment_label`. Default for aggregate queries: the group_by keys + `COUNT(*) AS volume`.
stringComma-separated grouping expressions. Switches the response shape to aggregate rows. Alias each expression for stable cursor pagination.
stringHAVING clause, applied post-aggregation. Requires `group_by`.
stringColumn or alias to sort by. Defaults to `time_published` (article rows) or `volume` (aggregate rows).
string`asc` or `desc`. Default `desc`.
integerRows per page. Per-plan max: Pro 200, Scale/Enterprise 1000. Default 50.
stringOpaque pagination cursor from a previous response.
Status codes
200results array of article rows or aggregate rows depending on group_by. Always carries as_of, count, next_cursor, and an echo of the parsed query.400bad_request / invalid_query — missing q, malformed parameter, disallowed identifier, or SQL parse error from Postgres.403news_tier_required — archive access requires Pro or above.