openapi: 3.1.0 info: title: alphai REST API version: "1.24.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. 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. The headers reflect origin processing, so a response served from a shared cache may omit them. News-archive depth is tiered: Free keys can page the feeds back 30 days, Basic 90 days, Pro 180 days. Paging past your horizon returns `403` with an `extra.reason` of `archive_horizon` (see the ArchiveGated response). Coverage note: our collectors expanded in June 2026, so archive months before 2026-06 hold fewer articles per day than the current feed. Insider data (SEC EDGAR Form 4) flows through the same news shape: `category=insider` rows in the feed, a dedicated `/api/news/insider/` route, and 30-day per-ticker rollups at `/api/symbols/{ticker}/insider-summary/`. For charts and analysis there is `/api/symbols/{ticker}/insider-trades/`: the complete per-ticker event history with 3m/12m/all-time rollups, weekly and monthly dollar buckets, and every event of the trailing 12 months in one response. Market-wide macro coverage (central-bank decisions, CPI/jobs prints, commodities, geopolitics) is in the feed too, under the `macro_economy`, `commodities` and `geopolitics` categories and a dedicated `/api/news/macro/` route. Macro rows are market-wide events, so most carry an empty `tickers` list — filter them by category, not by symbol. The forward half of the macro loop is `/api/calendar/`: the official schedule of upcoming US macro releases (FOMC decisions and minutes, CPI, PPI, jobs report, GDP estimates, PCE, retail sales, jobless claims, JOLTS), each with a stable occurrence `uid` that survives reschedules. Ask the calendar what's coming, then read `/api/news/macro/` for what a release meant once it's out. Query parameters are validated strictly. An unknown or misspelled parameter returns 400 naming the field, and the error body carries `extra.allowed_params` — every parameter that endpoint accepts — so you never have to guess. Common mix-ups also get a did-you-mean (`limit` and `per_page` point to `page_size`; `offset`, `page`, `skip` and the `after_id`/`before_id` family point to `cursor`; `ticker` points to `symbol`). Parameters that belong to the MCP server rather than to this API — free-text `q`/`query`/`search`, and `min_actionability` — say so instead of failing silently. See https://alphai.io/developers for the score and sentiment legend, and https://alphai.io/pricing for tier limits. 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: news description: Relevance-scored, ticker-linked news. - name: symbols description: Active equity, crypto, and foreign symbols (tickers). - name: calendar description: Scheduled US macro releases (FOMC, CPI, jobs, GDP, PCE…). paths: /api/news/: get: tags: [news] summary: List news (cursor-paginated) description: | 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. Delta mode carries live coverage only: when we add history in bulk (backfilling an earlier period), those rows do NOT enter this stream — they would arrive as thousands of "new" items that are months old and push your cursor far behind the live feed. They are served normally in `sort=published`, by date filters and by `/api/news/{uid}/`, so a one-off catch-up over the archive is a published-mode query. **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. 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: symbol description: >- Filter to articles that mention this ticker. US equities use the bare symbol (`NVDA`); cryptocurrencies use the `-USD` form (`BTC-USD`), and a bare crypto name resolves to it (`DOGE` matches `DOGE-USD`); foreign listings use the Yahoo suffix (`VOD.L`). A delisted symbol returns its news history. A renamed company's current ticker also matches articles tagged with its former ticker, and any share class of an issuer matches articles tagged with its other listed classes (`GOOGL` includes rows tagged `GOOG`); article tags stay as published. schema: { type: string, example: "NVDA" } - in: query name: category description: >- Keep only these categories. Accepts a single value, a CSV list (`category=earnings,insider`), or a repeated parameter — matches any of them (OR). style: form explode: true schema: type: array items: { $ref: "#/components/schemas/NewsCategory" } - in: query name: exclude_categories description: >- Drop these categories from the feed. Same single / CSV / repeated forms as `category`. style: form explode: true schema: type: array items: { $ref: "#/components/schemas/NewsCategory" } - in: query name: min_relevance description: Override the default ≥4 threshold. schema: { type: integer, minimum: 1, maximum: 10, default: 4 } - in: query name: page_size description: >- Items per page, default `10`. Any value from `1` to `20` is accepted on every tier; `21`-`50` requires a Pro key. A value outside `1`-`50`, or an over-`20` page without a Pro key, returns 400 — the page is never silently clamped. Pagination is otherwise unchanged: pass each response's `next_cursor` back as `cursor` regardless of page size. schema: { type: integer, minimum: 1, maximum: 50, default: 10 } - in: query name: collapse description: >- `story` collapses coverage of one event to a single representative row (the story root): syndicated reprints and independently written articles about the same event, matched on headline and lead similarity. Populates the `story_id`, `sources_count` and `sources` fields on each item. Omit for the full feed (every article, story fields `null`). Only `story` is accepted; any other value returns 400. Note: row-level filters (`symbol`, `category`, `min_relevance`) apply to the story's representative root, so a story whose root does not match the filter is omitted entirely. schema: { type: string, enum: [story] } - in: query name: sort description: >- `published` (default) is the reverse-chronological feed. `ingested` is delta-polling mode: rows in the order they became available, ascending, with `next_cursor` always returned (empty `results` = caught up) — see the endpoint description. All other filters apply unchanged in both modes. Cursors are mode-specific: a cursor is only valid with the sort mode that issued it (a mismatch returns 400). schema: { type: string, enum: [published, ingested], default: published } responses: "200": description: A page of enriched articles. content: application/json: schema: { $ref: "#/components/schemas/NewsPagination" } "400": { description: Invalid cursor (opaque; reuse `next_cursor`), a cursor replayed into the other `sort` mode, unsupported `collapse`/`sort` value, or a `page_size` outside 1-50 or not permitted on this tier. } "403": { $ref: "#/components/responses/ArchiveGated" } "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } /api/news/trending/: get: tags: [news] summary: Trending news (last 48h) description: >- Top stories from the trailing 48h: `relevance_score >= 8`, ranked by relevance score decayed by article age, so a fresh story outranks an equally-scored stale one. At most two stories per ticker. Fixed size of 10, no pagination. Coverage of one story, syndicated reprints and independent write-ups of the same event alike, is collapsed to a single representative, and each item carries the `story_id`, `sources_count` and `sources` fields for the story it represents. Most stories are carried by a single outlet, so `sources_count` is usually 1; see its field description. responses: "200": description: List of enriched articles. content: application/json: schema: type: array items: { $ref: "#/components/schemas/RichNewsArticle" } "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } /api/news/insider/: get: tags: [news] summary: Insider-transaction news (SEC Form 4) description: | Dedicated insider feed (`category=insider`): SEC EDGAR Form 4 insider activity ONLY — one deterministic-templated row per filing event (a filing's non-derivative trades of one type and holding form, aggregated: total shares, volume-weighted average price and executed range, total value), surfaced shortly after filing, with the relevance score computed from the event's total (size, buy vs. sell, 10b5-1 plan or not). Same enriched shape, ticker and `relevance_score >= 4` filtering, and cursor-based pagination as `/api/news/`. Equivalent to `/api/news/?category=insider`, surfaced as a first-class route. For aggregate stats instead of the stream, see `/api/symbols/{ticker}/insider-summary/`. To watch for new filings, poll with `sort=ingested` rather than re-reading the newest page: a Form 4 is filed up to several days after the trade it reports, so a new event often enters the feed already below the head of the publish-ordered page. Delta mode orders by the moment the row became available, so it never misses one. The contract is identical to `/api/news/?sort=ingested` — see that endpoint for the full description. 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: symbol description: >- Filter to insider news that mentions this ticker. Delisted symbols return their history; a renamed company's current ticker also matches rows tagged with its former ticker. schema: { type: string, example: "NVDA" } - in: query name: min_relevance description: >- Override the default ≥4 threshold. Insider rows score deterministically from the event's summed dollar value (plus buy/10b5-1 modifiers), so this acts as an "only large trades" dial — e.g. `min_relevance=7` keeps roughly $10M+ events. schema: { type: integer, minimum: 1, maximum: 10, default: 4 } - in: query name: page_size description: >- Items per page, default `10`. Any value from `1` to `20` is accepted on every tier; `21`-`50` requires a Pro key. A value outside `1`-`50`, or an over-`20` page without a Pro key, returns 400. schema: { type: integer, minimum: 1, maximum: 50, default: 10 } - in: query name: sort description: >- `published` (default) is the reverse-chronological feed. `ingested` is delta-polling mode, with the same contract as on `/api/news/`: rows in the order they became available, ascending, `next_cursor` always returned (empty `results` = caught up), and all other filters applied unchanged. Prefer it for watching insider activity — a Form 4 is filed days after the trade it reports, so a new event routinely lands below the newest page of the publish-ordered feed and a poller reading only the head misses it. Cursors are mode-specific: a cursor is only valid with the sort mode that issued it (a mismatch returns 400). schema: { type: string, enum: [published, ingested], default: published } responses: "200": description: >- A page of enriched insider-transaction articles. Each item additionally carries the structured `insider` event block (side / shares / average price / total value / reporting owner) — populated on this endpoint only. content: application/json: schema: { $ref: "#/components/schemas/NewsPagination" } "400": { description: Invalid cursor (opaque; reuse `next_cursor`), a cursor replayed into the other `sort` mode, an unsupported `sort` value, or a `page_size` outside 1-50 or not permitted on this tier. } "403": { $ref: "#/components/responses/ArchiveGated" } "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } /api/news/macro/: get: tags: [news] summary: Market-wide macro news (central banks, prints, commodities, geopolitics) description: | Dedicated macro feed: the `macro_economy`, `commodities` and `geopolitics` categories in one stream — central-bank decisions and statements (FOMC), inflation/jobs prints, oil and gold, geopolitical risk. Same enriched shape, `relevance_score >= 4` floor and cursor-based pagination as `/api/news/`; equivalent to `/api/news/?category=macro_economy,commodities,geopolitics`, surfaced as a first-class route. Macro rows are market-wide, so most carry an empty `tickers` list — that is by design (an FOMC statement is not news about one company), and the per-article analysis block is still populated. A macro row that IS about listed companies (an oil-price story tagged to majors) keeps its tickers. To watch for new releases, poll with `sort=ingested` — same delta contract as `/api/news/?sort=ingested`. 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: category description: >- Narrow to a subset of the macro categories, comma-separated. Values outside `macro_economy`/`commodities`/`geopolitics` return 400 (the full category filter lives on `/api/news/`). schema: { type: string, example: "macro_economy" } - in: query name: min_relevance description: >- Override the default ≥4 threshold. On release days the release itself scores 7-9, so `min_relevance=7` keeps roughly "the events" and drops the commentary. schema: { type: integer, minimum: 1, maximum: 10, default: 4 } - in: query name: page_size description: >- Items per page, default `10`. Any value from `1` to `20` is accepted on every tier; `21`-`50` requires a Pro key. A value outside `1`-`50`, or an over-`20` page without a Pro key, returns 400. schema: { type: integer, minimum: 1, maximum: 50, default: 10 } - in: query name: sort description: >- `published` (default) is the reverse-chronological feed. `ingested` is delta-polling mode, with the same contract as on `/api/news/`. Cursors are mode-specific: a cursor is only valid with the sort mode that issued it (a mismatch returns 400). schema: { type: string, enum: [published, ingested], default: published } responses: "200": description: A page of enriched macro articles. content: application/json: schema: { $ref: "#/components/schemas/NewsPagination" } "400": { description: Invalid cursor (opaque; reuse `next_cursor`), a cursor replayed into the other `sort` mode, a category outside the macro set, or a `page_size` outside 1-50 or not permitted on this tier. } "403": { $ref: "#/components/responses/ArchiveGated" } "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } /api/calendar/: get: tags: [calendar] summary: Scheduled US macro releases (economic calendar) description: | The official forward schedule of US macro releases: FOMC decisions (with SEP and press-conference markers) and minutes, CPI, PPI, the jobs report (nonfarm payrolls), GDP estimates (advance/second/third), PCE, advance retail sales, weekly jobless claims and JOLTS — sourced from the agencies' own schedule pages (Fed, BLS, BEA, Census, DOL). Each occurrence carries a stable `uid` (`US-CPI-2026-07`) that survives reschedules: a moved release keeps its identity, updates `scheduled_at` and reports `schedule_status`. `phase` says only whether the scheduled moment has passed (`upcoming`/`elapsed`) — it deliberately does not claim the agency actually published. Pair the calendar with `/api/news/macro/` to read what a release meant once it's out. The window is `[from_date, to_date)` — from inclusive, to exclusive; date-only values mean UTC midnight; defaults are today (UTC) → +7 days; the span is capped at 400 days. No pagination: a full year of every series is ~250 rows (hard cap 500), ordered by `scheduled_at` ascending. Cancelled and postponed occurrences stay in the response with their `schedule_status`. parameters: - in: query name: from_date description: >- Window start, inclusive. `YYYY-MM-DD` (UTC midnight) or an ISO datetime (naive = UTC). Default: today, UTC midnight. schema: { type: string, example: "2026-08-07" } - in: query name: to_date description: >- Window end, exclusive. Same formats. Default `from_date` + 7 days; span capped at 400 days. schema: { type: string, example: "2026-08-14" } - in: query name: event_key description: Narrow to specific series, comma-separated. Unknown keys return 400. schema: { type: string, example: "cpi,nfp,fomc_decision" } - in: query name: importance description: Narrow by importance tier. schema: { type: string, enum: [high, medium, low] } - in: query name: country description: v1 covers US releases only. schema: { type: string, enum: [US], default: US } responses: "200": description: Occurrences within the window, `scheduled_at` ascending. content: application/json: schema: { $ref: "#/components/schemas/CalendarEvents" } "400": description: >- Unknown parameter or value, a malformed date, `to_date` not after `from_date`, or a window over 400 days. "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } /api/news/{uid}/: get: tags: [news] summary: Get a news article by UID parameters: - in: path name: uid required: true schema: { type: string, pattern: "^[a-f0-9]{16}$" } responses: "200": description: A single enriched article. content: application/json: schema: { $ref: "#/components/schemas/RichNewsArticle" } "404": { $ref: "#/components/responses/NotFound" } "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } /api/news/{uid}/related/: get: tags: [news] summary: Articles related to one article description: | Up to 6 articles related to the given article: fresh articles sharing a ticker first, then same-category recents as backfill. Same enriched shape and `relevance_score >= 4` filtering as `/api/news/`; insider (SEC Form 4) rows are excluded unless the base article is itself insider. No pagination — the list is a fixed-size block. parameters: - in: path name: uid required: true schema: { type: string, pattern: "^[a-f0-9]{16}$" } responses: "200": description: Related enriched articles (possibly fewer than 6). content: application/json: schema: type: object required: [results] properties: results: type: array maxItems: 6 items: { $ref: "#/components/schemas/RichNewsArticle" } "404": { $ref: "#/components/responses/NotFound" } "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } /api/symbols/: get: tags: [symbols] summary: List active tickers description: >- All active symbols (US equities, crypto, and foreign listings), alphabetical by ticker. ~10k entries — cache aggressively, or slice with the optional `limit`/`offset` params (the response stays a bare array either way). Pass `search` to resolve a name, brand or ticker prefix to its canonical symbol (`search=bitcoin` returns `BTC-USD`, `search=spacex` returns `SPCX`). parameters: - in: query name: limit description: Return at most this many symbols. Omit for the full list. schema: { type: integer, minimum: 1, maximum: 10000 } - in: query name: offset description: Skip this many symbols from the start of the list. schema: { type: integer, minimum: 0, default: 0 } - in: query name: search description: >- Resolve a query to matching symbols: ticker-prefix, company-name substring, or the company's brand name where that differs from its registered name (`search=spacex` returns `SPCX`, registered as SPACE EXPLORATION TECHNOLOGIES CORP). Case-insensitive; exact ticker, exact name and brand matches rank above prefix and substring matches. Useful for finding the canonical form of a name (`search=bitcoin` returns `BTC-USD`). schema: { type: string, example: "bitcoin" } responses: "200": description: List of active symbols. content: application/json: schema: type: array items: { $ref: "#/components/schemas/Symbol" } "400": { description: Out-of-range or unknown query parameter. } "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } /api/symbols/{ticker}/: get: tags: [symbols] summary: Symbol detail parameters: - in: path name: ticker required: true description: >- Ticker symbol. US equities are bare (`AAPL`); cryptocurrencies use the `-USD` form (`BTC-USD`); foreign listings use the Yahoo suffix (`VOD.L`). schema: { type: string, pattern: "^[A-Z0-9][A-Z0-9.\\-]{0,19}$", example: "BTC-USD" } responses: "200": description: >- Symbol with extended metadata. Resolves delisted symbols too: check `status`, and `renamed_to` for the successor ticker when the company continues under a new symbol. content: application/json: schema: { $ref: "#/components/schemas/Symbol" } "404": { $ref: "#/components/responses/NotFound" } "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } /api/symbols/{ticker}/sentiment-summary/: get: tags: [symbols] summary: 7-day AI sentiment rollup for one ticker description: | Counts of bullish / neutral / bearish per-ticker sentiment calls from the enriched news flow over the trailing 7 days, plus per-day buckets. Insider (SEC Form 4) template rows are excluded — this reflects press coverage, not filings. parameters: - in: path name: ticker required: true description: >- Accepts the same ticker forms as the symbol detail endpoint: bare crypto names resolve to their `-USD` listing (`DOGE` → `DOGE-USD`; the response echoes the resolved ticker), and delisted or renamed symbols stay addressable. schema: { type: string, pattern: "^[A-Z0-9][A-Z0-9.\\-]{0,19}$" } responses: "200": description: >- Sentiment counts for the window. Zeros mean a quiet week for a real listing; a ticker no symbol owns returns 404 instead. content: application/json: schema: { $ref: "#/components/schemas/TickerSentimentSummary" } "400": { description: Malformed ticker. } "404": { $ref: "#/components/responses/NotFound" } "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } /api/symbols/{ticker}/insider-summary/: get: tags: [symbols] summary: 30-day insider-transaction rollup for one ticker description: | Aggregated SEC Form 4 activity over the trailing 30 days: buy/sell transaction counts, dollar volumes, the share of transactions executed under pre-arranged 10b5-1 plans, and the most active insiders ranked by absolute net value (buys minus sells). parameters: - in: path name: ticker required: true description: >- Accepts the same ticker forms as the symbol detail endpoint: bare crypto names resolve to their `-USD` listing, and delisted or renamed symbols stay addressable. Crypto and foreign listings have no Form 4 flow, so they return zeros. schema: { type: string, pattern: "^[A-Z0-9][A-Z0-9.\\-]{0,19}$" } responses: "200": description: >- Insider activity stats for the window. Zeros mean no filings in the window for a real listing; a ticker no symbol owns returns 404 instead. content: application/json: schema: { $ref: "#/components/schemas/TickerInsiderSummary" } "400": { description: Malformed ticker. } "404": { $ref: "#/components/responses/NotFound" } "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } /api/symbols/{ticker}/insider-trades/: get: tags: [symbols] summary: Form 4 event history and chart aggregates for one ticker description: | The complete per-ticker insider-trading payload behind alphai.io/stock/{ticker}/insider-trades: the paginated Form 4 event history plus everything a chart needs, in one request. One event is a filing's whole tranche group (shares summed, the price value-weighted, a 10b5-1 ladder is ONE event), same folding as the feed's `insider` block. The first page (no `cursor`) also carries the aggregates: `summary` (3-month, 12-month and all-time windows plus the most active insiders of the last 12 months), `series` (monthly buckets) and `series_weekly` (Monday-keyed weekly buckets), both zero-filled up to the current period and capped to the trailing 12 months, and `chart_events` — every event of the trailing 12 months regardless of pagination. The aggregates always cover both sides regardless of the `side` filter, and they are `null` on cursor pages, which carry only the next `events` chunk. Side semantics on this surface follow the value flow: code P is `buy`; codes S and D are both `sell`, because a code D sale back to the issuer moves dollars off the insider's position like any sale. This deliberately differs from the feed's `insider.side`, which labels D as `other`; use `transaction_code` to tell a market sale (S) from a sale to the issuer (D). parameters: - in: path name: ticker required: true description: >- Accepts the same ticker forms as the symbol detail endpoint: bare crypto names resolve to their `-USD` listing, and delisted or renamed symbols stay addressable. Crypto and foreign listings have no Form 4 flow, so they return empty history. schema: { type: string, pattern: "^[A-Z0-9][A-Z0-9.\\-]{0,19}$" } - in: query name: side description: >- Filters the paginated `events` list only; the first page's aggregates always cover both sides. schema: { type: string, enum: [buy, sell, all], default: all } - in: query name: cursor description: >- Opaque keyset cursor from a prior response's `next_cursor`. Omit it for the newest page, the only one carrying the aggregates. schema: { type: string } - in: query name: page_size description: >- Events per page. Not tier-gated: every tier may request up to 200, so one request can load a chart's whole working set. schema: { type: integer, minimum: 1, maximum: 200, default: 20 } responses: "200": description: >- One page of the event history. Empty arrays mean no recorded filings for a real listing; a ticker no symbol owns returns 404 instead. content: application/json: schema: { $ref: "#/components/schemas/TickerInsiderTrades" } "400": { description: Malformed ticker, cursor or query parameter. } "404": { $ref: "#/components/responses/NotFound" } "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } /api/symbols/{ticker}/peers/: get: tags: [symbols] summary: Related tickers for one ticker description: | Other companies in the same industry (widening to the broader sector when the industry is thin) that carry recent enriched news, most recently covered first. Powers the "Related tickers" links on the stock hub pages. parameters: - in: path name: ticker required: true schema: { type: string, pattern: "^[A-Z0-9][A-Z0-9.\\-]{0,19}$" } responses: "200": description: >- Up to ten related tickers. A well-formed but unknown ticker, or one with no peers carrying recent news, returns an empty list. content: application/json: schema: { $ref: "#/components/schemas/SymbolPeers" } "400": { description: Malformed ticker. } "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } /api/symbols/directory/: get: tags: [symbols] summary: Stock directory — most-covered tickers and sector cards description: | The hub index behind /stocks: the globally most-covered tickers over the trailing window, plus one card per sector (size + its busiest names). Ranked by recent enriched-news coverage. responses: "200": description: Most-active tickers and sector cards. content: application/json: schema: { $ref: "#/components/schemas/SymbolDirectory" } "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } /api/symbols/sectors/{slug}/: get: tags: [symbols] summary: One sector's most-covered tickers description: | The tickers in a sector (by slug, e.g. `financial-services`), ranked by recent enriched-news coverage. Powers the /stocks/{slug} sub-pages. parameters: - in: path name: slug required: true schema: { type: string, pattern: "^[a-z0-9-]+$" } responses: "200": description: The sector and its most-covered tickers. content: application/json: schema: { $ref: "#/components/schemas/SectorSymbols" } "404": { description: Unknown sector slug. } "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } components: securitySchemes: apiKey: type: http scheme: bearer bearerFormat: ak_live_* description: | Token of the form `ak_live_`. Issued from `/account/api-keys` on the website. Send as `Authorization: Bearer …`. responses: 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 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. 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" } NotFound: description: No item with that identifier. content: application/json: schema: { $ref: "#/components/schemas/Error" } ArchiveGated: description: | 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. content: application/json: schema: { $ref: "#/components/schemas/Error" } schemas: NewsCategory: type: string description: >- `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`. enum: - earnings - mergers_acquisitions - regulation - macro_economy - sector_analysis - market_movers - technology - commodities - crypto - ipo - geopolitics - insider - corporate_actions - other Sentiment: type: string enum: [positive, neutral, negative] CalendarEvent: type: object description: >- One scheduled occurrence of a US macro release. `uid` is a stable, opaque occurrence id — reschedules update the row in place, so the uid is safe to store and link. `phase` is computed against request time and only says whether the scheduled moment passed; check `schedule_status` first (a `cancelled` row's phase is meaningless). properties: uid: { type: string, example: "US-CPI-2026-07" } event_key: type: string enum: - fomc_decision - fomc_minutes - cpi - ppi - nfp - gdp - pce - retail_sales - jobless_claims - jolts title: { type: string, example: "CPI (Consumer Price Index)" } reference_period: type: string description: >- What the release covers, machine-readable: a month (`2026-07`), a quarter (`2026-Q2`), or — for weekly claims — the reference week-ending Saturday (`2026-07-25`). example: "2026-07" release_stage: type: string nullable: true description: GDP only — `advance`, `second` or `third`; null elsewhere. scheduled_at: type: string format: date-time description: Official release moment (UTC; 08:30 / 10:00 / 14:00 ET converted). phase: { type: string, enum: [upcoming, elapsed] } schedule_status: { type: string, enum: [scheduled, postponed, cancelled] } schedule_basis: type: string enum: [official, inferred] description: >- `official`: the date is printed on the agency's own schedule page. `inferred`: derived from the documented publication cadence — weekly jobless claims (DOL publishes no forward schedule) and FOMC minutes dates the Fed has not printed yet (three weeks after the meeting). Inferred dates flip to official once the agency lists them. importance: { type: string, enum: [high, medium, low] } category: { $ref: "#/components/schemas/NewsCategory" } country: { type: string, example: "US" } source_url: type: string format: uri description: The agency's own schedule page for the series. press_conference_at: type: string format: date-time nullable: true description: FOMC decisions only — the 14:30 ET press conference; null elsewhere. has_sep: type: boolean description: >- FOMC decisions only — true when the meeting carries a Summary of Economic Projections (the "dot plot"). CalendarEvents: type: object properties: events: type: array items: { $ref: "#/components/schemas/CalendarEvent" } Confidence: type: string enum: [high, medium, low] Actionability: type: string enum: [high, medium, low, negligible] Topic: type: object required: [topic, relevance] properties: topic: { type: string } relevance: { type: number, format: float } OriginalArticle: type: object description: The article as fetched from the source. `raw_text` is intentionally NOT exposed here. required: [uid, title, url, summary, time_published, source] properties: id: { type: integer } uid: { type: string, pattern: "^[a-f0-9]{16}$" } title: { type: string } url: { type: string, format: uri } time_published: { type: string, format: date-time } authors: { type: array, items: { type: string } } summary: { type: string, description: AI-generated summary safe to redistribute. } banner_image: { type: string, format: uri, nullable: true } source: { type: string } source_domain: { type: string } topics: type: array items: { $ref: "#/components/schemas/Topic" } tickers_sentiment: type: array items: { type: object, additionalProperties: true } ownership_form: type: string nullable: true enum: [direct, indirect] description: >- 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: type: string format: date-time description: >- 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: type: string format: date-time description: >- Last time the stored article row was touched (including internal maintenance). Informational only. ImpactAnalysis: type: object properties: summary: { type: string } sentiment: { $ref: "#/components/schemas/Sentiment" } price_impact_prediction: { type: string } confidence: { $ref: "#/components/schemas/Confidence" } reasoning: { type: string } TickerAnalysis: type: object properties: ticker: { type: string } relevance_context: { type: string } impact_analysis: { $ref: "#/components/schemas/ImpactAnalysis" } NewsTradingValue: type: object properties: actionability_score: { $ref: "#/components/schemas/Actionability" } information_novelty: type: integer minimum: 0 maximum: 10 description: >- 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: { type: string } market_sentiment_alignment: { type: string } estimated_read_time: { type: string } IndirectMarketEffects: type: object properties: sector_implications: { type: string } regional_market_impact: { type: string } global_market_relevance: { type: string } AlternativePerspectives: type: object properties: contrarian_view: { type: string } overlooked_factors: { type: string } KeyEntity: type: object properties: name: { type: string } type: { type: string, example: company } description: { type: string } NewsContextEnhancement: type: object properties: background_context: { type: string } impact_analysis: { type: string } key_entities: type: array items: { $ref: "#/components/schemas/KeyEntity" } market_relevance_summary: { type: string } estimated_read_time_minutes: { type: integer } AITradingInsights: type: object properties: ticker_analysis: type: array items: { $ref: "#/components/schemas/TickerAnalysis" } news_trading_value: { $ref: "#/components/schemas/NewsTradingValue" } indirect_market_effects: { $ref: "#/components/schemas/IndirectMarketEffects" } alternative_perspectives: { $ref: "#/components/schemas/AlternativePerspectives" } EnrichedArticle: type: object properties: category: { $ref: "#/components/schemas/NewsCategory" } tickers: type: array items: { type: string } description: >- 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`. example: ["NVDA"] relevance_score: type: integer minimum: 1 maximum: 10 description: | 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: { $ref: "#/components/schemas/AITradingInsights" } news_context_enhancement: { $ref: "#/components/schemas/NewsContextEnhancement" } RichNewsArticle: type: object required: [original, enrichment] properties: original: { $ref: "#/components/schemas/OriginalArticle" } enrichment: { $ref: "#/components/schemas/EnrichedArticle" } story_id: type: string nullable: true description: >- 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. example: "788e477c66f3849b" sources_count: type: integer nullable: true description: >- 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. example: 3 sources: type: array nullable: true items: { type: string } description: >- Story-collapsed responses only. Distinct source domains covering this story, in first-appearance order, capped at 10. `null` in the default feed. example: ["reuters.com", "apnews.com", "bloomberg.com"] insider: nullable: true allOf: [{ $ref: "#/components/schemas/InsiderEvent" }] description: >- `/api/news/insider/` only — the structured SEC Form 4 event block. `null` on every other endpoint and on rows without paired transaction data. InsiderEvent: type: object description: >- 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. required: [side, transaction_code, shares, is_10b5_1, insider_name, insider_title, is_officer, is_director, is_ten_percent_owner, transaction_date, filed_at, late_filing] properties: side: type: string enum: [buy, sell, other] description: >- 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: type: string description: Raw SEC Form 4 transaction code (`P`, `S`, `D`, …). example: "S" shares: type: string description: Total shares across the event's tranches (decimal string). example: "25000" avg_price_usd: type: string nullable: true description: >- Value-weighted average price per share over priced tranches. `null` when the filing prices no tranche. example: "187.32" total_value_usd: type: string nullable: true description: >- Total USD value across priced tranches (a lower bound when some tranches are unpriced). `null` when no tranche is priced. example: "4683000" is_10b5_1: type: boolean description: True when any tranche executed under a pre-arranged 10b5-1 plan. insider_name: { type: string, example: "STEVENS MARK A" } insider_title: { type: string, example: "Director" } is_officer: { type: boolean } is_director: { type: boolean } is_ten_percent_owner: { type: boolean } transaction_date: type: string format: date description: Date of the group's last fill (a ladder can span days). example: "2026-07-09" filed_at: type: string format: date-time description: >- When EDGAR accepted the filing (UTC). Compare against `transaction_date` for your own lateness rule. example: "2026-07-11T20:31:04Z" late_filing: type: boolean description: >- 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. NewsPagination: type: object required: [results, next_cursor] properties: results: type: array items: { $ref: "#/components/schemas/RichNewsArticle" } next_cursor: type: string nullable: true description: >- 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. Symbol: type: object required: [symbol, name] properties: symbol: { type: string } name: { type: string } asset_type: { type: string, example: Stock, description: "`Stock`, `ETF`, or `Crypto`." } exchange: type: string example: NASDAQ description: >- TradingView exchange prefix. US: NYSE / NASDAQ / AMEX / OTC / CBOE (from SEC's exchange mapping). Foreign listings carry their venue prefix (LSE, XETR, EURONEXT, TSE, HKEX, KRX, …); crypto carries the `CRYPTO` sentinel. Empty string when unknown. sector: { type: string } industry: { type: string } description: { type: string } website: { type: string, format: uri, nullable: true } brand_aliases: type: array items: { type: string } description: >- Lowercase names the issuer is known by, when they differ from `name` (`["spacex"]` on SPCX, registered as SPACE EXPLORATION TECHNOLOGIES CORP). Use them to match a name a user typed against a ticker. Usually empty: most issuers are already reachable by their registered name. List responses only. example: ["spacex"] country: type: string description: >- ISO alpha-2 country of domicile. `US` for SEC-listed symbols, the listing venue's country for foreign listings. Empty for crypto — a coin has no domicile. example: US currency: type: string description: >- Trading currency. `USD` for SEC-listed symbols and crypto pairs, the local currency for foreign listings. example: USD status: type: string example: active description: >- Listing status, `active` or `delisted`. Delisted symbols stay resolvable on this endpoint so their news history remains reachable. delisted_at: type: string format: date-time nullable: true description: >- When the symbol was marked delisted. Null for active symbols and for symbols delisted before July 2026. renamed_to: type: string description: >- Set on a delisted symbol when the company continues under a new ticker (for example SATS became ECHO). Empty otherwise. SymbolPeer: type: object required: [ticker, name] properties: ticker: { type: string, example: "AMD" } name: { type: string, example: "Advanced Micro Devices Inc" } SymbolPeers: type: object required: [ticker, group, group_kind, peers] properties: ticker: { type: string } group: type: string description: Industry (or sector) label the peers share; empty if none. example: "Semiconductors" group_kind: type: string enum: ["industry", "sector", ""] description: Whether the peers were grouped by industry or the broader sector. peers: type: array items: { $ref: "#/components/schemas/SymbolPeer" } DirectorySymbol: type: object required: [ticker, name, news_count] properties: ticker: { type: string, example: "NVDA" } name: { type: string, example: "NVIDIA Corp" } news_count: type: integer description: Above-floor enriched-news rows mentioning the ticker in the window. DirectorySector: type: object required: [sector, slug, count, top] properties: sector: { type: string, example: "Technology" } slug: { type: string, example: "technology" } count: type: integer description: Total active symbols in the sector. top: type: array items: { $ref: "#/components/schemas/DirectorySymbol" } SymbolDirectory: type: object required: [most_active, sectors] properties: most_active: type: array items: { $ref: "#/components/schemas/DirectorySymbol" } sectors: type: array items: { $ref: "#/components/schemas/DirectorySector" } SectorSymbols: type: object required: [sector, slug, count, symbols] properties: sector: { type: string } slug: { type: string } count: { type: integer } symbols: type: array items: { $ref: "#/components/schemas/DirectorySymbol" } TickerSentimentSummary: type: object required: [ticker, days, total, bullish, neutral, bearish, daily] properties: ticker: { type: string } days: { type: integer, example: 7 } total: { type: integer, description: Articles with a sentiment call in the window. } bullish: { type: integer } neutral: { type: integer } bearish: { type: integer } daily: type: array items: { $ref: "#/components/schemas/DailySentimentBucket" } DailySentimentBucket: type: object required: [day, bullish, neutral, bearish] properties: day: { type: string, format: date } bullish: { type: integer } neutral: { type: integer } bearish: { type: integer } TickerInsiderSummary: type: object required: [ticker, days, total_transactions, buy_count, sell_count, pct_10b5_1, top_insiders] properties: ticker: { type: string } days: { type: integer, example: 30 } total_transactions: { type: integer } buy_count: { type: integer } sell_count: { type: integer } buy_value_usd: type: string nullable: true example: "1240000.00" description: Decimal string in USD; null when the window has no buys. sell_value_usd: type: string nullable: true example: "224580213.05" description: Decimal string in USD; null when the window has no sells. pct_10b5_1: type: integer minimum: 0 maximum: 100 description: Share of transactions executed under a pre-arranged 10b5-1 plan, in percent. top_insiders: type: array items: { $ref: "#/components/schemas/TopInsider" } TopInsider: type: object required: [name, title, transaction_count] properties: name: { type: string, example: STEVENS MARK A } title: type: string description: Officer title from the filing; empty when the filer reports as a director only. transaction_count: { type: integer } net_value: type: string nullable: true example: "-221102600.00" description: Buys minus sells in USD over the window, as a decimal string. TickerInsiderTrades: type: object required: [ticker, events, next_cursor] properties: ticker: { type: string } coverage_start: type: string format: date nullable: true description: >- Earliest recorded transaction date for this ticker; charts should not claim history from before it. `null` when nothing is recorded. summary: nullable: true allOf: [{ $ref: "#/components/schemas/InsiderTradesSummary" }] description: First page only; `null` on cursor pages. series: type: array nullable: true items: { $ref: "#/components/schemas/InsiderTradesMonthBucket" } description: >- Calendar-month buckets, zero-filled up to the current month and capped to the trailing 12 months. First page only; `null` on cursor pages. series_weekly: type: array nullable: true items: { $ref: "#/components/schemas/InsiderTradesWeekBucket" } description: >- ISO-week buckets keyed by their Monday, zero-filled up to the current week over the same 12-month horizon. First page only; `null` on cursor pages. chart_events: type: array nullable: true items: { $ref: "#/components/schemas/InsiderTradeEvent" } description: >- Every event of the trailing 12 months, both sides, independent of the paginated list below — a chart never shows a bar without its marks. First page only; `null` on cursor pages. events: type: array items: { $ref: "#/components/schemas/InsiderTradeEvent" } description: One page of the event history, newest first; honors `side`. next_cursor: type: string nullable: true description: >- Opaque cursor for the next (older) page; `null` at the end of the recorded history. InsiderTradesSummary: type: object required: [last_3m, last_12m, all_time, top_insiders] properties: last_3m: { $ref: "#/components/schemas/InsiderTradesWindow" } last_12m: { $ref: "#/components/schemas/InsiderTradesWindow" } all_time: { $ref: "#/components/schemas/InsiderTradesWindow" } top_insiders: type: array items: { $ref: "#/components/schemas/InsiderTradesTopInsider" } description: Most active reporters of the last 12 months, at most five. InsiderTradesWindow: type: object description: >- Event-level aggregates over one rolling window. Counts here are grouped EVENTS, not tranches, so they run lower than the transaction counts of `/api/symbols/{ticker}/insider-summary/`. required: [buy_count, sell_count, unique_insiders, pct_10b5_1] properties: buy_count: { type: integer } sell_count: { type: integer } buy_value_usd: type: string nullable: true example: "1240000.00" description: Decimal string in USD; null when the window has no priced buys. sell_value_usd: type: string nullable: true example: "224580213.05" description: Decimal string in USD; null when the window has no priced sells. unique_insiders: type: integer description: Distinct reporting owners in the window. pct_10b5_1: type: integer minimum: 0 maximum: 100 description: Share of the window's events executed under a pre-arranged 10b5-1 plan, in percent. InsiderTradesTopInsider: type: object required: [name, title, event_count] properties: name: { type: string, example: "Intrator Michael N" } title: type: string description: Officer title from the filing; empty when the filer reports as a director only. event_count: { type: integer } net_value_usd: type: string nullable: true example: "-505000000.00" description: Buys minus sells in USD over the last 12 months, as a decimal string; positive means a net buyer. InsiderTradesMonthBucket: type: object required: [month, buy_count, sell_count, buy_value_usd, sell_value_usd] properties: month: type: string example: "2026-08" description: Calendar month, `YYYY-MM`. buy_count: { type: integer } sell_count: { type: integer } buy_value_usd: type: string example: "0" description: Decimal string in USD; `"0"` for a quiet side. sell_value_usd: { type: string, example: "48200000.00" } InsiderTradesWeekBucket: type: object required: [week_start, buy_count, sell_count, buy_value_usd, sell_value_usd] properties: week_start: { type: string, format: date, description: Monday of the ISO week. } buy_count: { type: integer } sell_count: { type: integer } buy_value_usd: { type: string, example: "0" } sell_value_usd: { type: string, example: "9886021.65" } InsiderTradeEvent: type: object description: >- One Form 4 economic event, same folding as the feed's `InsiderEvent` (a filing's whole tranche group: `shares` and `total_value_usd` are group sums, `avg_price_usd` is value-weighted over priced tranches, money and share fields are decimal STRINGS). Differences from the feed block: `side` here is only `buy` or `sell` (D counts as sell, see the endpoint description), and each event adds its stake impact, tranche count and the link to our enriched article. required: [side, transaction_code, ownership_form, shares, tranche_count, is_10b5_1, late_filing, insider_name, insider_title, is_officer, is_director, is_ten_percent_owner, transaction_date, filed_at, news_uid, news_title, news_published, has_article] properties: side: type: string enum: [buy, sell] description: Value-flow label; `transaction_code` carries the raw code. transaction_code: type: string description: Raw SEC Form 4 transaction code (`P`, `S`, `D`). example: "S" ownership_form: type: string enum: [D, I] description: Direct or indirect holding pool. security_title: type: string description: Security class from the filing; empty when unreported. example: "Common Stock" shares: type: string description: Total shares across the event's tranches (decimal string). example: "107692" avg_price_usd: type: string nullable: true description: >- Value-weighted average price per share over priced tranches. `null` when the filing prices no tranche. example: "91.80" total_value_usd: type: string nullable: true description: >- Total USD value across priced tranches (a lower bound when some tranches are unpriced). `null` when no tranche is priced. example: "9886021.65" tranche_count: type: integer description: Fills folded into this event (a 10b5-1 ladder files many). example: 8 stake_change_pct: type: string nullable: true description: >- Percent of the pre-event position this event moved, as a decimal string; sells are negative ("-26.0" = sold 26% of the stake). `null` when the pre-event position cannot be reconstructed. example: "-100.0" is_10b5_1: type: boolean description: True when any tranche executed under a pre-arranged 10b5-1 plan. late_filing: type: boolean description: >- The filing missed the SEC's two-business-day deadline, computed the same way as the feed's `InsiderEvent.late_filing`. insider_name: { type: string, example: "Intrator Michael N" } insider_title: { type: string, example: "CEO and President" } is_officer: { type: boolean } is_director: { type: boolean } is_ten_percent_owner: { type: boolean } transaction_date: type: string format: date description: Date of the group's last fill (a ladder can span days). example: "2026-08-04" filed_at: type: string format: date-time description: When EDGAR accepted the filing (UTC). example: "2026-08-07T00:36:56Z" news_uid: type: string description: >- Uid of the enriched news row fronting this event; matches `original.uid` in the feeds. news_title: { type: string } news_published: type: string format: date-time description: Publish time of the fronting news row. has_article: type: boolean description: >- Whether the alphai.io article page for `news_uid` exists. Events ingested by the historical backfill have no enriched article; render those without a link. 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`.