---
title: "List news (cursor-paginated)"
method: GET
path: "/api/news/"
tags: ["news"]
---

# List news (cursor-paginated)

`GET /api/news/`

Feed of enriched articles, newest first. Default filter:
`relevance_score >= 4` AND at least one active ticker. Pagination is
cursor-based with a default page size of 10 (any size from 1 to 20 via
`page_size=`; Pro keys go up to 50): omit `cursor` for the newest page,
then pass the
`next_cursor` from each response to fetch the next (older) page.
`next_cursor: null` means the end of the feed. Cursors are opaque — do
not construct or parse them; an invalid cursor returns 400. Archive
depth is tiered: paging back past your plan's horizon (Free 30 days,
Basic 90, Pro 180) returns 403 with an upgrade hint.

**Delta polling** (`sort=ingested`): the same feed ordered by the
moment rows became available, for "what is new since my last poll".
Articles reach the feed later than their publish time (collection
median ~30 min for general news, with a long tail), so a poller that
filters by `time_published` misses most late arrivals; `sort=ingested`
never misses a row. First call without `cursor` returns the newest
`page_size` rows and a cursor at the feed head; each later call with
the previous `next_cursor` returns only rows added since. In this mode
`next_cursor` is always non-null — empty `results` means you are
caught up, keep the cursor and poll again later (responses are cached
for 60 s, so polling more often than once a minute buys nothing). New
rows only: an update to an already-delivered article is not re-sent.
`time_published` is not monotonic within a delta page; sort client-side
if you need chronological order. A cursor is only valid with the sort
mode that issued it.

**Keeping up.** One call returns at most `page_size` rows, so a poller
holds its position at the head only while `calls per day × page_size`
stays above the daily volume of the stream it asked for. Below that it
falls a little further behind every day, and the symptom misleads:
`time_published` reads hours or days old while the data is current.
What went stale is the cursor, not the feed. So drain instead of
polling once per tick: when a page comes back with `results` filled,
call again immediately, and sleep only once `results` is empty. That
clears a burst within the same cycle and lets a poller catch up by
itself after downtime. Two levers if the call budget is still short,
and they multiply: `page_size` (up to 20, or 50 on a Pro key) and a
narrower stream (`min_relevance`, `symbol`, `category`). For scale, at
the default `>= 4` floor the feed carries roughly 2,700 rows a day,
while `min_relevance=7` carries roughly 1,000.

A poller that never catches up eventually meets the archive gate: the
403 below is keyed on the age of the first row you have not read yet,
so it fires on a delta poller that never paged back at all.

## Query parameters

- `cursor` string
- `symbol` string
- `category` NewsCategory[]
- `exclude_categories` NewsCategory[]
- `min_relevance` integer
- `page_size` integer
- `collapse` 'story'
- `sort` 'published' | 'ingested'

## Response `200`

A page of enriched articles.

- NewsPagination
  - `results` RichNewsArticle[], required
    - `original` OriginalArticle, required — The article as fetched from the source. `raw_text` is intentionally NOT exposed here.
      - `id` integer
      - `uid` string, required
      - `title` string, required
      - `url` string, uri, required
      - `time_published` string, date-time, required
      - `authors` string[]
      - `summary` string, required — AI-generated summary safe to redistribute.
      - `banner_image` string, uri, nullable
      - `source` string, required
      - `source_domain` string
      - `topics` Topic[]
        - `topic` string, required
        - `relevance` number, float, required
      - `tickers_sentiment` object[]
      - `ownership_form` 'direct' | 'indirect', nullable — SEC Form 4 insider rows only — the holding pool the transaction touched. `null` for non-insider news. A single Form 4 can surface a `direct` and an `indirect` leg of the same plan as two separate articles (same filing URL, same date); they are distinct economic events, so SUM them rather than dedupe by URL/accession.
      - `created_at` string, date-time — When AlphaAI received the article — NOT its publish time (that is `time_published`). Use it to judge how fresh a pickup is. Do not build "what's new" polling on this field; that is what `sort=ingested` on `/api/news/` and `/api/news/insider/` is for.
      - `updated_at` string, date-time — Last time the stored article row was touched (including internal maintenance). Informational only.
    - `enrichment` EnrichedArticle, required
      - `category` 'earnings' | 'mergers_acquisitions' | 'regulation' | 'macro_economy' | 'sector_analysis' | 'market_movers' | 'technology' | 'commodities' | 'crypto' | 'ipo' | 'geopolitics' | 'insider' | 'corporate_actions' | 'other' — `market_movers` is for articles whose subject IS a notable price move ("AMD up 5% today"); `sector_analysis` is genuine sector-level analysis; `insider` covers SEC Form 4 insider transactions only. SEC 8-K filings categorize by their primary item: an earnings release (Item 2.02) is `earnings`, a completed acquisition or disposition (Item 2.01) is `mergers_acquisitions`, and the remaining events (material agreements, debt, executive changes, annual-meeting results) are `corporate_actions`.
      - `tickers` string[] — Validated tickers the article mentions — only symbols present in `/api/symbols/` survive enrichment-time verification against the article text. Mirrors `ai_trading_insights.ticker_analysis[].ticker`.
      - `relevance_score` integer — How much trading value the article itself carries (rates the article, not the company; deterministic — same article, same score): 1–2 no trading relevance · 3–4 derivative content about already-known events · 5–6 macro/sector datapoints, minor-but-real company news · 7–8 real company news with a fresh catalyst · 9–10 primary, material, newly disclosed. SEC Form 4 rows are scored from the transaction itself (size, buy vs. sell, 10b5-1 plan or not) rather than by the model.
      - `ai_trading_insights` AITradingInsights
        - `ticker_analysis` TickerAnalysis[]
          - `ticker` string
          - `relevance_context` string
          - `impact_analysis` ImpactAnalysis
            - `summary` string
            - `sentiment` 'positive' | 'neutral' | 'negative'
            - `price_impact_prediction` string
            - `confidence` 'high' | 'medium' | 'low'
            - `reasoning` string
        - `news_trading_value` NewsTradingValue
          - `actionability_score` 'high' | 'medium' | 'low' | 'negligible'
          - `information_novelty` integer — How much NEW information the article carries (1–10), kept separate from relevance: a mega-cap post-earnings recap is high relevance but low novelty. 0 on rows enriched before the field existed.
          - `timing_relevance` string
          - `market_sentiment_alignment` string
          - `estimated_read_time` string
        - `indirect_market_effects` IndirectMarketEffects
          - `sector_implications` string
          - `regional_market_impact` string
          - `global_market_relevance` string
        - `alternative_perspectives` AlternativePerspectives
          - `contrarian_view` string
          - `overlooked_factors` string
      - `news_context_enhancement` NewsContextEnhancement
        - `background_context` string
        - `impact_analysis` string
        - `key_entities` KeyEntity[]
          - `name` string
          - `type` string
          - `description` string
        - `market_relevance_summary` string
        - `estimated_read_time_minutes` integer
    - `story_id` string, nullable — Populated whenever the response is story-collapsed — `?collapse=story` on `/api/news/`, and always on `/api/news/trending/`. The `uid` of this story's representative article — equal to this item's own `original.uid`, and resolvable via `/api/news/{uid}/`. `null` in the default (uncollapsed) feed.
    - `sources_count` integer, nullable — Story-collapsed responses only. Number of distinct outlets (source domains) covering this story; the same outlet running it more than once counts once. Most stories are carried by a single outlet, so this is usually 1. Treat a value above 1 as the signal, not the number itself. May exceed the length of `sources`, which is capped at 10. `null` in the default feed.
    - `sources` string[], nullable — Story-collapsed responses only. Distinct source domains covering this story, in first-appearance order, capped at 10. `null` in the default feed.
    - `insider` InsiderEvent — Structured SEC Form 4 event: the aggregate of the news row's whole transaction group (one row fronts a filing's non-derivative trades of one type and holding form, so a 10b5-1 ladder is ONE event). `shares` and `total_value_usd` are group sums; `avg_price_usd` is the value-weighted average over priced tranches. Money and share fields are decimal STRINGS to preserve precision.
      - `side` 'buy' | 'sell' | 'other', required — Signal label from the transaction code: `buy` (P, open-market purchase), `sell` (S, open-market sale), `other` for everything else — including D (sale to the issuer: a buyback/redemption, not an open-market disposition). Use `transaction_code` for your own mapping.
      - `transaction_code` string, required — Raw SEC Form 4 transaction code (`P`, `S`, `D`, …).
      - `shares` string, required — Total shares across the event's tranches (decimal string).
      - `avg_price_usd` string, nullable — Value-weighted average price per share over priced tranches. `null` when the filing prices no tranche.
      - `total_value_usd` string, nullable — Total USD value across priced tranches (a lower bound when some tranches are unpriced). `null` when no tranche is priced.
      - `is_10b5_1` boolean, required — True when any tranche executed under a pre-arranged 10b5-1 plan.
      - `insider_name` string, required
      - `insider_title` string, required
      - `is_officer` boolean, required
      - `is_director` boolean, required
      - `is_ten_percent_owner` boolean, required
      - `transaction_date` string, date, required — Date of the group's last fill (a ladder can span days).
      - `filed_at` string, date-time, required — When EDGAR accepted the filing (UTC). Compare against `transaction_date` for your own lateness rule.
      - `late_filing` boolean, required — The filing missed the SEC's two-business-day deadline (Rule 16a-3(g)). Computed on Eastern dates, since EDGAR accepts filings until ~22:00 ET, and with one weekday of slack so a trade in a holiday week is not flagged: true when more than three weekday-days separate `transaction_date` from the filing. About 3% of events carry it; the long tail is catch-up filings covering trades from years earlier.
  - `next_cursor` string, nullable, required — Opaque cursor for the next page. Pass it back as `?cursor=` with the same `sort` mode. Default (`sort=published`) feed: the next older page, `null` when the end of the feed has been reached. Delta mode (`sort=ingested`): always non-null — it is your polling position; empty `results` means caught up, keep the cursor and poll again later.

## Other responses

- `400` — Invalid cursor (opaque; reuse `next_cursor`)
- `401` — Missing or invalid API key.
- `403` — The requested `cursor` points past your plan's news-archive horizon (Free 30 days, Basic 90, Pro 180). The body's `extra` carries `reason: archive_horizon`, your `tier`, the plan's `archive_days`, and (below Pro) an `upgrade` block with the higher tiers' caps and the pricing URL. First pages and cursors within the horizon are unaffected.
- `429` — Rate limit exceeded — either the per-minute burst cap or the per-day volume cap. The `Retry-After` header tells you how long to wait (a burst block is short, ≤60s; a day-cap block is capped at 3600s — the true reset is `X-RateLimit-Reset`). The `X-RateLimit-*` trio shows the daily volume budget. The body's `extra` names your tier, its `limit_per_minute` / `limit_per_day`, `retry_after_seconds`, and — below Pro — an `upgrade` block with the higher tiers' caps and the pricing URL.

---

[API](https://skmtc.net/alphai/apis/alphai-rest-api.md) · [All operations](https://skmtc.net/alphai/apis/alphai-rest-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/alphai/alphai-rest-api/versions/2f34f38bad24/schema)
