---
title: "Full-text search over the feed"
method: GET
path: "/api/news/search/"
tags: ["news"]
---

# Full-text search over the feed

`GET /api/news/search/`

Search the enriched feed by words. `q` is matched against each
article's title, its summary and the entity names the enrichment
extracted; the page comes back ranked (best match first, newest first
among equal matches), with a per-item `search_match` and a `query`
block that says how the words were read. `symbol`, `category`,
`source_type`, `item`, `min_relevance`, `from_date`, `to_date` and
`collapse` apply as hard filters on top, exactly as on `/api/news/`:
`q=director&item=5.02` searches 8-K filings that carry item 5.02,
`q="going concern"&source_type=gdelt` searches press coverage only.

**Query syntax.** Bare words are all required (`chip tariffs`); a
quoted phrase must appear in order (`"going concern"`); `-word`
excludes (`apple -iphone`); `OR` separates alternatives
(`nvidia OR amd`). Words in a non-Latin script are ignored. Matching
tolerates one typo from five letters (`prices` reaches `price`) and a
prefix on the last word; there is no stemming.

**Modes** (`query.mode`). `strict`: every word matched. `broadened`:
the strict pass found fewer than 5 rows and the query has three or
more words, so the most frequent words were dropped and the page
carries rows with at least two of the query's words —
`search_match.terms_matched` says how many on each; read these as
leads, not matches. `no_match`: nothing in the searched window.
`no_terms`: the query held no searchable words. Absence is not proof
the event did not happen: the searched window starts at
`query.window_from`.

**Window and paging.** The searched window is your plan's archive
horizon (Free 30 days, Basic 90, Pro 180) unless `from_date` /
`to_date` narrow it; a window reaching past the horizon returns 403
like the feed. Results are bounded to the best 200 matches (`matched`
counts them, capped at 200) and paged with an opaque `cursor` that is
bound to the query that issued it. Responses are private and never
shared-cached.

**Failure.** When the search service does not answer in time the
endpoint returns 503 with `extra.reason = "search_unavailable"`. The
structured feed and every other endpoint are unaffected: retry the
search later rather than the whole chain.

## Query parameters

- `q` string, required
- `cursor` string
- `symbol` string
- `category` NewsCategory[]
- `source_type` string[]
- `item` string
- `min_relevance` integer
- `page_size` integer
- `collapse` 'story'
- `from_date` string, date-time
- `to_date` string, date-time

## Response `200`

A page of ranked matches.

- NewsSearchPage
  - `results` RichNewsArticle[], required — Feed items in rank order, each with `search_match`.
    - `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[] — Article authors. On SEC Form 4 rows: every reporting owner of the filing, primary first — a joint filing by a holding vehicle and the person behind it lists both.
      - `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 AlphAI 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, and since 2026-09-17 securities offerings, Regulation FD disclosures and other events under Items 7.01/8.01) are `corporate_actions`. A foreign private issuer's SEC 6-K appears only when it is an earnings release (`source` "SEC EDGAR 6-K") and is always `earnings`.
      - `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
    - `search_match` SearchMatch — How an article matched a `/api/news/search/` query.
      - `score` number — The engine's ranking score for this row. Comparable within one response only, never across queries or engine versions.
      - `terms_matched` integer, nullable — In `broadened` mode, how many of the query's words this row carries (at least two); `null` in `strict` mode, where every word matched.
      - `context` string, nullable — A fragment of the summary around the matched words, each wrapped in `**…**`; `null` when the match lies in the title or the entity names only.
    - `earnings` EarningsReport — AlphAI's own structured analysis of an earnings release (SEC Form 8-K item 2.02, or Form 6-K for a foreign private issuer — half-year and full-year periods, IFRS and non-USD figures copied as reported). Every figure was cross-checked against the filing text before storage; consensus estimates and price targets are omitted because they are not in the filing. MCP `alphai_article` returns the structured read; connector `fetch` includes its metrics, comparisons, segments, guidance and limitations in the citation-ready `text` when a completed read exists.
      - `company` string, required
      - `ticker` string, required
      - `fiscal_period` string, required
      - `period_end` string, nullable
      - `headline` string, required
      - `verdict` 'strong' | 'solid' | 'mixed' | 'weak', required
      - `verdict_reason` string
      - `key_metrics` object[], required
        - `name` string
        - `value` string — The figure exactly as the filing printed it — this is the string that was verified against the document, so a table in millions arrives as `$19,345` and a press-release line as `$6.76 billion`. Use the three companions below to compute.
        - `basis` 'GAAP' | 'non-GAAP' | 'other'
        - `prior_year` string, nullable
        - `prior_quarter` string, nullable
        - `yoy_change` string, nullable
        - `qoq_change` string, nullable
        - `numeric` number, nullable — `value` as a number at the face scale it was printed, sign applied (`$(5,396)` → -5396). Null when `value` carries no figure. The amount in base units is `numeric` × the `scale` multiplier (thousands 1e3, millions 1e6, billions 1e9).
        - `unit` string, nullable — ISO-style currency code (`USD`, `EUR`, `TWD`, `CNY`, …), `pct` for a percentage, `bp` for basis points, null for a plain count (shares, units, multiples).
        - `scale` 'ones' | 'thousands' | 'millions' | 'billions' | 'trillions', nullable — The multiplier `numeric` is expressed in. Filled only when the printed value names it (`$6.76 billion`, `$60,801 (In millions)`), the metric name names it, or the filing's own table header names one scale for its tables; per-share and percentage figures are `ones`. Null means the filing did not say — treat `numeric` as unscaled at your own risk rather than assume millions. Added 2026-09-12; reads stored earlier carry the companions after the backfill of the same day.
      - `segments` object[]
        - `name` string
        - `revenue` string
        - `yoy_change` string, nullable
        - `qoq_change` string, nullable
        - `driver` string
      - `guidance` object, nullable
        - `period` string
        - `revenue` string, nullable
        - `gross_margin` string, nullable
        - `operating_expenses` string, nullable
        - `tax_rate` string, nullable
        - `other` string[]
      - `vs_prior_guidance` object[] — Reported figure vs the company's own prior outlook, when available.
        - `metric` string
        - `prior_guidance` string
        - `actual` string
        - `verdict` 'above' | 'in line' | 'below' | 'n/a'
      - `capital_returns` string[]
      - `balance_sheet_cash_flow` string[]
      - `drivers` string[]
      - `concerns` string[]
      - `what_to_watch` string[]
      - `quotes` object[]
        - `speaker` string
        - `role` string, nullable
        - `text` string
      - `analysis` string, required — Several paragraphs of grounded analysis.
      - `missing_items` string[] — What the filing did NOT state, named rather than guessed.
      - `numbers_verified_from_document` boolean
    - `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 the story's ROOT article — the same value for a given story on every collapsing surface (this feed, trending, and the MCP tools) and across calls, so it works as a stable story key. On `?collapse=story` the root is the row itself, so it equals this item's own `original.uid`; on `/api/news/trending/` the item is the story's strongest-ranked member, which may differ from the root. Either way it resolves 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. Filterable on this endpoint: `?is_10b5_1=false` keeps only discretionary events.
      - `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.
    - `filing` EightKFiling — Structured SEC 8-K filing: what the filing itself says, next to the AI read of it. One news row fronts one filing.
      - `items` string[], required — Every item code the filing carries, in the filing's order (`["5.02", "9.01"]`). `?item=` matches any of them. Empty for a filing SEC served no document for (header-only).
      - `primary_item` string, nullable — The item that drove the row's `category` (`2.02` → `earnings`, `2.01` → `mergers_acquisitions`, the rest → `corporate_actions`). `null` when the filing lists no items.
      - `accession_number` string, required — SEC accession number of the filing — the id to cite.
      - `filed_at` string, date-time, required — When EDGAR accepted the filing (UTC).
      - `event_date` string, date, nullable — The filing's own "date of report" — the date of the event it reports, which can precede `filed_at` by up to four business days. `null` when the header carries none.
      - `exhibit_url` string, uri, nullable — The press-release exhibit (EX-99.x) the summary was built from, on sec.gov. `null` when the filing has no such exhibit (the summary then reads the cover document).
  - `next_cursor` string, nullable — Pass back as `cursor` for the next page; `null` past the 200-match bound.
  - `count` integer, required — Items on this page.
  - `matched` integer, required — Visible matches in the bounded candidate set (at most 200); never a global total.
  - `query` NewsSearchQueryInfo, required — How `/api/news/search/` read the query.
    - `mode` 'strict' | 'broadened' | 'no_match' | 'no_terms', required — See the endpoint description.
    - `sort` 'relevance' | 'published', required — `relevance` (ranked) or `published` (newest first) — the latter when every word is too common to rank on.
    - `terms` string[], required — The normalized words that were searched.
    - `required_terms` string[] — In `broadened` mode, the words at least one of which every row carries, when the search reports them; empty otherwise (the current engine does not — `search_match.terms_matched` on each item is the count to trust).
    - `optional_terms` string[]
    - `window_from` string, date-time, nullable — Start of the searched window (your archive horizon, or `from_date` if later).
    - `note` string — One sentence for an agent to act on — what selected the rows, and what they are not.

## Other responses

- `400` — `q` missing or outside 2-200 characters, an invalid cursor or one issued for another query, a `page_size` outside 1-50 or not permitted on this tier, `from_date` after `to_date`, an unknown `source_type`, a malformed `item` or one combined with a non-8-K `source_type`, or an unsupported `collapse` value.
- `401` — Missing or invalid API key.
- `403` — The request reaches past your plan's news-archive horizon (Free 30 days, Basic 90, Pro 180) — either the `cursor` points beyond it, or `from_date` does. 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. The window is checked on the FIRST page, unlike the cursor: a `from_date` older than the horizon is refused immediately rather than after paging down to it. A window with only `to_date` is refused when that bound itself is past the horizon (the window is open-ended into the past). Windows and cursors inside the horizon are unaffected, and the request is refused rather than trimmed to the horizon, so a given URL always returns the same body.
- `429` — Rate limit exceeded — either the per-minute burst cap or the per-day volume cap. The `Retry-After` header is the earliest second a retry can succeed, not a guess (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. A rejected request does not itself consume quota, so retrying after `Retry-After` costs you nothing extra.
- `503` — The search service did not answer in time. The body carries `extra.reason = "search_unavailable"`; the feed endpoints are unaffected.

## Changes

- **2026-09-24** `44fde16b286e` — 1 info
  - endpoint added

[Change history](https://skmtc.dev/alphai/apis/alphai-rest-api/changes/api/news/search/get.md)

---

[API](https://skmtc.dev/alphai/apis/alphai-rest-api.md) · [All operations](https://skmtc.dev/alphai/apis/alphai-rest-api/llms.txt) · [OpenAPI document](https://skmtc.dev/alphai/apis/alphai-rest-api/revisions/8bc11a8dc9ca?raw)
