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

# Latest stories (cursor-paginated)

`GET /api/stories/`

## Request

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

Stories newest-first by the time of their latest article
(`last_material_at`): stories with a published summary and
`importance >= 7`. Keyset pagination: omit `cursor` for the newest
page, pass `next_cursor` back for the next (older) one; `null` means
the end. Cursors are opaque; an invalid one is a 400. Archive depth
is tiered as on the feeds — paging back past your plan's horizon
(Free 30 days, Basic 90, Pro 180) returns 403 `archive_horizon`.

A story keeps moving up this list while articles keep joining it
(`last_material_at` is the keyset), and its summary is rewritten as
materials arrive, so re-reading a `story_id` later can return a
different summary — `updated_at` says when the facts last changed.

## Query parameters

- `cursor` string
- `page_size` integer

## Response `200`

A page of stories.

- StoriesPage
  - `results` StoryCard[], required
    - `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.
  - `next_cursor` string, nullable, required: Opaque cursor for the next (older) page; `null` at the end.

## Other responses

- `400`: Invalid cursor (opaque; reuse `next_cursor`)
- `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`: The Stories section is switched off.
- `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-09** `8cf7aaea97e3`: 1 info
  - endpoint added

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