---
title: "Get a story by its id"
method: GET
path: "/api/stories/{story_id}/"
tags: ["stories"]
---

# Get a story by its id

`GET /api/stories/{story_id}/`

## Request

- Base URL: `https://api.alphai.io`
- URL: `https://api.alphai.io/api/stories/{story_id}/`
- Auth: HTTP bearer

One story — the summary AlphAI wrote from its articles, the key facts
each with the publisher it was read from and the `source_uid` of that
article, the per-ticker read, the open questions — wrapped in a
lookup `status`, because a `story_id` taken from an article row does
not always name a current story:

* `found` — `story` is filled.
* `merged` — this story was folded into another one; `redirect_to`
  is the surviving story's id. Fetch that one (a web page would 308).
* `not_a_story` — the id belongs to an article that only one
  publisher carried, so there is no story to summarise; `article`
  names it (`/api/news/{uid}/`). An article's `story_id` resolves
  here for every row, so this is the normal answer for most of the
  feed: most events are carried by a single outlet.
* An unknown id is a 404.

`summary_state` tells you how to read the text: `published` (the
summary covers every article the story had when it was written),
`stale` (`new_materials` articles joined since; the rewrite is
queued — the summaries are debounced, so a fast-moving story is
rewritten minutes, not seconds, after each new article), or
`preparing` (no summary yet: `title` is the lead article's headline
and `summary` its collector summary; `key_facts`, `why_it_matters`,
`ticker_takes` and `uncertainties` are empty). A story older than
your plan's archive horizon (by `last_material_at`) is a 403
`archive_horizon`, like a deep feed cursor.

The summary is the model's own text, checked against the articles
before publishing: every figure in it appears in a source, and no
passage of more than a dozen words is copied from one. It is
AI-generated financial information for research, not investment
advice. Article bodies are never served; `materials` link to the
publishers.

## Path parameters

- `story_id` string, required

## Response `200`

The lookup — `found`, `merged` or `not_a_story`.

- StoryLookup: What a story id resolves to. Exactly one of `story`, `redirect_to` and `article` is filled, according to `status`.
  - `status` 'found' | 'merged' | 'not_a_story', required: `found`: `story` is filled. `merged`: this story was folded into another one, `redirect_to` is its id. `not_a_story`: the id belongs to an article only one publisher carried, `article` names it. (An unknown id is a 404, not a status.)
  - `story` Story: A story in a list (`/api/stories/top/`, `/api/stories/`): the summary's title and lead, the story's importance, breadth and timing.
    - `story_id` string, required: Permanent story key — the `uid` of the story's first root article; the same value article rows carry in their `story_id`. Opens `/api/stories/{story_id}/`.
    - `title` string, required: The summary's headline, written by AlphAI from the articles. In `preparing` state, the lead article's own headline.
    - `summary` string, required: One paragraph: what happened, with the figures. In `preparing` state, the lead article's collector summary.
    - `importance` integer, required: How much the event matters, on the feed's 1-10 relevance scale: the median of each publisher's best-scored article on the event, rounded down — so one outlet's 9 does not lift a story every other outlet scored 7. Stories below 7 are not listed and carry no summary.
    - `category` string, required: The feed category of the story's root article (same 14 values as `enrichment.category`).
    - `tickers` string[], required: Active tickers the story's articles are tagged with. Macro stories carry an empty list.
    - `publishers` integer, required: Distinct publishers (source domains) covering the story — always 2 or more.
    - `materials` integer, required: Articles in the story that the feed shows (`relevance_score >= 4`), reprints included.
    - `first_published_at` string, date-time, nullable, required: Publication time of the story's earliest article.
    - `last_material_at` string, date-time, nullable, required: Publication time of the story's latest article; `/api/stories/` is ordered by it.
    - `updated_at` string, date-time, nullable, required: When the story's key facts last changed — a rewrite that only reworded the summary does not move it. The freshness Top stories rank by.
    - `summary_state` 'published' | 'stale' | 'preparing', required: `published`: the summary covers every article the story had when it was written. `stale`: articles joined since; the rewrite is queued. `preparing`: no summary yet — the texts are the lead article's own.
    - `why_it_matters` string, required: Why the event matters to the market, one short paragraph. Empty in `preparing` state.
    - `key_facts` StoryKeyFact[], required: Up to six facts, each attributed to the article it was read from.
      - `text` string, required: The fact as the summary states it; every figure in it appears in the source article.
      - `source_uid` string, required: The `uid` of the article the fact was read from — a row of `/api/stories/{story_id}/materials/`, openable with `/api/news/{uid}/`. Empty when that article has since been removed.
      - `publisher` string, required: The source domain of that article.
    - `ticker_takes` StoryTickerTake[], required: The read per ticker — direction and a one-line note.
      - `ticker` string, required
      - `impact` 'positive' | 'negative' | 'neutral', required: The summary's read of the event for this ticker — an editorial call, not a price label.
      - `note` string, required: One line on why.
    - `uncertainties` string[], required: What the articles leave open or disagree on.
    - `based_on` integer, required: Articles whose text the model read when writing the summary. Shown as "based on N of M sources" next to `materials_at_build`.
    - `materials_at_build` integer, required: The story's visible article count when the summary was written.
    - `new_materials` integer, required: Articles that joined after the summary was written (non-zero only in `stale` state).
    - `revision_generated_at` string, date-time, nullable, required: When the published summary was written; `null` in `preparing` state.
    - `indexable` boolean, required: Whether alphai.io lets search engines index this story's page (a published summary, `importance >= 8` and at least five publishers). Informational — it says nothing about the API's own access.
  - `redirect_to` string, nullable: The surviving story's id, when `status` is `merged`.
  - `article` StoryArticleRef
    - `uid` string, required
    - `title` string, required
    - `time_published` string, date-time, required

## Other responses

- `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.
- `404`: No story or article with that id
- `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.

## Changes

- **2026-10-25** `1fc074b5ae59`: 1 breaking
  - removed the required property `story/allOf[#/components/schemas/Story]/allOf[subschema #2]/model` from the response with the `200` status
- **2026-10-09** `8cf7aaea97e3`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/alphai/apis/alphai-rest-api/changes/api/stories/:story_id/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/7a88b559ccfa?raw)
