AlphaAI Stories API

Market events covered by two or more publishers, summarised from their sources.

Operations 4

GET /api/stories/top/ Top stories (last 48h) #
GET /api/stories/ Latest stories (cursor-paginated) #
GET /api/stories/{story_id}/ Get a story by its id #
GET /api/stories/{story_id}/materials/ A story's articles (cursor-paginated) #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/alphaai:alphaai-stories-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

alphaai-stories-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: AlphAI REST Stories API
  version: 1.52.0
  description: 'Public REST API for AlphAI''s relevance-scored, ticker-linked financial news. All endpoints accept `Authorization: Bearer ak_live_…` (issued from your

    `/account/api-keys`). Authenticated traffic is metered per account with two

    layers — a per-minute burst cap and a per-day volume cap (Free 20/min +

    100/day, Basic 60/min + 10,000/day, Pro 150/min + 100,000/day); a request

    passes only if both are under budget. The Free tier is for evaluation /

    personal non-commercial use; Basic is licensed for internal commercial use;

    Pro adds redistribution of the enriched feed. Send all API traffic to

    `api.alphai.io`; a key is required on every request. The API is server-side only. It sends no CORS headers, so a cross-origin

    call from a browser page fails at the preflight: the `OPTIONS` request is

    answered with `403` at the edge and never reaches the API. Keep the key on

    your server and expose your own route to the browser; a key embedded in

    page code is readable by every visitor and spends your quota. Every keyed response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`

    and `X-RateLimit-Reset`; these report the per-day volume layer (Reset =

    epoch seconds of the next 00:00 UTC reset) so you can watch your daily budget

    without provoking a 429. A per-minute burst surfaces only as a 429 with a

    short `Retry-After`. A 429 body also names your tier, its caps and an

    `upgrade` block. A request rejected with 429 does not itself spend quota. The headers reflect origin processing, so a response served from a shared

    cache may omit them. On the MCP server (`mcp.alphai.io`) the same two layers meter **tool calls

    only** — the connect handshake (`initialize`, `tools/list`, notifications,

    keepalives, the SSE stream) is protocol traffic and does not consume your

    quota. It carries a separate, far larger ceiling that exists only to stop a

    client reconnecting in a loop; hitting it returns a 429 whose body reports

    `"window": "transport"`, which means your client is reconnecting too often,

    not that you are out of quota. MCP news tools accept `detail="compact"` for headline triage and polling:

    `alphai_news_search`, `alphai_ticker_news`, `alphai_insider_news`,

    `alphai_trending`, `alphai_actionable_now`, `alphai_macro` and

    `alphai_pair_analysis`. Compact items keep identity, source, publication

    and ingest times, tickers, category, relevance_score, top-level

    actionability and information_novelty, story fields, ownership_form and

    search_match. Summary, analysis, images and SEC detail blocks are omitted;

    fetch `alphai_article(uid)` for those details. `detail="full"` is the

    default and preserves the existing format, including the condensed recent

    lists on pair_analysis. Selection, order, cursors, query interpretation

    and macro releases are unchanged, so detail can change between pages. This parameter belongs to MCP tools only; REST parameters are unchanged. Interactive MCP clients use a separate OAuth approval flow. Check the

    callback host or app scheme and full callback address before approving;

    app names are self-declared, not verified by AlphAI. MCP access includes

    news, market data and the saved watchlist, plus creating, changing and

    disabling the user''s own alerts on Basic or Pro (including after an

    upgrade). The OAuth scope `tools.read` is not a read-only restriction. Registered callbacks can be reviewed in Account → MCP connections. That account management surface requires website login, not an API key. Disconnect blocks all existing MCP OAuth access and refresh tokens for that

    app connection, including its other sessions; work already in progress may

    finish. Other connections and API keys are unaffected. Revoking either token

    type through the separate MCP host''s `/oauth/revoke` has the same effect. Serialize refresh requests and save each replacement token: reuse ends the

    connection.'
  contact:
    name: alphai support
    email: support@alphai.io
    url: https://alphai.io/contact
  license:
    name: Proprietary
servers:
- url: https://api.alphai.io
  description: Production (API host — key required)
security:
- apiKey: []
tags:
- name: Stories
  description: Market events covered by two or more publishers, summarised from their sources.
paths:
  /api/stories/top/:
    get:
      tags:
      - Stories
      summary: Top stories (last 48h)
      description: 'The strongest stories of the trailing 48 hours, ranked: a story''s

        `importance` plus a coverage bonus of `min(3, log2(publishers))`,

        minus one point per 12 hours since its key facts last changed

        (`updated_at`). One slot per ticker and two per no-ticker macro

        category, so one event cannot fill the list. Only stories with a

        published summary and `importance >= 7` qualify. Fixed size of up to

        50, no pagination; `ranking_version` names the formula so a change

        is visible, `as_of` is when the list was ranked.


        Compared with `/api/news/trending/`: trending ranks single articles

        and collapses reprints; this ranks the stories themselves (two or

        more publishers), each with an AlphAI-written summary. A story''s

        `story_id` is the same key an article row carries, so a trending

        item''s `story_id` opens the story here.'
      responses:
        '200':
          description: Ranked stories, strongest first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TopStories'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: The Stories section is switched off.
        '429':
          $ref: '#/components/responses/RateLimited'
      operationId: getApiStoriesTop
      x-operation-id-source: derived
  /api/stories/:
    get:
      tags:
      - Stories
      summary: Latest stories (cursor-paginated)
      description: '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.'
      parameters:
      - in: query
        name: cursor
        description: Opaque cursor from a prior response's `next_cursor`. Omit for the newest page.
        schema:
          type: string
      - in: query
        name: page_size
        description: Stories per page, 1 to 50 on every tier (default 20). `limit` is accepted as an alias.
        schema:
          type: integer
          minimum: 1
          maximum: 50
          default: 20
      responses:
        '200':
          description: A page of stories.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StoriesPage'
        '400':
          description: Invalid cursor (opaque; reuse `next_cursor`)
          a `page_size` outside 1-50: null
          or an unknown parameter (the body's `extra.allowed_params` lists the accepted ones).: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ArchiveGated'
        '404':
          description: The Stories section is switched off.
        '429':
          $ref: '#/components/responses/RateLimited'
      operationId: getApiStories
      x-operation-id-source: derived
  /api/stories/{story_id}/:
    get:
      tags:
      - Stories
      summary: Get a story by its id
      description: '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.'
      parameters:
      - in: path
        name: story_id
        required: true
        description: A story id — the `story_id` of any article row, or an id from `/api/stories/`.
        schema:
          type: string
          pattern: ^[a-f0-9]{16}$
      responses:
        '200':
          description: The lookup — `found`, `merged` or `not_a_story`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StoryLookup'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ArchiveGated'
        '404':
          description: No story or article with that id
          or the Stories section is switched off.: null
        '429':
          $ref: '#/components/responses/RateLimited'
      operationId: getApiStoriesByStoryId
      x-operation-id-source: derived
  /api/stories/{story_id}/materials/:
    get:
      tags:
      - Stories
      summary: A story's articles (cursor-paginated)
      description: 'Every article behind a story, oldest first — the materials the

        summary was written from and the ones that joined after it. Same

        visibility as the feed (`relevance_score >= 4`); each row carries its

        `uid` (open it with `/api/news/{uid}/`), publisher, publication time,

        the publisher''s URL and its relevance score. A story''s `key_facts`

        point into this list by `source_uid`. Keyset pagination with

        `cursor` / `next_cursor` as on `/api/stories/`; most stories fit one

        page. Only a current story (`status: found`) has materials: a merged

        or not-a-story id is a 404 here — resolve it through

        `/api/stories/{story_id}/` first.'
      parameters:
      - in: path
        name: story_id
        required: true
        schema:
          type: string
          pattern: ^[a-f0-9]{16}$
      - in: query
        name: cursor
        description: Opaque cursor from a prior response's `next_cursor`. Omit for the first (oldest) page.
        schema:
          type: string
      - in: query
        name: page_size
        description: Articles per page, 1 to 50 on every tier (default 50). `limit` is accepted as an alias.
        schema:
          type: integer
          minimum: 1
          maximum: 50
          default: 50
      responses:
        '200':
          description: A page of the story's articles, oldest first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StoryMaterials'
        '400':
          description: Invalid cursor
          a `page_size` outside 1-50: null
          or an unknown parameter.: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No current story with that id (merged and single-publisher ids included)
          or the Stories section is switched off.: null
        '429':
          $ref: '#/components/responses/RateLimited'
      operationId: getApiStoriesByStoryIdMaterials
      x-operation-id-source: derived
components:
  schemas:
    StoryMaterials:
      type: object
      required:
      - story_id
      - results
      - next_cursor
      properties:
        story_id:
          type: string
        results:
          type: array
          items:
            $ref: '#/components/schemas/StoryMaterial'
        next_cursor:
          type: string
          nullable: true
          description: Opaque cursor for the next (newer) page; `null` at the end.
    StoryTickerTake:
      type: object
      required:
      - ticker
      - impact
      - note
      properties:
        ticker:
          type: string
          example: NVDA
        impact:
          type: string
          enum:
          - positive
          - negative
          - neutral
          description: The summary's read of the event for this ticker — an editorial call, not a price label.
        note:
          type: string
          description: One line on why.
    StoryCard:
      type: object
      description: 'A story in a list (`/api/stories/top/`, `/api/stories/`): the summary''s title and lead, the story''s importance, breadth and timing.'
      required:
      - story_id
      - title
      - summary
      - importance
      - category
      - tickers
      - publishers
      - materials
      - first_published_at
      - last_material_at
      - updated_at
      - summary_state
      properties:
        story_id:
          type: string
          description: 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}/`.
          example: 788e477c66f3849b
        title:
          type: string
          description: The summary's headline, written by AlphAI from the articles. In `preparing` state, the lead article's own headline.
        summary:
          type: string
          description: 'One paragraph: what happened, with the figures. In `preparing` state, the lead article''s collector summary.'
        importance:
          type: integer
          minimum: 1
          maximum: 10
          description: '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:
          type: string
          description: The feed category of the story's root article (same 14 values as `enrichment.category`).
        tickers:
          type: array
          items:
            type: string
          description: Active tickers the story's articles are tagged with. Macro stories carry an empty list.
        publishers:
          type: integer
          description: Distinct publishers (source domains) covering the story — always 2 or more.
        materials:
          type: integer
          description: Articles in the story that the feed shows (`relevance_score >= 4`), reprints included.
        first_published_at:
          type: string
          format: date-time
          nullable: true
          description: Publication time of the story's earliest article.
        last_material_at:
          type: string
          format: date-time
          nullable: true
          description: Publication time of the story's latest article; `/api/stories/` is ordered by it.
        updated_at:
          type: string
          format: date-time
          nullable: true
          description: 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:
          type: string
          enum:
          - published
          - stale
          - preparing
          description: '`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.'
    TopStories:
      type: object
      required:
      - results
      - as_of
      - ranking_version
      properties:
        results:
          type: array
          maxItems: 50
          items:
            $ref: '#/components/schemas/StoryCard'
        as_of:
          type: string
          format: date-time
          description: When the list was ranked.
        ranking_version:
          type: integer
          description: The ranking formula's version; a change to the formula increments it.
          example: 1
    Error:
      type: object
      properties:
        message:
          type: string
        error:
          type: string
        detail:
          type: string
        extra:
          type: object
          description: 'Machine-readable context on capped responses: the 429 carries `tier`, `limit_per_minute`, `limit_per_day`, `retry_after_seconds`; the archive 403 carries `reason: archive_horizon`, `tier`, `archive_days`. Both include an `upgrade` object (higher tiers'' caps + `pricing_url`) for callers below Pro. A 400 caused by an unknown query parameter additionally carries `allowed_params` — the complete list this endpoint accepts, read straight off the endpoint''s own schema — plus `docs`, a link to the developer reference. Individual entries in `extra.fields` may carry a did-you-mean hint in their `msg`.'
    StoryLookup:
      type: object
      description: What a story id resolves to. Exactly one of `story`, `redirect_to` and `article` is filled, according to `status`.
      required:
      - status
      properties:
        status:
          type: string
          enum:
          - found
          - merged
          - not_a_story
          description: '`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:
          nullable: true
          allOf:
          - $ref: '#/components/schemas/Story'
        redirect_to:
          type: string
          nullable: true
          description: The surviving story's id, when `status` is `merged`.
        article:
          nullable: true
          allOf:
          - $ref: '#/components/schemas/StoryArticleRef'
          description: The single-publisher article the id belongs to, when `status` is `not_a_story`.
    Story:
      description: The full story — a `StoryCard` plus the summary's body.
      allOf:
      - $ref: '#/components/schemas/StoryCard'
      - type: object
        required:
        - why_it_matters
        - key_facts
        - ticker_takes
        - uncertainties
        - based_on
        - materials_at_build
        - new_materials
        - revision_generated_at
        - model
        - indexable
        properties:
          why_it_matters:
            type: string
            description: Why the event matters to the market, one short paragraph. Empty in `preparing` state.
          key_facts:
            type: array
            maxItems: 6
            items:
              $ref: '#/components/schemas/StoryKeyFact'
            description: Up to six facts, each attributed to the article it was read from.
          ticker_takes:
            type: array
            items:
              $ref: '#/components/schemas/StoryTickerTake'
            description: The read per ticker — direction and a one-line note.
          uncertainties:
            type: array
            items:
              type: string
            description: What the articles leave open or disagree on.
          based_on:
            type: integer
            description: 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:
            type: integer
            description: The story's visible article count when the summary was written.
          new_materials:
            type: integer
            description: Articles that joined after the summary was written (non-zero only in `stale` state).
          revision_generated_at:
            type: string
            format: date-time
            nullable: true
            description: When the published summary was written; `null` in `preparing` state.
          model:
            type: string
            description: The model that wrote the published summary; empty in `preparing` state.
          indexable:
            type: boolean
            description: 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.
    StoryMaterial:
      type: object
      required:
      - uid
      - title
      - publisher
      - time_published
      - url
      - relevance_score
      - seo_eligible
      properties:
        uid:
          type: string
          description: The article's uid — `/api/news/{uid}/` has the enriched row.
        title:
          type: string
        publisher:
          type: string
          description: Source domain.
          example: apnews.com
        time_published:
          type: string
          format: date-time
        url:
          type: string
          description: The publisher's own URL of the article.
        relevance_score:
          type: integer
          minimum: 1
          maximum: 10
        seo_eligible:
          type: boolean
          description: Whether the article passes AlphAI's quality gate (relevance 7+, tickers, a news category, new information); the story page links such articles to their alphai.io page. Informational — the API serves every row regardless.
    StoryKeyFact:
      type: object
      required:
      - text
      - source_uid
      - publisher
      properties:
        text:
          type: string
          description: The fact as the summary states it; every figure in it appears in the source article.
        source_uid:
          type: string
          description: 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:
          type: string
          description: The source domain of that article.
          example: reuters.com
    StoriesPage:
      type: object
      required:
      - results
      - next_cursor
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/StoryCard'
        next_cursor:
          type: string
          nullable: true
          description: Opaque cursor for the next (older) page; `null` at the end.
    StoryArticleRef:
      type: object
      required:
      - uid
      - title
      - time_published
      properties:
        uid:
          type: string
        title:
          type: string
        time_published:
          type: string
          format: date-time
  responses:
    ArchiveGated:
      description: '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.

        '
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: '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.

        '
      headers:
        Retry-After:
          description: Seconds to wait before retrying (burst ≤60s; day cap ≤3600s).
          schema:
            type: integer
        X-RateLimit-Limit:
          description: The tier's per-day request volume.
          schema:
            type: integer
        X-RateLimit-Remaining:
          description: Requests left in today's volume budget.
          schema:
            type: integer
        X-RateLimit-Reset:
          description: Epoch seconds of the next 00:00 UTC reset.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: ak_live_*
      description: 'Token of the form `ak_live_<random>`. Issued from

        `/account/api-keys` on the website. Send as `Authorization: Bearer …`.

        '
x-provenance:
  method: harvested
  publisher: AlphaAI
  source: https://api.alphai.io/api/schema/
  note: split by refine-openapis from the provider's own published document, verified by fetch (openapi/_original/*.fetch.json);
    stamped by stamp-openapi-provenance.py (roadmap#793)