# Finradar API Documentation (Full Reference) > Version: 3.61.0 | Generated: 2026-06-20 | Content Hash: 2d596dd5 > Fetch this file at: https://uat.finradarapi.com/llms-full.txt ## Changelog - 2026-06-19 (v3.61.0): **Fixes MCP tools that bricked every Anthropic MCP client.** Several endpoints documented query params with placeholder/alias shorthand in the literal `name` field — `/financials/screen` (`{metric}_gt`/`{metric}_lt`), `/form-13f/aggregation/by-ticker` (`min_{col} / max_{col}`), and pagination on `/form-13f/fund/{cik}` + `/fund/bulk` (`limit / size`, `from / offset`). These names flow verbatim into the generated OpenAPI spec and the MCP tool schemas, where the `{` `}` / space / `/` characters violate Anthropic's `input_schema.properties` key rule `^[a-zA-Z0-9_.-]{1,64}$` — so the Messages API returns HTTP 400 on the ENTIRE request and bricks any MCP session the moment such a tool enters the tools list (confirmed: `screen_stocks_by_financials` froze a client with repeated identical 400s and no recovery). Replaced all of them with the real identifiers the backend accepts: `/financials/screen` now lists its 38 concrete ratio filters (19 ratios x `_gt`/`_lt`); `/form-13f/aggregation/by-ticker` lists its concrete `min_*`/`max_*` range params; pagination uses canonical `limit`/`offset`/`size`/`from` with the accepted alias noted in the description. Also adds a build guard in `generate-openapi.js` that FAILS OpenAPI generation if any param/property name violates the Anthropic rule, so this class can never ship again. No backend change — docs/OpenAPI/MCP-schema accuracy only. - 2026-06-16 (v3.60.0): **`GET /api/v1/sec/filings`: cold-start EDGAR backfill is now concurrency-bounded (#459 P0-9).** The endpoint still self-heals a cold ticker by backfilling its full EDGAR history on first touch, but only ONE backfill per CIK now runs at a time, capped globally (default 3 concurrent, `SEC_BACKFILL_MAX_CONCURRENT`). Concurrent duplicate requests for the same ticker — or requests beyond the global cap — no longer each launch a multi-second DB-write + EDGAR crawl (which could hammer the database and risk an EDGAR rate-limit); instead they return the current (possibly partial/RSS-only) results immediately with a new `meta.backfill_pending: true` flag — re-query shortly for the complete history. The first/winning request is unchanged (synchronous backfill, returns full results). No request-contract change; `meta.backfilled_from_edgar` is unchanged. - 2026-06-16 (v3.59.0): **`GET /api/v1/sec/filings/{accession_number}/html`: optional size-bound paging (`offset`, `length`/`max_bytes`).** A raw 10-K can be tens of MB; pulling one whole into an LLM context window blows the token budget (and streams MBs back per call). You can now bound the response in UTF-8 bytes — `?max_bytes=200000` returns the first 200 KB, `?offset=200000&length=200000` the next 200 KB — and read the truncation state from response HEADERS (`X-Content-Total-Bytes`, `X-Content-Offset`, `X-Content-Returned-Bytes`, `X-Content-Truncated`). Fully backward-compatible: with no params the full document is returned exactly as before (the dashboard iframe is unaffected). Part of the MCP commercial-readiness security pass (#459). **Same pass — MCP surface hardening (no REST contract change):** the account/auth/payment/OAuth-lifecycle MUTATION tools (login/register/forgetpassword, payments, billing-portal, billing/quota, PUT /user, oauth/* + well-known) are removed from the agent-callable MCP tool surface (135→119 tools); `/oauth/revoke` now enforces RFC 7009 client-ownership; a per-credential request-rate + concurrency cap is enforced on the MCP proxy; and every MCP tool now carries read-only/open-world annotations plus its per-call token cost. - 2026-06-16 (v3.58.0): **Section Extractor (`GET /api/v1/scrapping/extractor`): two more megabank bleed fixes.** (1) 10-Q filers that print the "Financial Review" (Items 2/3/4) BEFORE the "Financial Statements" (Item 1) block — non-standard physical order — no longer run a narrative item through the entire statements block to Part II: the section is now bounded at the financial-statement cluster that physically follows it (e.g. Wells Fargo Item 4 Controls ~221,000→~2,400 characters; Bank of America Item 4 →~1,100; BNY Mellon combined Items 2 & 3 ~293,000→~54,000; Charles Schwab Item 3 ~119,000→a short cross-reference stub). (2) A 10-K Item 8 (Financial Statements) that is INCORPORATED BY REFERENCE — a brief note immediately followed by the Part IV "Financial Statement Schedules" list (e.g. Wells Fargo, JPMorgan) — now returns just that reference note instead of running through the exhibit list and signatures (~22,000→~280 characters). Verified with no change to filings that already worked (real financial statements, standard-order 10-Q, and 8-K extraction are unchanged). No request-contract change: identical parameters and identical `text` / `html` response shapes. - 2026-06-16 (v3.57.0): **Section Extractor (`GET /api/v1/scrapping/extractor`): megabank 10-K sections that over-extended past their true end are now bounded.** A complement to the descriptive-heading recovery in 3.55.0: where a bank renders sections under full canonical titles with no "Item N" prefix — sometimes in non-standard physical order (e.g. Citigroup prints Business → MD&A → Risk Factors) — a located section could run to end-of-document or into later sections because the next boundary carried no "Item N" label to stop at. The section end is now tightened to the next physical descriptive section heading, so these no longer bleed: e.g. Citigroup 10-K Item 1A (Risk Factors) dropped from ~1,000,000 characters (it had run through the financial statements to the glossary) to ~353,000, Item 1 (Business) from ~146,000 to ~15,000 (it had swallowed the MD&A), and Morgan Stanley Item 1A from ~669,000 to ~73,000. Monotonic and scoped: it only ever shortens an over-extended section — normal "Item N"-prefixed filers are byte-for-byte unchanged, and 10-Q / 8-K extraction is unaffected. No request-contract change: identical parameters and identical `text` / `html` response shapes. - 2026-06-15 (v3.56.0): Insider API documentation reconciled with the backend across the insider endpoints (apiDocs↔handler drift audit). Fixes the broken `holdings_snapshot` MCP tool (documented `/holdings/snapshot` with phantom `ticker`/`as_of` params → corrected to the real `/holdings/snapshot/{snapshot_date}` route with required `snapshot_date`). Removed phantom query params (`start_date`/`end_date` on by-ticker/by-insider, `ticker`/`start_date`/`end_date` on /screen), corrected response wrapper keys (top/search `data[]`, screen `results[]`, query `transactions[]`, clusters bare array, recommendations `buy[]`+`sell[]`), removed the non-existent `standard_type` response field from search/top/screen, corrected fabricated response shapes (screen per-company aggregate, clusters `cluster_type`/`first_date`/`last_date`, query item shape, recommendations buy/sell, summary `today_date` removed), and fixed documented default values (`limit` 50 not 100; `size` 50). No backend change — docs/OpenAPI/MCP-schema accuracy only. - 2026-06-15 (v3.55.0): **Section Extractor (`GET /api/v1/scrapping/extractor`): recovers the megabank "descriptive heading" section class.** Citigroup, Morgan Stanley, Wells Fargo and similar filers render each 10-K / 10-Q section under its full canonical SEC title with **no "Item N" prefix** (e.g. `MANAGEMENT'S DISCUSSION AND ANALYSIS OF FINANCIAL CONDITION AND RESULTS OF OPERATIONS`, `CONSOLIDATED FINANCIAL STATEMENTS`), sometimes in non-standard physical order — so the extractor previously returned **empty** for those items. These are now matched by full title and bounded at the next physical section heading, recovering the MD&A, market-risk (Item 7A / 10-Q Item 3), financial-statements (Item 8 / 10-Q Item 1), controls, and exhibits sections that were missing — e.g. Morgan Stanley 10-K Item 7 (MD&A) and Item 8 (financial statements), previously empty, now extract in full. Verified across the affected banks with **no change to filings that already worked** (10-K / 10-Q / 8-K extraction is otherwise byte-for-byte unchanged). No request-contract change: identical parameters and identical `text` / `html` response shapes. - 2026-06-15 (v3.54.0): **Section Extractor (`GET /api/v1/scrapping/extractor`): three out-of-sample extraction fixes.** (1) 10-K body headings written with a hyphen separator — `Item 7 - Management's Discussion` (e.g. Lowe's) — are now matched (the separator class previously accepted only period / colon / en-dash / em-dash). (2) The **last** 10-Q item — Part II, Item 6 (Exhibits) — is now bounded at the trailing **SIGNATURES** block instead of running to end-of-document, so it no longer swallows the signature block (e.g. Bank of America's Item 6 dropped from ~389,000 characters to the correct exhibit index). (3) `Other Items` is accepted as a synonym for the Part II, Item 5 (Other Information) heading (e.g. AbbVie). No request-contract change: identical parameters and identical `text` / `html` response shapes. - 2026-06-15 (v3.53.0): **Section Extractor (`GET /api/v1/scrapping/extractor`): 10-Q MD&A (Part I, Item 2) that returned empty for a class of filings now extracts.** Filings that print page furniture — bare page numbers and per-page "Table of Contents" back-anchors — between the MD&A heading and its first paragraph (e.g. Boeing) previously exhausted the body-detection window before reaching the prose, so the section came back empty; that chrome is now skipped so the body is found (Boeing 10-Q MD&A: empty → ~48,000 characters). The same fix also recovered some Part II, Item 6 exhibit indexes with the same layout. Scoped to the 10-Q path — 10-K and 8-K extraction is unchanged. No request-contract change: identical parameters and identical `text` / `html` response shapes. - 2026-06-15 (v3.52.0): `GET /api/insiders/transactions/search` documentation now matches the actual endpoint — fixing the `search_insider_transactions` MCP tool, which returned HTTP 422 on every call. The docs had declared four non-existent params (`ticker`, `insider_name`, `start_date`, `end_date`) and OMITTED the REQUIRED free-text `query` param (min 2 chars; matched case-insensitively against ticker / company name / insider name in a single field). Corrected to the real parameter set (`query`, `filed_at_days`, `transaction_days`, `transaction_code`, `standard_type`, `min_value`/`max_value`, `is_officer`/`is_director`/`is_ten_percent_owner`/`is_c_suite`, `exclude_10b5_1`/`exclude_cashless`/`exclude_sell_to_cover`, `sector`, `industry`, `dedup_owners`) and fixed the documented default `limit` (50, not 100). Docs / OpenAPI / MCP-schema accuracy fix — no backend change. - 2026-06-15 (v3.51.0): The `standard_type` filter on the insider transaction endpoints — `GET /api/insiders/transactions/latest`, `/transactions/by-ticker/{ticker}`, and `/transactions/search` — is now actually applied. It was previously advertised in the docs / OpenAPI / MCP tool schema but silently ignored by the backend, so `standard_type=purchase` returned a mix of sales, grants and exercises instead of only open-market purchases. It now maps FinRadar's 6 normalized buckets to the raw SEC Form 4 transaction codes — `purchase`→P, `sale`→S, `grant`→A, `exercise`→M/X/C/O, `gift`→G, `other`→every remaining code (F tax-withholding, D, J, K, …) — verified end-to-end as a complete 6-bucket partition of all rows. The parameter is now a validated enum: an unrecognized value returns HTTP 422 instead of silently returning unfiltered results. Response shape is unchanged; the raw `transaction_code` / `acquired_disposed_code` filters are unaffected. - 2026-06-15 (v3.50.0): **`GET /insider-module/api/insiders/transactions/latest` and `/transactions/top` gain an `include_total` query parameter (default `true`).** Set `include_total=false` to skip the exact `COUNT(*)` over the full matched set on these market-wide list endpoints: `meta.pagination.total` is then returned as `null` while `meta.pagination.has_more` stays exact (computed from a limit+1 fetch). On wide windows — e.g. `filed_at_days=3650` across all issuers or a whole sector/industry — the exact count is a multi-million-row scan that dominated response time; opting out returns the page in a fraction of the time. Backward-compatible and additive: the default is unchanged, so every existing caller still receives the exact `total`; only callers that pass `include_total=false` opt into the faster, count-free response (the response shape is identical apart from `total` being `null`). Entity-scoped endpoints (by-ticker / by-insider) are already selective and are intentionally not affected. - 2026-06-15 (v3.49.0): **Section Extractor (`GET /api/v1/scrapping/extractor`): broader coverage of the 10-Q "Financial Statements" section.** A class of 10-Q filings whose Part I, Item 1 (Financial Statements) previously returned empty now extracts correctly — including compact two-column income statements, statements introduced by a caption row (e.g. a "Condensed Consolidated Statements of Income" title before the figures), body headings written as "Item 1. Condensed Consolidated Financial Statements", and headings that use a hyphen separator ("Item 1. - Financial Statements"). Verified across the affected issuers (e.g. Microsoft, NIKE, McKesson, Verizon) with no change to filings that already worked, and 10-K / 8-K extraction is unchanged. No request-contract change: identical parameters and identical `text` / `html` response shapes. - 2026-06-14 (v3.48.0): **MCP Connect (OAuth 2.1) is live.** MCP clients (Cursor, Claude Desktop) can now connect to FinRadar's hosted MCP server with a one-click browser login instead of pasting an X-API-Key: the client runs the OAuth 2.1 authorization-code + PKCE flow against FinRadar's own login (with Dynamic Client Registration, RFC 7591), gets a short-lived RS256 access token, and calls tools on behalf of the signed-in user — tools debit that user's token balance exactly as API-key calls do. New users can create a free account during Connect (verify-first). Existing X-API-Key MCP clients are unaffected (dual auth). Discovery follows RFC 9728 (protected-resource) + RFC 8414 (authorization-server); the resource is `/api/mcp/`. - 2026-06-14 (v3.47.0): **Section Extractor (`GET /api/v1/scrapping/extractor`) now returns the COMPLETE item section.** The previous 6,000-character output cap (a leftover from an internal card-rendering engine) has been removed, so 10-K / 10-Q / 8-K extractions return the full section instead of a truncated excerpt — e.g. a large 10-K Item 1A (Risk Factors) now returns its full ~70,000 characters rather than the first 6,000. End-of-section boundary detection was hardened across all three form types so multi-section filings cut at the correct next-item heading (no over- or under-extraction), and a worst-case performance issue on minified single-line megabank filings (e.g. a 9 MB bank 10-Q) was fixed. No request-contract change: identical parameters and identical `text` / `html` response shapes — responses are simply longer and more accurate. - 2026-06-09 (v3.46.0): Hosted **MCP server** (Model Context Protocol) at `/api/mcp/` over streamable HTTP — exposes the public data REST operations as MCP tools for Claude and other MCP-compatible agents, generated at startup from `/openapi.json` so the tool surface tracks the API automatically. The surface is curated: operational/admin endpoints (admin, sql-proxy, sniper, bulk dumps) are excluded and the marquee data tools carry friendly names (e.g. `search_sec_filings`, `get_fund_13f_portfolio`, `latest_insider_transactions`). The MCP client passes its FinRadar API key as an `X-API-Key` header on the MCP transport; the server forwards it to the backend where the usual per-endpoint token cost, auth, and balance checks apply unchanged (the MCP server stores no key and adds no billing of its own). Routing mirrors the production split (insider vs main API) and calls the backends directly on the internal network. Stateless proxy — no new data and no contract change to any existing endpoint. - 2026-06-09 (v3.45.0): Machine-readable integration surfaces for AI agents and codegen — both generated from `apiDocs.js` (the same single source of truth as `/llms.txt`), so they track every endpoint change automatically. **(1) OpenAPI 3.1 spec at `/openapi.json`** covering the full public REST surface (150 operations across 145 paths): per-operation `operationId`, parameters (path / query / header), request bodies, security schemes (`X-API-Key` header, `apiKey` query, Bearer JWT), a per-endpoint `x-token-cost`, and a response example per operation. WebSocket streams are documented under an `x-websockets` extension (they are Socket.IO, not HTTP, so they are not REST paths). Lints clean under the repo Spectral ruleset (0 errors). **(2) Agent Skill** — `SKILL.md` + bundled per-section reference + a zero-dependency Python helper, published under `/skill/finradar-api/` for Claude and other agent runtimes that support the Skill convention. No endpoint or response contract changed — these are additive discovery + integration artifacts. - 2026-06-08 (v3.44.0): PR #426 — `GET /api/v1/scrapping/extractor` is now a dedicated sec-api.io Extractor replica scoped to **10-K / 10-Q / 8-K**, served by an isolated engine (`commercial_extractor.py`, zero shared code with the internal /business-description extractor). One in-house detection locates the section and renders it two ways: `type=text` (tags stripped, HTML entities kept literal, tables wrapped `##TABLE_START`/`##TABLE_END`) and `type=html` (style-PRESERVING — keeps inline font-weight/size/underline, drops color/background/positioning and all XSS via the nh3 allowlist). BREAKING contract changes vs the prior endpoint: (1) **20-F / 40-F are no longer supported** and return a refunded 400 (foreign annual reports are served by the OOM-safe /business-description card instead); (2) **10-Q items must be PART-PREFIXED** (`part1item2`, `part2item1a`) — a bare `item=1` is rejected so the Part I / Part II Item-number collision can never resolve to the wrong section; (3) **8-K items use dotted event codes** (`2.02`, `9.01`, …; hyphen `2-2` also accepted) and require `form_type=8-K`; (4) an **invalid item-for-form now returns a refunded 400** instead of a silent wrong-200. A valid-but-absent item still returns a refunded 404. The public URL is unchanged. - 2026-06-08 (v3.43.0): PR #422 — the fund-level `activity` block's `turnover_pct` (from `GET /api/v1/form-13f/fund/{cik}?include_activity=true`) now divides by the 'total # of holdings for the quarter' = current holdings PLUS the names fully sold out this quarter, matching WhaleWisdom note 1: `turnover_pct = (new_count + sold_out_count) / (holdings_count + sold_out_count) × 100`. Previously it divided by `holdings_count` alone, dropping the sold-out names and over-reporting turnover — e.g. Berkshire Hathaway 2026-03-31 read 65.52% (19/29) instead of WhaleWisdom's published 42.22% (19/45), confirmed live end-to-end. `turnover_alt_pct` and `net_flow_pct` already used the correct flow and are unchanged. Computed live per request — no stored column, no backfill. Display-only metric on the filer-page 13F Activity card; the response shape is byte-identical (same field, corrected value). - 2026-06-07 (v3.42.0): PR #418 — `GET /api/v1/form-13f/fund/{cik}/cusip/{cusip}/history` now SPLIT-ADJUSTS `history[].shares` (forward-adjusted to the latest split basis via the Sharadar split feed, ticker-keyed) so a stock split is no longer mislabelled as a phantom buy/sell. Previously `shares` was as-filed: AAPL's 2020 4:1 split made Berkshire's count jump 245.2M → 944.3M, rendering as a fake +699M 'buy' that dwarfed real trades on the filer-page transaction-timeline chart. That quarter now correctly shows the real -36.3M trim (verified against Dataroma: 2019-12-31 = 980,622,264). NEW additive response fields: `shares_raw` (literal as-filed count, per row), `split_adjusted` (bool), and `splits` ([{date, ratio}] applied). `value` is unchanged (already split-invariant). SELECT-only; degrades to raw shares if the ticker is unresolved or the split-feed read fails. No frontend change required — same field names, corrected values, plus additive fields callers can ignore. - 2026-06-04 (v3.41.0): `GET /api/v1/scrapping/extractor` `type=html` now returns STRUCTURED HTML (``/`

`/``/``/`` markup, server-side allowlist-sanitized with nh3) instead of the legacy `
`-in-`
` wrapper. It is powered by a new IN-HOUSE BeautifulSoup extractor (no edgartools on the hot path) that places ~99% of 10-K and 20-F item sections directly (88/89 on the regression benchmark, all foreign 20-F strategic reports recovered); on the rare filing it cannot locate (e.g. a back-of-filing cross-reference index with no item heading), it transparently falls back to the edgartools cascade, then to the legacy `
` shape — so a miss never regresses below the prior behaviour. Read the `X-Extractor-Mode` response header (`structured` | `legacy_fallback` | `legacy_html` | `pending` | `notfound` | `text`) to know which path produced the body. The in-house path is also markedly faster (no edgartools accession round-trip or green-thread deadline machinery) — warm requests stay sub-second. `type=html_structured` (opt-in, edgartools-first) and `type=text` are unchanged. Cost unchanged (25 tokens).
- 2026-06-03 (v3.40.2): PR #388 — `type=html_structured` no longer returns a gateway 504 on the FIRST (cold-cache) request for a large foreign 20-F. The structured fetch+parse now runs under a wall-clock deadline in an eventlet thread-pool worker so the deadline reliably fires; on a cold cache the server returns HTTP 503 with `X-Extractor-Mode: pending` + a `Retry-After` header (NOT billed) while it prepares the section in a background thread that warms the cache, so a retry a few seconds later returns the finished `structured` body. The slow legacy fallback fetch is bounded the same way (no more unbounded cold fetch). Env knobs: `EXTRACTOR_USE_TPOOL` (default on), `EXTRACTOR_LEGACY_DEADLINE_SEC` (default 15s). No change to warm requests or to the `text`/`html` legacy shapes.
- 2026-06-03 (v3.40.1): PR #386 (+ review hardening) — the `type=html_structured` extractor cascade gains an 8th and final layer (L8) that recovers `Item 4` ('Information on the Company') for UK/AUS LSE 20-F filers — Barclays (BCS), HSBC, Unilever (UL), Rio Tinto (RIO) and peers — whose UK FCA strategic-report headings arrive through edgartools as plain prose with no `#`/`**bold**` markers, defeating the structural probes in layers 1-7. L8 substring-matches the FCA strategic-report taxonomy (generic across LSE filers, no per-ticker logic) and returns a body slice for these filers where the endpoint previously fell back to the legacy `
` shape (`X-Extractor-Mode` flips from `legacy_fallback` to `structured` for this filer class). L8 fires ONLY for 20-F Item 4/4A after layers 1-7 all return nothing, so no existing 10-K/40-F or already-handled 20-F output changes. Review hardening on top of the original PR: each returned slice is bounded by a hard upper cap (no run-away whole-document capture), the anchor lists are de-duplicated, the cross-reference-TOC filter now also rejects em-dash (U+2014) separators, and the layer ships with offline unit tests. Cost unchanged (25 tokens).
- 2026-06-03 (v3.40.0): PR #383 (+ review hardening) — `GET /api/v1/scrapping/extractor` gains an OPT-IN structured-HTML mode `type=html_structured`, backed by a 7-layer edgartools-v5 cascade (clean ``/`

`/``/``/`

` markup, server-side allowlist-sanitized with nh3 so it is safe to render in a browser). The legacy `type=html` (extracted text wrapped in `
` inside `
`) and `type=text` shapes are UNCHANGED — existing consumers see no difference, and the structured shape ships ONLY when explicitly requested (zero-disconnect; a future release may flip the default after consumer migration). Covers 10-K and 20-F (foreign private issuers); 40-F (Canadian filers) is best-effort. Additional hardening: the endpoint now returns a stable `X-Extractor-Mode` response header (`text` | `legacy_html` | `structured` | `legacy_fallback` | `notfound`); a not-found item returns HTTP 404 (and is NOT billed) instead of a 200; the `url` is validated to be on `sec.gov` for every path; and the response body is the raw extracted string (NOT a JSON envelope — the prior `responseSchema` JSON fields never matched the actual response and have been corrected). On any structured-cascade failure (e.g. transient SEC access) the response transparently falls back to the legacy `html` shape, signalled via `X-Extractor-Mode: legacy_fallback`. Cost unchanged (25 tokens). - 2026-05-31 (v3.39.0): PR #366 — new opt-in `?include_activity=true` query param on `GET /api/v1/form-13f/fund/{cik}` adds a fund-level `activity` object with WhaleWisdom-parity quarter-over-quarter portfolio metrics: prior-quarter market value + `market_value_change_pct`, `inflow_value`/`outflow_value` (USD of newly-opened / fully-exited positions), `net_flow_pct` = (inflow−outflow)/market_value, `turnover_pct` = (new+sold_out)/holdings_count, `turnover_alt_pct` = min(inflow,outflow)/market_value, the new/sold_out/increased/decreased/unchanged counts, and `time_held_top10_qtrs`/`top20`/`all` (average holding period in quarters over the top-10/top-20/all positions by value). Computed REQUEST-TIME from the current + prior-quarter holdings the report already loads — no extra quarter fan-out — plus at most one indexed lookup into the `sec_13f_holdings_analytics` warm cache for the time-held averages. Powers the `13F Activity` card on the `/filer/-/` page in a single call. Fund-wide regardless of `?status=` (snapshotted before the status filter). Prior-dependent metrics are `null` on a fund's first filing; the three `time_held_*` fields are best-effort and `null` until the Phase 79 cost-basis analytics cache is warm for that `(cik, period)` (the other scalars always populate). Applies to `detail=full` only. Default `false` keeps the byte-identical pre-PR response shape (no `activity` key); cache key partitions on the new `:activity` dimension so pre-flag entries stay valid. Same opt-in flag pattern as `include_options` / `include_analytics` / `?status=`. - 2026-05-31 (v3.38.0): New real-time WebSocket stream — `/13dg` (13D/G Live Stream). Schedule 13D/13G beneficial-ownership filings are now pushed live (Socket.IO namespace `/13dg`, HTTP path `/socket.io`) the moment they are parsed and committed. A single `13dg_filing` event per filing carries the COMPLETE detail object — byte-identical to `GET /api/v1/ownership/beneficial-ownership/filings/{accession}` (all filers, positions with sole/shared voting & dispositive power, classified purpose + intent flags, exhibits, structured items, and for amendments the prior/current % + threshold-crossing delta block) — plus an `event` tag and `is_catchup` flag. Server-side `subscribe` filters: issuer (CIK + ticker), filer (CIK + name substring), form type (SC_13D / SC_13G / +amendments), min ownership %, isAmendment, isConversion, purpose category, and threshold crossings (5/10/15/20%); all optional, ANDed, empty = match-all. Catch-up replays recent matching filings on connect; subscriptions persist 7 days across reconnects; `13dg_heartbeat` every 30s. Auth + tiering + billing mirror the `/13f` stream (api_key via Socket.IO auth payload / X-API-Key / Authorization / ?api_key=; Free=1 / Paid=5 concurrent per namespace; flat 100-token/UTC-day connection fee, charged separately per namespace; billing currently OFF). Runs on a dedicated `sec_13dg` Celery queue + worker so 13F deadline-day storms cannot delay the live 13D/G feed. No REST contract change. - 2026-05-20 (v3.37.1): PR #295 — `GET /api/v1/sec/filings/{accession_number}/html` now returns the INFORMATION TABLE (positions) for `13F-HR` / `13F-HR/A` filings, not the cover page. Previously the endpoint returned the rendered cover page (`primary_doc.xml`, Form 13F front matter) for every 13F-HR filing because the stored `primary_html_url` pointed there — the actual positions live in a separate XML file in the same accession folder (`xslForm13F_X02/.xml`) and were unreachable from the consumer iframe. The handler now skips the cached `primary_html_url` for 13F-HR and parses the index page's Document Format Files table, picking the row whose `doc_type` is `INFORMATION TABLE` (preferring the XSL-rendered URL so SEC's server-side XSLT returns styled HTML with COLUMN 1-8 position headers). Existing cached cover-page content is detected on read (title contains `FORM 13F COVER PAGE` and not `FORM 13F INFORMATION TABLE`) and re-fetched automatically — no backfill or migration required, no client action needed. Affects 13F-HR + 13F-HR/A. Unchanged for 10-K, 10-Q, 8-K, Form 4, 13D/G — those forms' `primary_html_url` already points to a `.htm` file. User-visible: `finradar.ai/sec/<13f-accession>/` now renders the holdings list instead of just the cover sheet. - 2026-05-19 (v3.37.0): PR #285 — new `?status=` query param on `GET /api/v1/form-13f/fund/{cik}` for server-side filtering of the holdings list to a single position-classification bucket. Valid values: `all` (default — no filter, byte-for-byte pre-PR behavior), `new`, `increased`, `decreased`, `unchanged`, `closed`. Powers the New / Increased / Decreased / Unchanged / Closed status pills on the `/filer/-/` page where client-side filtering breaks down for BlackRock-class filers (5K+ holdings, only a partial buffer in memory at any time). `closed` is special-cased — closed positions are absent from the current `holdings` list (in prior quarter, not current), so the handler synthesizes standard-shape rows from the `closed_list` analysis bucket with `shares=0`, `value=0`, `change_percent=-100`, `status='Closed'`, and the prior values preserved as `shares_sold` / `value_sold`. `meta.pagination.total` reflects the FILTERED count so pagination stays correct across pill switches; `analysis.*.total` continues to reflect the UNFILTERED per-category totals so the pill counts themselves stay accurate. Cache key partitions on the new dimension; default `all` omits the suffix so pre-PR cache entries remain byte-for-byte valid. Mirrors the precedent on `GET /api/v1/tickers/{ticker}/holders?status=`. Unknown values silently fall through unchanged (same convention as `sort_by`/`sort_order`). - 2026-05-19 (v3.36.1): Frontend now ships real pages for the forgot/reset password flow (no API contract change). Previously the backend's `POST /api/v1/auth/forgetpassword` + `PUT /api/v1/auth/newpassword` were live but the matching `/forgot-password` and `/reset-password` SPA routes did not exist, so the 'Forgot password?' link on the login screen and the 302 redirect from `GET /api/v1/auth/verifyPasswordLink` both hit the catch-all and bounced users to `/`. This release adds those two pages: `/forgot-password` posts to `/auth/forgetpassword` and shows the anti-enumeration generic toast; `/reset-password` reads the `?token=` (or `?error=invalid_or_expired`) emitted by `verifyPasswordLink` and PUTs `{token, newpassword}` to `/auth/newpassword`. On success the user is redirected to `/login?reset=success`, which the Login page now renders as a green success banner (matches the existing `?verify_email=success` pattern). Two small backend changes ride along: (1) `users.password_changed_at TIMESTAMP NULL` column added, written by `/auth/newpassword` on a successful reset (and backfilled to `COALESCE(updated_at, created_at)` for existing rows) — surfaces a 'Last changed' line on the future `/account/security` page and stamps the new notification email; (2) on a successful password reset the user now receives a 'Your FinRadar password was changed' email with a self-rescue 'Reset password' button — closes the security-audit P0 'no notification of security-relevant events to user email' gap. No API contract change for any caller — bodies, status codes, headers, and the `verifyPasswordLink` 302 target are byte-identical to v3.36.0. - 2026-05-18 (v3.36.0): News section removed from public documentation pending commercial release. The 4 endpoints (`GET /api/v1/news/feed`, `GET /api/v1/news/ticker/{ticker}`, `GET /api/v1/news/sources`, `GET /api/v1/news/stats`) are sourced from a separate aggregated-news pipeline that is not part of the launch offering and will not be billable. The section is no longer reachable from the sidebar, the `/docs` router, the `/docs/llms-txt` viewer, the `/llms.txt` LLM-fetch surface, the Cmd+K search palette, or the public sitemap. Backend routes are unchanged for the moment; the section will be re-surfaced in apiDocs when the underlying pipeline is ready to commercialize. No client-facing breaking change — these endpoints were never advertised as a billed product. - 2026-05-18 (v3.35.0): Pre-launch close-out batch (issues #260 / #261 / #276). **(1) `GET /api/v1/account/transactions` rows now carry `response_status` (#260).** Each debit row in the response now includes the HTTP status code the handler returned for that request (200 / 400 / 401 / 403 / 404 / 500 / etc.). Refund rows stay `null` (they're bookkeeping follow-ups, not responses). Pre-migration rows also stay `null` (the column was just added). New nullable `SMALLINT` column on `token_transactions`; populated by the token meter via an UPDATE after the handler returns. Useful for ops + users to audit error-billed vs success-billed history (combined with the auto-refund from #254, the typical error row appears as a `debit` with status=4xx/5xx paired with an immediate `refund` row — net-zero on the ledger, but now visibly correlated). **(2) `GET /api/v1/tickers/{ticker}/overview` negative-cache TTL reduced from 1 hour to 60 seconds (#261).** Previously a single transient Yahoo Finance throttle / hiccup locked the ticker out of the endpoint for an hour because the 404 response was cached for 3600s. Most 404s here aren't 'ticker doesn't exist' — they're 'yahooquery's upstream momentarily refused us' — so an hour-long lockout was wildly disproportionate. The successful-response cache stays at 24h (unchanged). Also added an `INFO` log on the 404 path with the inferred yahooquery shape so ops can correlate spikes with Yahoo's throttling. **(3) Local-dev: insider_api on `localhost:5002` (#276) is unreachable via host port-forward on Windows Docker Desktop (TCP connects but server closes without responding). Internal Docker network access (`http://insider_api:8000/health` from another container) works fine, and production routes via nginx — so the issue is local-dev only and not user-visible. Documented for future devs; no code change needed.** No API contract changes other than the additive `response_status` field on `/account/transactions` rows. - 2026-05-18 (v3.34.0): Auth UX polish bundle (issues #257 / #258 / #262 / #263). **(1) `GET /api/v1/auth/verifyEmail` no longer renders a JS `alert()` + `window.close()` (#257).** Modern browsers refuse to close tabs the user opened from an email link, so the legacy behaviour left a blank page after dismissing the alert — terrible first impression. The handler now 302-redirects to the FinRadar frontend at `/login?verify_email=success` on success and `/login?verify_email=invalid` on any failure (anti-enumeration: same `invalid` code for token-decode-fail, wrong purpose, unknown user, and already-consumed row). The Login page reads the query param and renders an emerald banner ("Email verified. Please sign in.") or a red one. **(2) Welcome email no longer carries the raw API key in the body (#258).** Mailbox compromise alone used to be enough to take over the account's API access (key sat in inbox forever, archive-readable). The email now shows a 'Get your API key →' CTA linking to `/account/credentials` on the frontend; user must log in with their password to view + copy + rotate. The first-call curl example uses a `YOUR_API_KEY_HERE` placeholder. Signature change: `send_welcome_email(email, credentials_url, docs_url=..., feedback_email=...)` — `api_key` arg removed. The register handler now passes `_frontend_base_url() + '/account/credentials'`. **(3) Register form client-side validates password length (#262).** The form already checked password ≠ confirmPassword; it now also checks `password.length >= 6` (the same threshold the server-side validator enforces) so a too-short attempt doesn't round-trip to the server. Pure UX, server is still the security boundary. **(4) Register form no longer sends hardcoded `first_name: 'User'` / `last_name: 'Name'` (#263).** The backend `register()` handler doesn't read those fields and the `users` table has no such columns; the hardcoded sentinels were misleading dead code. Dropped from the POST body. All four are UX hardening — no public-contract changes, no migration. - 2026-05-18 (v3.33.0): Rate-limit infrastructure hardening (issues #255 / #256). **(1) Flask-Limiter switched to Redis backend (#256).** Previously the limiter used per-worker in-memory storage, which meant the documented `60 per minute per IP` global cap became `60 × N workers per IP` in practice (the limiter's startup warning had been firing on every container boot). Storage is now `redis://redis:6379/2` (dedicated DB number to avoid collision with Celery on db 0 and Socket.IO session data on db 1). Override via `RATELIMIT_STORAGE_URI` env var. Single shared counter across every worker; container restarts no longer reset the rolling window. **(2) Rate-limit headers now emitted on every response (#255).** Every limited endpoint now carries four headers that clients (incl. our own Try-It panel) can use for proper backoff: `X-RateLimit-Limit` (the cap, e.g. 60), `X-RateLimit-Remaining` (requests left in the current window), `X-RateLimit-Reset` (Unix epoch when the window resets), and `Retry-After` (seconds until you can retry — particularly useful on the 429). All four are added to `Access-Control-Expose-Headers` so browsers don't strip them on cross-origin reads. **Behavioural note**: the headers ride along on 200 OKs as well, so frontend code can render a `requests remaining` indicator pre-emptively rather than waiting for the first 429. Backwards compatible — no public-contract changes, no migration needed; clients that don't read the new headers see no difference. - 2026-05-18 (v3.32.0): Pre-launch security hardening pass #3 (issues #269 / #274). **(1) `X-Request-ID` idempotency on every metered endpoint (#274).** When a client sends an `X-Request-ID` request header, the token meter dedup-checks against prior debits — if a non-refunded debit row already exists for `(user_id, X-Request-ID)`, the request short-circuits the bill (returns `X-Tokens-Charged: 0`) but still hits the handler so the client receives a fresh response. This lets retry loops (axios-retry, urllib3, server-to-server clients re-driving after a transient network failure) safely replay without double-billing the user. Refunded prior debits do NOT count for dedup: a 5xx that was auto-refunded (per #254) followed by a successful retry with the same X-Request-ID bills the retry fresh — the user paid for nothing the first time. New nullable column `token_transactions.idempotency_key` + partial unique index `uq_token_transactions_user_idem_debit ON (user_id, idempotency_key) WHERE idempotency_key IS NOT NULL AND reason = 'debit'`. Backwards compatible: callers that don't supply `X-Request-ID` keep the prior per-request billing semantics. **(2) `email_verify` table now carries a `purpose` discriminator (#269).** Previously the `email_verify` table was shared by the email-verification AND password-reset flows without a discriminator, which allowed a row issued for one flow to be silently consumed by the other flow's handler (e.g. a mistakenly-clicked password-reset link verified the user's email; a mail-preview prefetch on an email-verification link burned the user's reset token). The table now has `purpose VARCHAR(32) NOT NULL DEFAULT 'email_verification'` with composite uniques `(uuid, purpose)` and `(email, purpose)` (the legacy single-column uniques on uuid + email were dropped). JWTs issued by `/auth/register`, `/auth/resend-verification`, `/auth/forgetpassword` now carry a `purpose` claim in the payload (`email_verification` | `password_reset`), and the verifying handlers (`/auth/verifyEmail`, `/auth/verifyPasswordLink`, `/auth/newpassword`) reject tokens whose `purpose` doesn't match. Invite-redemption tokens are tagged `purpose=invite` for defense-in-depth (no cross-consumption was ever possible anyway since invite tokens don't touch the email_verify table). **Backwards compatibility:** the JWT payload's `purpose` claim defaults to `email_verification` when absent (so legacy verify-email tokens still work); password-reset rows in flight at the time of the migration are silently re-classified to email_verification and become unusable for password-reset — the affected user just requests a fresh `/forgetpassword` (no manual intervention required). The composite uniques replace the per-user single-row constraint, so a single user can now hold one email_verification row AND one password_reset row simultaneously. - 2026-05-18 (v3.31.0): Pre-launch security hardening pass #2 (issues #259 / #272 / #273 / #275). **(1) `POST /api/v1/auth/login` brute-force lockout (#272).** Per-account consecutive-failure counter: 5 fails → 60s lockout, 10 → 5min, 20 → 1hr. During lockout the endpoint returns HTTP 429 with a `Retry-After` header in seconds — even if the supplied password is correct (intentional: an attacker must not be able to fish for whether they've guessed right inside the lockout window). A successful login resets the counters to zero. New columns on `users`: `failed_login_count INT NOT NULL DEFAULT 0`, `lockout_until TIMESTAMP NULL`. **(2) `POST /api/v1/sql-proxy` error sanitization (#273).** DB-runtime errors (division by zero, type mismatch, etc.) no longer echo the raw psycopg2/SQLAlchemy trace + offending SQL back to the caller — those revealed the backend stack and helped blind-SQLi recon. Response body is now a generic `{success: false, error: 'Query execution failed.'}` with HTTP 500 (was HTTP 400 + full trace). Full detail logged server-side with the request_id for ops forensics. 500 also lets the token meter's auto-refund branch credit back the 25-token charge (the user didn't make a bad request; the DB couldn't satisfy a syntactically-valid one). Policy-rejection errors (forbidden keyword, disallowed table) still emit their short non-revealing messages at 4xx — those are also AUTO-REFUNDED per the 2026-05-18 batch-1 #254 change. **(3) HTTP 402 payload enriched (#259).** `current_balance < cost` responses now also include `plan_display_name` and `monthly_quota` so SDK consumers can render 'X / Y free tokens used' without a round-trip to `/account/balance`. The pre-existing `current_balance`, `required_cost`, `next_refill_at`, `plan`, `upgrade_url` keys are unchanged. **(4) Wrong HTTP method now returns 405 (#275).** A global `werkzeug.exceptions.HTTPException` errorhandler was added + an explicit 405 handler with `code: METHOD_NOT_ALLOWED`. Previously any non-handled HTTPException (405 / 406 / 413 / 415 / 422 / 423 / 501 / 502 / 503 / etc.) fell through to the `Exception` catchall and emerged as 500 — that hid real server bugs in 5xx telemetry and triggered the token meter's 5xx refund branch unnecessarily. The explicit per-status handlers (400, 401, 403, 404, 429, 500) are unchanged. **Backwards compatibility:** the lockout response is a NEW 429 status on `/login` (callers that already handled 429 elsewhere — e.g. global rate-limit — see no shape change); the sql-proxy status flip 400 → 500 changes which branch retry libraries take, but the success / 401 / 403 paths are unchanged; the 402 body adds two fields (existing parsers ignore unknown fields); 405 callers that were getting 500 should switch to checking 405. No client-config changes required for FinRadar's own frontend. - 2026-05-18 (v3.30.0): Pre-launch security hardening pass (issues #254 / #264 / #265 / #266 / #267 / #268 / #270 / #271). **Auth + billing changes that touch the public contract:** (1) `POST /api/v1/auth/forgetpassword` — `weblink` body field is REMOVED; the reset link in the email now always points at the FinRadar frontend's `/reset-password?token=...` page (server-controlled). The endpoint now always returns 200 with the generic message `If that email is registered, a password-reset link has been sent.` regardless of whether the email is registered (anti-enumeration). (2) `PUT /api/v1/auth/newpassword` — **BREAKING:** `userId` body field is REMOVED; the endpoint now REQUIRES `token` (the reset JWT from the email link). Previously the endpoint accepted only `{userId, newpassword}` with no auth — a full account-takeover vector for anyone who knew a target's UUID. The token is validated against the live `email_verify` row and consumed atomically with the password update; all active sessions are invalidated on success (OWASP). (3) `GET /api/v1/auth/verifyPasswordLink` — the `link` query param is now IGNORED. The handler always 302-redirects to the FinRadar frontend's `/reset-password?token=...` page. The previous open-redirect (`?link=https://evil.example.com`) is closed; the user's UUID no longer appears anywhere in the redirect URL. (4) `POST /api/v1/auth/login` — error messages for 'unknown email' and 'wrong password' are now identical (`Invalid email or password`) at status 401 (was 400). Closes the email-enumeration channel. (5) `POST /api/v1/auth/resend-verification` — already-verified accounts now receive the same generic 200 message as unknown emails, instead of a distinguishable 400. **Auth + billing changes that are invisible to well-behaved callers:** (6) The `Origin: https://finradar.ai` (and 5 other finradar.* origins) auth + billing bypass on metered endpoints is REMOVED. CLI/non-browser callers spoofing that Origin no longer get free unauthenticated data. (7) The token meter now REFUNDS 4xx responses (previously only 5xx). 401 / 403 / 400 / 404 calls show `X-Tokens-Charged: 0` and produce a paired `debit` + `4xx_refund` row in `/account/transactions`; net-zero on the ledger. Closes the `/sql-proxy` abuse vector where a malicious caller could drain a victim's bucket via `DROP TABLE` 403s. (8) JWT `session_id` invalidation now applies to every JWT-accepting decorator (`api_auth_required`, `account_auth_required`, `jwt_required_custom`). A JWT whose session has been kicked (device-limit eviction or password reset) now 401s across the full metered surface, not just the account-billing endpoints. - 2026-05-17 (v3.29.0): New endpoint (PR #248) — `GET /api/v1/form-13f/aggregation/by-industry` — industry-first mirror of `/aggregation/by-sector`. Returns a flat industries array with sector context nested per industry, instead of grouping industries under their parent sector. Answers cross-sector questions like 'which industries had the biggest fund inflows this quarter regardless of sector?' in a single round-trip, no client-side flatten+re-rank required. Same `sec_13f_sector_summary` data source, same 2-level top-movers resolution (`cusip_mappings.sector` → `cusip_mappings.ticker → all_stocks_list`), same `coverage_pct` math, and same `quarter_metadata` block as the sector endpoint. Params: `quarter` (defaults to latest), `compare=prev` (adds per-industry QoQ deltas: `flow_change`, `prev_momentum_label`, `momentum_shift`, `fund_count_change`, `value_change_pct`), `limit` (default 50, max 200, or `all` — when no `sector` filter set, returns top-N inflows + top-N outflows; when `sector` filter set, returns top-N by `abs(metric)`), `sort_by` (`fund_flow` or `significance`; legacy `sort` alias accepted), `sector` (optional single-sector drill-down — skips the inflow/outflow split). Free — `cost: 0` token charge. No backend schema changes; no impact on `/by-sector` or any other route. - 2026-05-16 (v3.28.1): Sniper accession-base prediction now queries the filer's actual latest accession from the local DB index (indexed by LEFT(accession_no,10) prefix + 2-digit year) rather than using a stale in-memory snapshot. Both self-filing and historical third-party filer prefixes are probed in parallel, ranked by filing recency. Year boundaries reset automatically (December near-exhaustion + calendar-year rollover). A background rebase loop (SNIPER_REBASE_ENABLED flag) periodically refreshes candidate_urls during the active sniper window via a SQL-guarded UPDATE that no-ops on completed targets. New sniper_targets columns: baseline_source (db_primary / legacy) and last_baselined_at. Internal improvement — no API surface change. - 2026-05-15 (v3.28.0): Sniper hits now trigger full 13F parse + holdings-aware notification (stages: filing_acquired, enriched, no_holdings, failed_fallback). New response fields on /api/v1/sniper/targets (both GET and POST): filing_uuid (FK to filings.uuid once parsed), processing_state (one of detected/parsing/parsed/enriched/no_holdings/parse_failed — orthogonal to status), holdings_count (number of form_13f_holdings rows when parsed or later), parsed_at (ISO 8601 UTC timestamp when parse completed), enriched_at (ISO 8601 UTC timestamp when OpenFIGI enrichment completed), candidate_urls (range-scan window of predicted [{accession, url}] objects assembled at target creation). WebSocket payload (event=snipe_success) gains: stage, holdings_count, tickers_resolved, tickers_pending, period_of_report, view_url. - 2026-05-14 (v3.27.0): Phase 72-02 — on-the-fly fund analytics pipeline + deadline snapshot backfill. (1) **5-15 min latency for multi-period returns.** `process_filing` now enqueues into a new `pending_fund_analytics` queue table at the end of every successful 13F parse (gated on `ENABLE_FUND_ANALYTICS_HOOK=true` env var; reparse / data-correction paths explicitly opt out via `enqueue_analytics=False`). A new Airflow DAG `fund_analytics_drain` (every 5 min, `light-ops` pool) claims pending rows, groups by quarter, and dispatches batches of ≤50 CIKs to a new Celery task `compute_fund_perf_batch` on heavy_ops that performs: per-CIK quarter-row UPSERT (via new `FundPerformanceService.compute_for_cik_batch`) → daily-returns extension → multi-period rollup → cache invalidation. Compared to the prior 8h `market_analytics` cadence + 24h `fund_analytics_daily` cadence, this is a 100× latency improvement for new filings going forward. (2) **Pre-deadline backfill of ~7,500 non-tracked CIKs.** New one-shot Airflow DAG `fund_analytics_snapshot_backfill` (paused on creation, manual trigger) populates `return_1y/3y/5y/10y/inception`, `sp500_return_*`, `alpha_*` for every non-tracked CIK currently NULL on `sec_13f_fund_performance`, using a quarter-snapshot rollup method (`return_N = portfolio_value[q] / portfolio_value[q-N*4] - 1`) instead of the daily mark-to-market method. The 580 tracked funds keep their daily-method values untouched (decision C7 hybrid methodology). Verified `<1%` drift vs the daily method on Berkshire / BlackRock / Lone Pine reference funds via the new `scripts/ab_verify_snapshot_rollup.py` C8 gate (must PASS before unpausing the backfill DAG). Full 7,500-CIK run estimated ~12–15 min wall-clock on heavy_worker, ordered by `MAX(portfolio_value) DESC NULLS LAST` so famous funds populate first if the run is interrupted. (3) **Postgres advisory lock** on `quarter_date` (`pg_advisory_xact_lock(hashtext('fund_perf_q_' || quarter_date))`) serializes `compute_quarter`'s whole-quarter DELETE+INSERT against the new per-CIK upserts so concurrent runs can't corrupt each other's writes. (4) **30-min escape hatch** on stuck claimed rows in `pending_fund_analytics` (`failure_count < 3` AND (claimed > 30 min ago OR claimed_at IS NULL)) so worker crashes or dispatch-skip scenarios don't permanently strand pending work. **Backwards compatibility:** every existing `/api/v1/form-13f/fund-performance` caller sees byte-identical response shapes; only delta is that previously-NULL multi-period columns now carry values for all 8K+ filers. **H8 recommendation for finradar.ai:** when a response row has `return_1y=null AND portfolio_return_pct!=null`, render a 'Returns computing — refreshes shortly' placeholder in the perf card section to signal the 5–15 min backfill latency (covers the brief window between new-filing parse and drain-DAG processing). - 2026-05-14 (v3.26.0): Phase 72/leaderboard enhancements (PR #185) — `/api/v1/form-13f/fund-performance` leaderboard mode gains four additive features for the new Fund Performance Leaderboard table on FinRadar.ai's hedge-fund-analyzer page: (1) **`?q=` search** — same filer-universe match as [/filers](/docs/institutional-holdings/form-13f-api/get-form-13f-filers) (`company_name` / padded CIK / unpadded CIK / `signature_name` / `filer_aliases` famous-manager lookup like `buffett` → Berkshire). Empty matches return `data.leaderboard=[]` (NOT 404) so frontends can render an empty-state UI while `quarter_metadata` + `pagination` stay valid. Logic extracted into a shared `_search_filer_ciks(q)` helper that both `/filers` (autocomplete) and `/fund-performance` (leaderboard) now call — guaranteed identical match pools. `/filers` keeps its alias-priority `ORDER BY` ranking (alias-matched filers float to top) since leaderboard sort is user-driven. (2) **`?has_returns=true` filter** — drops rows where `return_1y IS NULL` to hide the ~75% multi-period-rollup coverage gap. Default `false` keeps the full filer universe browsable; FinRadar's leaderboard UI defaults this toggle ON. (3) **Sort whitelist extended from 4 → 20 keys** — adds `return_1y/3y/5y/10y/inception`, `sp500_return_1y/3y/5y/10y`, `alpha_1y/3y/5y/10y`, `portfolio_turnover`, `buy_value`, `sell_value`, `company_name`. The Phase 62-01 and Phase 72-01 multi-period rollup columns are now finally exposed as sort keys. Pre-existing keys (`portfolio_return_pct`, `portfolio_value`, `holdings_count`, `price_coverage_pct`) unchanged. Both `sort_by` and the legacy `sort` aliases work. (4) **`data.available_quarters` on first page** — distinct list of `quarter_date` values (newest-first) present in `sec_13f_fund_performance`, returned ONLY when `offset=0`. Lets frontends populate a quarter-selector dropdown without a second round-trip. Omitted on infinite-scroll continuations to keep them lean. **Default-behavior preservation:** every existing caller (no `?q=`, no `?has_returns=`, no new `sort_by` values) hits the SAME SQL filters + SQL sort + response shape as v3.25.0; only delta is the additive `data.available_quarters` field on `offset=0` responses (existing parsers ignore unknown fields). - 2026-05-13 (v3.25.0): New endpoint (PR #184) — `GET /api/v1/form-13f/fund/{cik}/cusip/{cusip}/history` returns the per-quarter share + value history for a single (fund, CUSIP) position. One row per 13F-HR quarter where the position appeared, carrying `quarter_date`, `shares` (as-filed, not split-adjusted), `value` (USD), `shares_delta` (QoQ), and `transaction_type` ∈ {`new`, `increased`, `decreased`, `unchanged`}. Filing selection per quarter mirrors `CostBasisService` / `/fund/{cik}` exactly — pick the filing with MOST non-option holdings (#264 amendment-handling) so a 4-holding 'correction' 13F-HR/A doesn't overwrite a 36-holding original. CUSIP-keyed (not ticker) so corporate-action chains (mergers, spinoffs, the SIRI 2024 reverse-split CUSIP flip `82968B103 → 829933100`) don't break the timeline. Best-effort ticker + `security_name` enrichment via `cusip_mappings`. CUSIP shape validation: alphanumeric only, ≤ 12 chars → 400 BAD_REQUEST. Unknown (cik, cusip) combination → 404 NOT_FOUND. Closed positions stop appearing in the response when the fund no longer holds the security — no synthetic 0-share row is emitted. Powers the chevron-expanded transaction-timeline chart on FinRadar.ai's `/filer/-/` page (replaces the placeholder 'Transaction-timeline chart coming next — needs per-ticker quarterly history endpoint'). Free — `cost: 0` token charge. - 2026-05-13 (v3.24.0): Phase 73 — options visibility two-PR bundle: (1) `normalized_holdings_deduped` matview partition key extended (PR #181) to include `put_call_options`, recovering ~1.49M previously-collision-dropped option rows for filers holding BOTH common stock AND options on the same underlying CUSIP (legal per SEC 13F-HR spec). Pre-fix: Duquesne Family Office (CIK 0001536411) Q4 2025 showed 2 of 4 Call options on `/api/v1/form-13f/fund/{cik}?include_options=true` (SPY + IWM survived; EWZ $134M Call + AMZN $23M Call were silently dropped because Duquesne also held the underlying common). Post-fix matview rebuild (22m 45s wall-clock, atomic `--single-transaction` swap on UAT 2026-05-13): all 4 Calls now visible, total matview row count 72.70M → 74.72M. Backwards-compatible — filers holding only common OR only options on a given CUSIP see no behavioral change (the new fourth partition dimension is single-valued for them). (2) New `?options_only=true` query parameter on `/api/v1/form-13f/fund/{cik}` (PR #182). When `true`, response carries ONLY options rows (`is_option=true`) — overrides `include_options`. Lets clients fetch the Options-tab payload for `/filer/-/` without pulling the full equity book (2K-5K rows for BlackRock-class; 0 rows for Berkshire). Cache key partitions independently with `:options-only` suffix replacing `:opts` so the three shapes (default / `:opts` / `:options-only`) never collide. Default `false` keeps the byte-identical pre-3.24.0 response for every existing caller (hedge-fund-search-v2, screener, sandbox FundReport.jsx, AI Chat Tool 22). Cutover: FinRadar's filer-proxy switches from `?include_options=true` (60-5K-row) to `?options_only=true` (2-50-row) for the Options-tab fetch — one-line URL edit in `filer-proxy.php`. - 2026-05-12 (v3.23.0): Phase 69 Plan 05 (PR #166) — opt-in per-holding cost-basis analytics on `/api/v1/form-13f/fund/{cik}` + soft-deprecation of `/holdings-analytics`. (1) New `?include_analytics=true` query param on `/fund/{cik}` (default `false`). When `false` (or omitted), response is BYTE-IDENTICAL to v3.22.0 — same SQL filter, same row count, same Redis cache key, same `holdings[]` shape. When `true`, each `holdings[]` row gains 11 cost-basis analytics fields (`avg_buy_price`, `current_price`, `cost_basis`, `market_value`, `unrealized_pnl`, `unrealized_pnl_pct`, `holding_period_days`, `holding_period_quarters`, `first_added`, `last_share_delta`, `last_transaction_quarter`; plus `sector` when /fund/{cik}'s row doesn't already carry one) — same VWAP-of-quarterly-closes methodology as the now-deprecated `/holdings-analytics` route. Cache key composes correctly: the four combinations (no flags / `:opts` / `:analytics` / `:opts:analytics`) cache independently. If `CostBasisService` raises for an exotic CIK, the analytics decoration is skipped with a warning log — the default `/fund/{cik}` response is never broken by an analytics failure. Same opt-in flag pattern as `?include_options=true` (PR #149). (2) `/api/v1/form-13f/fund/{cik}/holdings-analytics` is now SOFT-DEPRECATED — returns HTTP 302 Found with `Location: /api/v1/form-13f/fund/{cik}?include_analytics=true`. The redirect is returned BEFORE any service call so the deprecated route never triggers `CostBasisService` (no DB load on deprecated clients). HTTP clients that follow redirects (axios/fetch/curl -L/PHP/WP defaults) keep working transparently; clients that explicitly disallow redirects must update their URL. The softer 302 (vs the originally-planned 410 Gone) is the deployment-ordering escape valve — UAT auto-deploys on merge and a hard 410 would degrade the live filer-profile page in the window between this PR's UAT-deploy and the downstream WordPress-adapter cut-over. The route is expected to remain a 302 redirect for at least one release after adapter migration; full removal will get its own deprecation notice in this changelog before it ships. NO-replicated-variables policy preserved (D-04): the 11 analytics fields are owned by `/fund/{cik}?include_analytics=true` only — `/fund-performance` and `/daily-returns` (Phase 69 Plan 03, PR #165) remain free of replicated cost-basis state. - 2026-05-12 (v3.22.0): Phase 69 Plan 03 (PR #165) — `/api/v1/form-13f/fund-performance` leaderboard rows drop seven previously-replicated fields: `new_positions`, `closed_positions`, `increased_positions`, `decreased_positions`, `unchanged_positions`, `top_sector`, `sector_concentration_hhi`. Each of these is now owned exclusively by `/api/v1/form-13f/fund/{cik}` (under `analysis.new.total` / `analysis.closed.total` / `analysis.increased.total` / `analysis.decreased.total` / `analysis.unchanged.total` for the position-change counts; `sector_allocation.top_sector` / `sector_allocation.sector_concentration_hhi` for the sector-concentration pair) per the Phase 69 D-04 'each variable owns exactly one endpoint' decision. **BREAKING** for any caller relying on these keys on `/fund-performance` — switch to `/fund/{cik}` for the same data. `top_contributors`, `top_detractors`, `portfolio_return_pct`, `portfolio_value`, `holdings_count`, `price_coverage_pct` are unchanged on `/fund-performance`. Same PR also drops the top-level `summary` block (`total_return_pct`, `sp500_return_pct`, `alpha_pct`) from `/api/v1/form-13f/fund/{cik}/daily-returns` — those scalars duplicate `/fund-performance`'s `return_*y` and `alpha_*y` for the selected period. The `data` block (chart arrays / table rows) on `/daily-returns` is preserved verbatim. Three previously-undocumented fields (`increased_positions`, `decreased_positions`, `unchanged_positions`, `sector_concentration_hhi`) were never in apiDocs.js for `/fund-performance` so their removal is doc-invisible on the leaderboard surface — but they were live in the JSON response and any LLM that introspected the response shape will need to drop them. - 2026-05-10 (v3.21.0): Phase 68 (issue #148, PR #149) — opt-in options on `/api/v1/form-13f/fund/{cik}` + holdings-analytics enrichment. (1) New `?include_options=true` query param on `/fund/{cik}` (default `false`). When false, response is byte-identical to pre-3.21.0 — same SQL filter, same row count, same Redis cache key. When true, response includes options positions (puts/calls) alongside long-equity holdings; rows always carry the new `is_option` boolean (and existing `put_call` field) regardless of mode so clients can filter client-side. The two response shapes cache independently (`:opts` suffix on the cache key when true). All other callers (`POST /fund/bulk`, `/13f` WebSocket builders, hedge-fund-search-v2 inline report, screener, basket aggregator) pass the kwarg as default-False ⇒ zero behavior change. (2) `/api/v1/form-13f/fund/{cik}/holdings-analytics` per-row output gains three additive Phase 68 fields: `sector` (Sharadar GICS-style; resolution priority `cusip_mappings.ticker → all_stocks_list.sector` with `holding_info.sector` fallback excluding the literal 'Equity'), `last_transaction_quarter` (ISO `YYYY-MM-DD` of most-recent quarter where the split-adjusted share count moved — null for static-since-acquisition positions), and `last_share_delta` (signed split-adjusted share delta on that quarter; positive = added, negative = trimmed). Tracked AFTER split adjustment so a 4:1 stock split alone does NOT register as a transaction. Powers the 'Holdings' tab + 'Latest Trades' sort mode + Sector / Industry filter dropdown on `/filer/-/`. Endpoint also newly documented in apiDocs (was missing — pre-Phase-59 long-tail backfill gap closed). No DB migration; no removed fields; no breaking changes. - 2026-05-06 (v3.20.2): Issue #101 — WebSocket auth: `/sec`, `/13f`, `/insider-module` now accept api_key via three methods, first match wins: (1) Socket.IO `auth` payload (`io(url, { auth: { api_key: '...' } })`) — recommended; (2) HTTP header `X-API-Key` or `Authorization: Bearer`; (3) query `?api_key=`. Pre-fix only query params worked, so customers following the docs got `One or more namespaces failed to connect`. **BREAKING:** undocumented `?token=` query param REMOVED — move to one of the three methods above. JWT browser-session auth still works via `auth.token` payload. Token billing (100/day) and concurrent-cap unchanged. - 2026-05-06 (v3.20.1): Issue #100 — `/api/v1/account/balance`, `/api/v1/account/usage`, and `/api/v1/account/transactions` now accept `X-API-Key` (or `Authorization: Bearer `) in addition to the existing session JWT. Closes the commercial-readiness gap where server-side automation scripts could not budget-check before bursty workloads (the docs already advised non-browser callers to use api_key, but these three endpoints were the only customer-facing exemption). Auth method does NOT change billing — the endpoints stay free (`cost: 0`, `@meters_tokens` exempt) regardless of how the caller authenticated. `POST /api/v1/account/billing-portal` remains JWT-only by design (Stripe Customer Portal is a browser-redirect flow with no automation use case). Backed by a new `account_auth_required` decorator in `app/middleware/auth.py` that omits the first-party-origin bypass present in `api_auth_required` — account routes always need a real user identity. - 2026-05-02 (v3.20.0): Phase 59 long-tail content backfill (Wave 3, plans 59-04 through 59-09) — 91 previously-undocumented endpoints across 11 sidebar groups now ship full Polygon-grade content: populated `responseSchema[]` (with `field` / `type` / `nullable` / `description` per row), realistic `sampleResponse` data using marquee public-domain identifiers (AAPL/MSFT/NVDA/GOOGL/TSLA + Berkshire 0001067983 / BlackRock 0001364742 / Vanguard 0000884144 + Tim Cook 0001214156 / Warren Buffett 0000315090), persona-grounded `longDescription` (2-4 paragraphs with inline `/docs/...` cross-links), and 4-entry `related[]` cross-section adjacency arrays. Per-plan endpoint coverage: Plan 59-04 = 15 Insider Trading endpoints (event-sourced ledger + Section-16 P/S transaction codes + cluster-detection tier signals); Plan 59-05 = 15 Account-group endpoints (cents-vs-dollars unit emphasis + Stripe-PaymentIntent client-secret pattern + signed-delta token-ledger semantics); Plan 59-06 = 14 SEC Filings group endpoints (canonical accession-number XXXXXXXXXX-YY-NNNNNN format + form-type catalog cross-linking + non-JSON body endpoint pattern); Plan 59-07 = 13 Form 13F API endpoints (10-char zero-padded CIK convention + ISO YYYY-MM-DD calendar quarter-end + Phase 51 holdings-quality value-normalization + Phase 52 split-adjustment redirect); Plan 59-08 = 15 Company Data endpoints (XBRL units-and-concept-explicit pattern + Yahoo-vs-XBRL ±2% normalization-difference disclaimer + non-calendar fiscal-year-end aware behavior); Plan 59-09 = 19 long-tail endpoints across CUSIP API (8 — Phase 11/47 successor-chain semantics + OpenFIGI 24% null-ticker caveat + CUSIP6 NOT stable across reorgs), Ownership & Positions (5 — Phase 47 unified-holder layer + holder_uuid stable cross-source identifier + per-source-type drill-down vs merged-feed shape contracts), Sniper (3 — 3-state lifecycle CREATED/ACTIVE/STOPPED + plan-allocated sniper-slot budget + manual_stop idempotency), Webhooks (1 — DIRECTIONAL FLIP contract for events FinRadar SENDS to customer URL with HMAC-SHA256 X-FinRadar-Signature + 10s/60s/300s exponential-backoff retry policy + WS-stream alternative), Home (1 — `/api/v1/stats` poll-safe platform metrics), and Resources (1 — `/api/v1/status/history` with hourly-vs-daily resolution-clamp guard). After this release: ZERO non-deprecated, non-admin, non-WS endpoints in the documentation surface have missing `responseSchema` (Phase 59 completeness criterion fully met). Combined with Phase 58's top-50 Polygon-grade polish (Plans 58-07 + 58-11), the v5.0 API Documentation Rework milestone now ships full Polygon/sec-api.io/IEX-Cloud-grade documentation across all customer-facing endpoints. No backend behavior changes — documentation surface only. Regenerated `/llms.txt` and `/docs-search-index.json` at build time to expose all 91 newly-populated endpoints to the LLM-fetch consumer + cmdK search palette. - 2026-05-01 (v3.19.0): Phase 58 Plan 11 polish pass — re-authored `utility`, `params[].description`, and `responseSchema[].description` text across all 50 top-50 endpoints to meet Maroun's quality bar (per-Maroun review of Plan 58-07's content output). Each utility is now 2-4 sentences explaining the persona / financial problem / decision the endpoint enables (replacing the previous one-line summaries). Each param description now explains the lever, valid range/format, default behaviour, and edge cases (replacing the previous restated-field-name shorthand). Each responseSchema description now explains semantic meaning, null cases, units, and computation notes (replacing the previous one-line types). Tone matches Polygon.io / sec-api.io / IEX Cloud documentation depth. Endpoint coverage by group: Insiders 7, SEC Filings 8, Institutional Holdings 12, Ownership & Positions 5, Company Data 5, Bulk Data 3, Account 2, Webhooks 3, Reference Data 1, Resources 1, Home 1, Market Status 1, Daily Index 1. No backend behavior changes — documentation surface only. - 2026-05-02 (v3.18.0): Phase 58 Plan 07 — top-50 customer-facing endpoints now ship full Polygon-grade documentation: `responseSchema` (4-19 typed field rows per endpoint with nullable + description), `sampleResponse` (hand-curated representative JSON), `longDescription` (2-4 paragraph prose with EndpointRef links to related endpoints), `related[]` (3-5 cross-section adjacency links), and `since` (introduction-version chip). Selection algorithm: deterministic HYBRID (all tier-10 + tier-25 unconditionally → section-coverage floor → tier-5/1 fillers) with 6 explicit Maroun swaps applied (drop 6 cost-0 placeholder reps, add 6 customer-facing tier-10 insider-module hot paths: /transactions/search, /transactions/top, /clusters, /screen, /recommendations/daily, /query). The remaining ~106 endpoints fall through to the Plan 58-08 fallback callout. No backend behavior changes — documentation surface only. - 2026-05-01 (v3.17.1): Phase 65 PR2 — `/aggregation/by-sector.unique_insiders` semantics fix. The field previously returned `count(distinct(ReportingOwner.name_as_filed))`, which inflated on multi-entity joint filings (e.g. LGN's 9 Blackstone LP/GP/manager entities reporting one $514M trade became `unique_insiders=9` on the Industrials row). Now returns `count(distinct(_canonical_owner_id))` — one canonical owner per economic transaction, matching `/clusters.insider_count` semantics. Field name preserved; AI-chat consumers see corrected numbers without prompt changes. Empirical impact (per Phase 65 audit, last 45d): Industrials sells +222.8%, Technology sells +166.3%, Energy sells +111.6% over-counting eliminated. Other fields on this endpoint (buy_value/sell_value/net_value/transaction_count/filing_count/unique_issuers) were already deduped — only `unique_insiders` was broken. - 2026-05-01 (v3.17.0): Phase 65 PR1 — opt-in `dedup_owners=true` query parameter on the 5 raw insider transaction endpoints (/transactions/latest, /transactions/by-ticker/{ticker}, /transactions/by-insider/{cik}, /transactions/search, /transactions/top). When true, the API collapses joint-filer rows (PE LP+GP+manager, 13D groups, SPAC sponsors) into one canonical row per economic transaction and surfaces a `co_owners` JSON array ([{name, cik, is_officer, is_director, is_ten_percent_owner}, ...]) plus a `co_owner_count` integer. Eliminates the +112-223% aggregate-flow inflation observed across Industrials/Technology/Energy sells over the last 45 days from joint Form 4 filings (e.g. LGN $514M × 9 owners = $4.6B reported; FANG $2.15B × 2 = $4.3B; RDW $231M × 8 = $1.85B). When false (default) or omitted, the response is byte-identical to pre-3.17.0 production — no new keys appear. Dedup partition: (issuer_cik, transaction_date, transaction_code, shares, price_per_share, COALESCE(shares_owned_following, 0), ownership_nature). Deterministic winner: lowest transaction_id. WebSocket payloads are unchanged. No commercial peer (sec-api.io, FMP, Intrinio, EODHD) offers server-side joint-filer dedup — finradar is now a strict superset. - 2026-04-26 (v3.16.0): Token-based metering (Phase 56 — #commercialization): every billed `/v1/*` and `/api/insiders/*` endpoint now carries an explicit `cost` field (1 / 5 / 10 / 25 tokens per call) so customers can see the price of every API call in the docs page AND in the `/llms.txt` LLM-fetched reference. Exempt endpoints (auth, account self-serve, billing, payments, admin, sniper, user, webhooks, health) carry `cost: 0`. WebSocket endpoints follow a flat connection-day model (`cost: 100` tokens per connection-day, reconnects on the same UTC day are free). Free tier = 2,000 tokens/month; Paid tier = 200,000 tokens/month for $29/mo. Refilled on subscription anniversary; no rollover. Insufficient balance returns 402 with `current_balance` + `required_cost` + `next_refill_at` + `upgrade_url`. New customer self-serve endpoints (also exempt): GET /api/v1/account/balance (live balance + plan + days_until_refill), GET /api/v1/account/usage (30-day daily consumption + top-10 endpoints), GET /api/v1/account/transactions (signed-delta ledger), POST /api/v1/account/billing-portal (Stripe Customer Portal redirect URL). Customer dashboard at /account/usage. - 2026-04-19 (v3.14.2): Ticker normalization (Phase 53, #247): All 17 ticker-accepting endpoints now coerce user input to canonical hyphen form (BRK.A, BRK/A, BRKA -> BRK-A). Canonical form matches SEC EDGAR company_tickers.json and the DB-layer norm_ticker_bi trigger invariant. Applies to /tickers/{ticker}/overview, /holders, /fund-trends, /cusip/ticker/{ticker}, /filings, /backfill/ticker, /financials/metrics, /ratios, /snapshot, /changes/{ticker}, /stock-overview, /tickers/logos/domain/{symbol}, /tickers/logos/domains, /insiders/transactions/by-ticker/{ticker}, /insiders/summary/by-ticker/{ticker}, /insiders/filings/by-ticker/{ticker}, /insiders/holdings/by-ticker/{ticker}. Backed by the Phase 49 ticker_norm_aliases table (~101 rows, seeded weekly from cf_ticker_lookup by the new ticker_autocomplete_weekly Airflow DAG). No breaking changes for well-formed inputs. - 2026-04-16 (v3.14.1): Add 4 signal tables to SQL proxy whitelist (#253): all_stocks_signals_monthly, all_etfs_signals_daily, all_etfs_signals_weekly, all_etfs_signals_monthly. Create all_etfs_signals_monthly table in Postgres (matching ETF weekly schema + indexes). Enables migration of all signal pages from DO MySQL to PostgreSQL. - 2026-04-14 (v3.14.0): Document 15 previously undocumented endpoints across 4 sections. Insider Module: GET /recommendations/daily (cluster-based buy/sell signals), GET /stats/global (market-wide insider activity), GET /filings/by-ticker/{ticker} (company filing history), GET /filings/{accession_number} (full filing detail), GET /insider/search (name/CIK search). CUSIP: GET /cik/{cik} (reverse CIK lookup), GET /cusip6/{cusip6} (issuer prefix lookup), GET /search (full-text search), GET /stats (mapping coverage). Ticker Research: GET /autocomplete (full ticker list), GET /logos/domain/{symbol} (company logo), GET /logos/domains (batch logo lookup). Bulk Data: GET /bulk/form-{type}/index.json (monthly file index), GET /bulk/form-{type}/{year}/{filename} (JSONL.gz download). Total endpoint count: 119 → 134. - 2026-04-13 (v3.13.0): Add POST /api/v1/sql-proxy endpoint for WordPress SCC migration (#246). Executes read-only SELECT queries against whitelisted tables (all_stocks_strategies_yf_daily, all_stocks_list, all_stocks_signals_daily, all_stocks_signals_weekly, market_calendar, ai_daily_summaries). Auth via api_key in POST body/header. 30s timeout, 30 req/min rate limit. Extend /financials/stock-overview with 7 new columns: rsi, score, volume, pct_change, pct_from_high, above_200_sma, above_50_sma (#246). - 2026-04-13 (v3.12.0): Materialize enrichment columns (close_price, market_cap, shares_outstanding, sector, industry, is_etf, first_traded, fund_flow_mcap_pct) into sec_13f_summary during reaggregation (#245). Raises /aggregation/by-ticker limit from 500 to 10000. 8k Stocks in <2s vs ~2min before. Add CUSIP to search filter (#244). Fix volume stats to respect security_group filter (#244). Make security_group enrichment failure hard-fail (#244). - 2026-04-12 (v3.11.0): Replace security_type + is_etf/etf filters with unified security_group param on /aggregation/by-ticker (#241). Six groups: Stocks, ETFs, Preferred, Warrants, Debt, Other. Old params still work as deprecated aliases. Coverage restored from 7,968 to ~25,000 CUSIPs by removing equity-only whitelist. - 2026-04-12 (v3.10.0): Add Operations / Airflow admin proxy endpoints: GET /admin/airflow/dags (DAG list + stats), GET /admin/airflow/dags/{id}/runs (run history), POST /admin/airflow/dags/{id}/trigger (manual trigger), GET /admin/airflow/health (health check), PATCH /admin/airflow/dags/{id}/toggle (pause/unpause). All admin-only. Enables pipeline monitoring and control from the FinRadar dashboard. - 2026-04-11 (v3.9.0): 13F API math + consistency hotfix batch (#239). Bug 1: fund_count_prev now uses fair comparison (only counts funds that filed in BOTH quarters) — AAPL Q4 2025 fund_change_pct is now +11% instead of -98%. Bug 2: closed position fund_flow uses cross-fund weighted avg current price instead of up-to-7.5-month-stale prev quarter price. Bug 4: holdings query filters against new cusip_equity_filter lookup table (Sharadar equity whitelist), replacing the unreliable filer-reported security_type='equity' filter — bonds, warrants, convertibles, and preferred stock no longer leak through. Row count for Q4 2025 dropped from 25,060 to 7,968 after filtering. Rename /api/v1/form-13f/market/tickers → /api/v1/form-13f/aggregation/by-ticker (old path kept as deprecated alias). Rename response field float_shares → shares_outstanding (was a misleading proxy, not true float). Remove /api/v1/form-13f/market/summary endpoint (redundant with /aggregation/by-ticker). Remove fund_flow_pct column and response field (never used, misleadingly named — fund_flow_mcap_pct is the real metric). API consistency: sort/order params renamed to sort_by/sort_order with backward-compat aliases, etf param renamed to is_etf (boolean) with backward-compat alias. - 2026-04-09 (v3.8.0): Add GET /api/v1/financials/stock-overview endpoint (#236): market data snapshot from all_stocks_strategies_yf_daily table (beta, close, sector, industry, market cap, enterprise value, float, short data). Unblocks DCF and Financials module migration from DO MySQL. Add nginx rate limiting for /api/v1/financials/ endpoints (#237): 30 req/min per IP with burst of 10, returns 429 when exceeded. - 2026-04-07 (v3.7.3): Add missing filters to activists and threshold-crossings endpoints (#235): Activists now supports issuerTicker, filerName, and minOwnership filters. Threshold-crossings now supports issuerTicker, threshold (5/10/15/20), direction (up/down), and dateTo filters. Also adds crossed_15_percent to default threshold detection. - 2026-04-04 (v3.7.2): XBRL Round 4 DO parity (#228): TTM sum now uses LAG(1/2/3) with ORDER BY period_end (crosses fiscal year boundaries like DO), TTM latest uses ROW_NUMBER with period_end DESC tiebreaker, averaged ratios (ROE/ROA/turnover) use deterministic fiscal_period ordering in LAG window, standardization removes filed_date tiebreaker to match DO exactly. - 2026-04-04 (v3.7.1): Add automatic re-aggregation of latest quarter's sec_13f_summary every 6 hours via Celery beat (#229). Keeps /market/summary and /market/tickers fresh during filing season as new 13F filings arrive, without manual CLI runs. Also re-aggregates sector data. - 2026-04-04 (v3.7.0): Add GET /api/v1/form-13f/market/tickers endpoint for grouped hedge fund analysis (#214). Returns all tickers for a given quarter with 31 sortable columns, 11 range filters (min/max), sector/industry/ETF category filters, and text search. Enriches 13F summary data with close price, market cap (close x shares_outstanding from XBRL), float (shares_outstanding proxy), first traded date, and ETF flag from Sharadar. Includes filter_ranges for frontend slider initialization, available_quarters for dropdown, and quarter_metadata for incomplete/complete distinction. Replaces DO backend for Grouped Hedge Funds page. Also adds ?quarter= param to GET /market/summary so incomplete filing-season quarters can be requested (#229). - 2026-04-03 (v3.6.1): XBRL Round 3 parity fixes (#228): Fix cashflow Q2/Q3 duration filter — was accepting discrete (70-200/70-290 days) instead of cumulative-only (170-200/260-290 days), causing 169+ companies to have wrong quarterly cashflow (e.g. MSFT Q2 OCF was -$11.9B, now correct +$22.3B). Ticker resolution now goes through cf_ticker_lookup so alternate tickers like GOOGL resolve correctly. Year filter matches DO behavior (relative to current year). LAG window determinism fix for averaged ratios (ROE/ROA/turnover). - 2026-04-02 (v3.6.0): XBRL critical data fixes (#228): Fix Q3 cashflow discretization (was off by +Q1 for every company), add period duration filter (root cause of AAPL mismatch), Q4 quarter guard (prevents FY duplication), TTM minimum 2-quarter count, turnover ratio positive denominator filter. API: /ratios accepts period=TTM, /snapshot accepts period=Q for latest quarter, tiered cash_runway_status (healthy/moderate/caution/critical), oldest-first sort order. Deterministic primary ticker scoring via _score_ticker(). 7 new performance indexes. concept_used audit trail for all calculated rows. - 2026-04-01 (v3.5.0): XBRL DO-to-PG parity fixes (#224/#228): Add frame filter to prevent cross-year fact pollution, unit filter matching DO regex patterns, remove form_type restriction (recover 127K+ rows). Fix ROE/ROA/turnover to use averaged denominators, ROIC to match DO formula (0.75 fixed tax, subtract cash), cash runway restricted to TTM. New endpoints: GET /api/v1/financials/screen (stock screener with 20+ ratio filters), GET /api/v1/financials/changes/{ticker} (per-ticker change details), GET /api/v1/financials/status (pipeline health). Add cf_ticker_lookup table for alternate ticker resolution. - 2026-03-31 (v3.4.0): Add Company Financials (XBRL) section: GET /api/v1/financials/metrics for standardized financial data (36 metrics), GET /api/v1/financials/ratios for 29 derived financial ratios with category filtering, GET /api/v1/financials/snapshot for TTM summary, GET /api/v1/financials/tickers for search, GET /api/v1/financials/changes for delta tracking. Data sourced from SEC EDGAR companyfacts bulk download (~7,000+ companies), updated daily. - 2026-03-28 (v3.3.3): Add offset pagination to GET /tickers/{ticker}/holders (#216). New offset query param (default 0) and total_count/offset/limit in response. Enables paging through all holders including Closed positions that were previously cut off by the 500 limit. - 2026-03-27 (v3.3.2): Add position change deltas to GET /ownership/beneficial-ownership/filings (#213): priorPercent, priorShares, percentChange, sharesChange, sharesChangePct, daysSincePriorFiling. Auto-links amendments after parse. Backfill Pass 3 covers all unlinked filings with priors (Fintel parity). - 2026-03-27 (v3.3.1): Add 20-F form type support to GET /scrapping/extractor (#212). New form_type query parameter (default '10-K'). 14 item patterns for 20-F foreign private issuer filings (Items 1-12, 19). Enables extraction of 'Information on the Company' (Item 4), 'Operating and Financial Review' (Item 5), etc. for tickers like BABA, TSM, NVO, BTDR. - 2026-03-26 (v3.3.0): Rewrite /tickers/{ticker}/holders and /tickers/{ticker}/fund-trends to query ALL 9,600+ 13F filers via form_13f_holdings (previously limited to 668 tracked funds). CUSIP successor chain expansion and amendment deduplication (latest filing per CIK+period). CIK normalization handles zero-padding inconsistency. Split-adjusted share counts preserved. Response shape unchanged — fully backwards compatible. - 2026-03-26 (v3.2.0): 13D/13G sec-api.io full parity (#210): Fix 43K filings with corrupt issuer_cik='None' string (B1-B5). Add QoQ change fields to POST /form-13f/holdings via includeChanges param — returns changeShares, changePercent, status (New/Increased/Decreased/Unchanged/Closed), closedPositions array, previousPeriod (#209). BO API model changes: cusip as array, sourceOfFunds/typeOfReportingPerson as arrays, new fields nameOfIssuer, titleOfSecurities, eventDate, secFilingUrl, entities (SGML index metadata), groupMembers, entityType (entity/individual detection), per-filer footnotes, amountOfFunds, indirectOwnershipExplanation, sharesAcquirable60Days, legalProceedingsDisclosureRequired, amountExcludesCertainShares. Parser v11 with aggregateAmountExcludesCertainSharesFlag extraction. - 2026-03-15 (v3.1.1): Fix insider cluster insider_count inflation: clusters now count unique transactions instead of distinct owner entities, preventing fund vehicles of the same beneficial owner from being counted as separate insiders. Affects /insider-module/api/insiders/clusters and daily recommendations. - 2026-03-15 (v3.1.0): Add unified position tracking: event-sourced ledger with assertion-based position calculation, holder-issuer position history, issuer holder list, timeline, and cross-reference endpoints. Historical 13D/13G backfill pipeline via EDGAR EFTS. Parser v3 with 96% ticker resolution, fixed CIK disambiguation, and 80%+ ownership data extraction. Schema aligned with full Ralph spec (~70 new columns). - 2026-03-15 (v3.0.0): Major 13D/13G beneficial ownership overhaul: Normalized multi-table schema (bo_filings, bo_filers, bo_positions, bo_purpose, bo_amendments, bo_groups, bo_exhibits). New GET /api/v1/ownership/* endpoints for filings, issuer summary, filer portfolio, activist tracker with purpose classification, conversions, groups, unified holders, filing history, ownership concentration, and threshold crossings. Rule-based NLP purpose classification with 13 categories and intent flags. Unified holder identity linking across Form 4, 13F, and 13D/G via CIK and name matching. Legacy POST /api/v1/form-13d endpoint preserved for backward compatibility. - 2026-03-14 (v2.2.0): Add Form 13D/13G API: POST /api/v1/form-13d for querying parsed Schedule 13D/13G beneficial ownership filings. Supports filtering by ticker, issuer/filer CIK, filer name, ownership percentage, form type, and date range. Includes structured ownership data (percent of class, shares owned, voting power, purpose). Auto-triggers parsing for new 13D/13G filings from RSS feed. Admin backfill endpoints at /api/v1/admin/backfill/13dg/*. - 2026-03-14 (v2.1.0): Add public System Status page and API: GET /api/v1/status (current health of all services with uptime %), GET /api/v1/status/history (time-bucketed uptime history for charts). Watchdog now persists health checks to DB. Add public Changelog page at /dev/changelog. - 2026-03-11 (v2.0.0): v3.0 13F Analytics documentation complete. Fixed market/summary endpoint: added ?detail=basic|full query parameter and sampleResponse (gap from Phase 32). Added sampleResponse to by-sector endpoint (was missing despite having params). Audited all v3.0 endpoint docs (fund-performance, market-rotation, cross-filing-signals, filing-progress) -- all verified complete. Version bump to 2.0.0 marks v3.0 milestone completion. - 2026-03-11 (v1.9.0): Added /filing-progress endpoint for quarter completeness tracking with recurring-filer baseline, deadline countdown, and AUM. Added quarter_metadata to market/summary, by-sector, fund-performance, market-rotation, and cross-filing-signals responses indicating whether the quarter's data is complete or still accumulating. - 2026-03-11 (v1.8.0): Add market rotation signals (GET /api/v1/form-13f/market-rotation) for value/growth and cyclical/defensive sector rotation detection with QoQ comparison. Add cross-filing convergence signals (GET /api/v1/form-13f/cross-filing-signals) for smart money convergence detection where both insiders and institutions are buying, with investment attractiveness scoring. - 2026-03-11 (v1.7.0): Add GET /api/v1/form-13f/fund-performance endpoint for VWAP-based quarterly fund performance ranking with leaderboard mode, single-fund detail mode, position-level return attribution, eligibility filtering, and sector concentration metrics. - 2026-03-11 (v1.6.0): Add GET /api/v1/form-13f/aggregation/by-sector endpoint for sector/industry-level institutional fund flow analytics with momentum labels, flow significance metrics, top movers drill-down, and QoQ comparison support. - 2026-03-08 (v1.5.0): v2.3 Sector & Industry Enrichment: Add sector and industry response fields to all insider transaction, holdings, cluster, and filing endpoints (Sharadar classification). Add sector and industry query parameters to 6 endpoints (transactions/latest, transactions/search, transactions/top, screen, clusters, filings/latest). New GET /aggregation/by-sector endpoint for sector/industry-level insider activity metrics with full filter support. - 2026-03-05 (v1.4.0): Add total_beneficial_ownership field across all insider transaction, holdings, and profile endpoints. Aggregates shares_owned_following across all ownership vehicles (direct + indirect trusts/entities) per insider per company. Add 8 new insider endpoint docs: by-ticker, by-insider, search, query POST, insider detail, holdings by-ticker/by-insider/snapshot. Insider-centric endpoints (by-insider, insider detail) include ownership_summary with per-company vehicle breakdown. Fix holdings GROUP BY bug that silently dropped indirect holdings. - 2026-02-23 (v1.3.3): Add Ticker Research section: GET /api/v1/tickers/{ticker}/overview (company financials), /holders (institutional holders with QoQ change), /fund-trends (historical quarterly fund ownership). - 2026-02-23 (v1.3.2): Fix GET /api/v1/sec/filings sort order: default is now filed_date:desc (newest first). Add sort parameter supporting filed_date and updated_at fields with asc/desc direction. limit=1 now correctly returns the most recent filing. - 2026-02-23 (v1.3.1): Add description field to GET /api/v1/sec/forms endpoint. Covers all 352 root SEC form types with human-readable descriptions. Amendment variants (/A) auto-append '(Amendment)'. Also documents q and limit query params. - 2026-02-21 (v1.3.0): Add Market Status section: GET /api/market/status for real-time NYSE session state with countdown, holiday awareness, and next-event info. Public endpoint (no auth required). - 2026-02-21 (v1.2.1): Add GET /billing/history and GET /billing/invoices endpoints for wallet transaction history and Stripe invoice retrieval. - 2026-02-21 (v1.2.0): Add Billing & Subscription section: GET /billing/subscription, GET /plans/, POST /billing/quota, POST /payment/create-checkout-session. Stripe subscription webhooks for plan lifecycle management. - 2026-02-18 (v1.1.0): Add 13F calendar timeline fields to /filing-stats: previousDeadline, lastCompleteQuarterEnd, beforeLastCompleteQuarterEnd, nextReportingPeriodStart, nextDeadline, daysUntilNextDeadline. Default period now shows last completed quarter when between filing seasons. - 2026-02-18 (v1.0.1): Fix duplicate User Data section - 2026-02-18 (v1.0.0): Initial LLM docs release with all endpoints ## Authentication All endpoints require an API key. Pass it via query parameter `?apiKey=YOUR_KEY` or header `X-API-Key: YOUR_KEY`. WebSocket endpoints accept the key in the `token` auth payload or query parameter. --- ## Token Pricing Every billed endpoint costs 1, 5, 10, or 25 tokens per call. Free tier ships with 2,000 tokens/month. Paid tier ships with 200,000 tokens/month for $29/mo USD. Tokens refill on your subscription anniversary; balances do NOT roll over. Insufficient balance returns HTTP 402 with `current_balance`, `required_cost`, `next_refill_at`, `plan`, and `upgrade_url` so your client can prompt the user before the next call. Check your live balance at GET /api/v1/account/balance and manage your subscription via POST /api/v1/account/billing-portal (returns a Stripe Customer Portal redirect URL). WebSocket connections follow a flat connection-day model (100 tokens per UTC day connected, regardless of message volume). Exempt endpoints (auth, account self-serve, billing, payments, admin, sniper, user, webhooks, health) carry `cost: 0` and never debit the ledger. ### GET /api/v1/account/balance Live token balance, plan, and days until next refill. EXEMPT — does not consume tokens. Accepts session JWT (browser dashboard) OR `X-API-Key` (server-side automation pre-flighting bursty workloads). **Token cost:** 0 (EXEMPT — auth / billing / account / admin / health) **Response fields:** - `current_balance` (integer): Tokens left in the current billing cycle. Always non-negative — when a billed call would push this below zero, the call returns 402 instead of debiting and the balance stays at the pre-call value. Refilled to `monthly_quota` on `billing_cycle_end`; balances do NOT roll over. - `plan` (string): Plan tier — `free` (2,000 tokens/month, no credit card required) or `paid` ($29/mo, 200,000 tokens/month). Use to gate feature visibility client-side (e.g. show 'Upgrade for full Try-It' button on free plans only). - `plan_display_name` (string): Human-readable plan name for UI display (e.g. `Free`, `Paid`). Casing follows brand guidelines — use this verbatim in dashboards rather than capitalizing the `plan` field client-side. - `monthly_quota` (integer): Total tokens granted on each billing-cycle refill. `2000` for free; `200000` for paid. Display alongside `current_balance` for the customer-facing 'X / Y tokens used' progress bar. - `billing_cycle_end` (string): ISO-8601 UTC timestamp of the next refill (subscription anniversary). At this exact moment the account's `current_balance` is reset to `monthly_quota` regardless of prior balance — there is NO rollover of unused tokens. For paid customers, also coincides with Stripe billing event. - `days_until_refill` (integer): Whole days remaining until `billing_cycle_end` (rounded down). Useful for dashboard countdown chips ('29 days until refill'). When 0, the refill is happening within the next 24 hours. - `suspended` (boolean): True when `current_balance == 0` AND the account has consumed all monthly tokens — subsequent billed calls return HTTP 402 with `current_balance`, `required_cost`, `next_refill_at`, `plan`, and `upgrade_url`. False otherwise (still has balance OR has been topped up via admin grant). Use to render 'Account suspended — upgrade now' banners. - `as_of` (string): ISO-8601 UTC timestamp the balance snapshot was generated server-side. NOT cached — always reflects the live balance at request time, since stale balance display in the dashboard would be confusing during active token consumption. **Since:** v3.16.0 **Utility:** Live customer-facing balance probe — the canonical 'how many tokens do I have left?' call. EXEMPT (`cost: 0`) so balance polling never debits the ledger; safe to call as aggressively as 1 Hz. Returns the current balance, plan tier (free / paid), monthly quota, days until next refill, and a `suspended` boolean that flips to `true` when balance hits zero (subsequent billed calls then return HTTP 402). Customer dashboards typically poll every 30 seconds for near-real-time display; server-side automation scripts should poll before bursty workloads to pre-flight the budget and avoid mid-batch 402s. For per-endpoint spend breakdowns use `/api/v1/account/usage`; for the full debit/credit ledger use `/api/v1/account/transactions`. **Use case:** Customer dashboard polls this every 30 seconds for near-real-time balance display. Server-side scripts can also poll before bursty workloads to avoid 402s. **Sample response:** ```json { "current_balance": 198432, "plan": "paid", "plan_display_name": "Paid", "monthly_quota": 200000, "billing_cycle_end": "2026-05-26T08:39:00.000Z", "days_until_refill": 29, "suspended": false, "as_of": "2026-04-26T10:14:45.000Z" } ``` ### GET /api/v1/account/usage Last-N-days daily token consumption + top-10 endpoints by spend. EXEMPT. Same auth model as `/balance` — accepts session JWT or `X-API-Key`. **Token cost:** 0 (EXEMPT — auth / billing / account / admin / health) **Response fields:** - `window_days` (integer): Echo of the requested `days` query parameter, AFTER clamping to [1, 90]. Use to confirm the server applied the expected window (e.g. caller passed `days=180` but server clamped to 90 — `window_days=90` confirms the truncation). - `daily` (array): Per-day spend buckets, sorted by `day DESC` (most recent day first). Empty array on accounts with zero token activity in the window (free-tier accounts that signed up but never called a billed endpoint). Days with zero activity are ABSENT from the array — densify client-side if rendering a continuous bar chart. - `daily[].day` (string): ISO-8601 date (YYYY-MM-DD) at UTC day boundary. Bucketing uses `date_trunc('day', created_at)` — a debit at 23:59:00 UTC and a debit at 00:01:00 UTC the next morning land in DIFFERENT buckets. Display in user's local timezone if the dashboard expects local-day semantics. - `daily[].tokens_consumed` (integer): Total tokens debited on this day (positive integer; the underlying signed delta is negative on debits, but this field flips the sign for human-readable display). Always non-negative — refunds and grants do NOT subtract from this counter (they show up in `/api/v1/account/transactions` instead). For the live signed-delta ledger use `/api/v1/account/transactions`. - `daily[].calls` (integer): Number of debit transactions (= billable API calls) recorded on this day. Use to compute average cost-per-call (`tokens_consumed / calls`) for a 'most expensive day' diagnostic. Calls with `cost: 0` (auth, account, billing, etc.) are EXEMPT and never appear here. - `top_endpoints` (array): Top-10 endpoints by token spend over the window, sorted by `tokens DESC`. Empty array on accounts with zero billed activity. The leaderboard is computed live (no cache) so newly added endpoints surface within seconds of their first paid call. - `top_endpoints[].endpoint` (string): Endpoint identifier in `METHOD /path` format (e.g. `GET /api/v1/sec/filings`, `POST /api/v1/form-13f/holdings`). Path-parameter values are normalized to placeholders so all calls to `/transactions/by-ticker/AAPL` and `/transactions/by-ticker/NVDA` aggregate into a single `/transactions/by-ticker/{ticker}` row. - `top_endpoints[].tokens` (integer): Total tokens spent on this endpoint over the window (sign-flipped from the underlying signed delta — always non-negative). Use to drive the 'where did my tokens go?' donut chart on the dashboard. - `top_endpoints[].calls` (integer): Number of times this endpoint was called over the window. Combined with `tokens` reveals the cost tier (`tokens / calls` ∈ {1, 5, 10, 25} for the four canonical tiers; WS connection-day rows show `tokens / calls = 100`). - `as_of` (string): ISO-8601 UTC timestamp the usage snapshot was generated server-side. NOT cached — always reflects the live `token_transactions` state at request time. **Since:** v3.16.0 **Utility:** Per-day usage histogram + top-10 endpoints by spend — drives the customer dashboard usage chart and 'where my tokens went' breakdown. EXEMPT (`cost: 0`) so dashboards can poll without quota concerns. Direct query against `token_transactions` (no rollup table in v1) — ~50ms p99 at 10K txns/day per the locked-in CONTEXT.md performance budget. The `daily[]` array is sorted by `day DESC` (most recent day first) and back-fills with `tokens_consumed: 0` only for days that had at least one transaction; gap-days are absent from the array (frontend should densify if a continuous bar chart is desired). The `top_endpoints[]` array is sorted by `tokens DESC` (highest spender first) and is capped at 10 entries server-side. **Use case:** Customer dashboard renders a 30-day bar chart of daily spend + a top-10 leaderboard of which endpoints consumed the most tokens. Use to drive the 'usage trend' chip ('You burned 45,800 tokens this week, +12% vs last week'). **Parameters:** - `days` (query, optional, default: 30): Window size in days (1-90; clamped at the bounds; non-numeric falls back to 30). 30 is the canonical dashboard default; 7 for compact widgets; 90 for quarterly review. **Sample response:** ```json { "window_days": 30, "daily": [ { "day": "2026-05-02", "tokens_consumed": 1250, "calls": 248 }, { "day": "2026-05-01", "tokens_consumed": 980, "calls": 196 }, { "day": "2026-04-30", "tokens_consumed": 1420, "calls": 284 } ], "top_endpoints": [ { "endpoint": "GET /api/v1/sec/filings", "tokens": 8400, "calls": 1680 }, { "endpoint": "GET /api/insiders/transactions/latest", "tokens": 6200, "calls": 1240 }, { "endpoint": "POST /api/v1/form-13f/holdings", "tokens": 4500, "calls": 450 } ], "as_of": "2026-05-02T15:51:00.000Z" } ``` ### GET /api/v1/account/transactions Signed-delta ledger of recent token transactions (debits, refunds, refills, grants, manual adjustments). EXEMPT. Same auth model as `/balance` — accepts session JWT or `X-API-Key`. `delta < 0` is a debit, `delta > 0` is a credit. NOT to be confused with `/api/v1/billing/history` (Stripe charges + admin grants — different table, different sign convention). **Token cost:** 0 (EXEMPT — auth / billing / account / admin / health) **Response fields:** - `transactions` (array): Array of signed-delta transaction rows, sorted by `created_at DESC` (most recent first). Empty array on freshly-registered accounts that have neither been granted free-tier tokens (race window before `ensure_ledger_row` fires) nor called any billed endpoint. Cap of 200 rows enforced server-side. - `transactions[].created_at` (string): ISO-8601 UTC timestamp the transaction was recorded. For debits this is the API call's request-completion time (within milliseconds of the response being sent); for `subscription_refill` events this is the Stripe webhook arrival time. - `transactions[].delta` (integer): Signed token movement — NEGATIVE for debits (e.g. `-5` for a 5-token API call), POSITIVE for credits (`+200000` for a paid-plan refill, `+2000` for a free-tier signup grant, `+10` for an admin manual adjustment). Zero for status-only events (e.g. `subscription_cancelled`, `payment_failed`) where token balance does not move but the event must be recorded for audit. Sum across all rows in the current cycle = `current_balance - monthly_quota`. - `transactions[].reason` (string): Transaction kind — one of `debit` (billed API call), `refund` (per-call refund, e.g. handler error), `subscription_refill` (monthly Stripe charge → token grant), `signup_grant` (free-tier 2,000-token initial grant), `manual_adjustment` (admin-applied credit/debit), `subscription_cancelled` / `payment_failed` (status-only, `delta=0`). Use to dispatch UI badges and audit-trail filters. - `transactions[].endpoint` (string (nullable)): Endpoint identifier in `METHOD /path` format for `debit` and `refund` rows (e.g. `GET /api/v1/sec/filings`). Null for non-call events (`subscription_refill`, `signup_grant`, `manual_adjustment`, status-only events) — these are account-level movements unattached to a specific endpoint. - `transactions[].metadata` (object (nullable)): JSONB blob with event-specific context. For `debit`/`refund`: `{request_id, cost_tier}` (correlate with server logs via `request_id`). For `subscription_refill`: `{stripe_invoice_id, plan}`. For `manual_adjustment`: `{admin_user_id, reason}`. For status-only events: `{stripe_event_id, error_code}`. Null when the row was inserted by an early-Phase-56 path that did not yet write metadata. - `transactions[].response_status` (integer (nullable)): HTTP status code the handler returned for this request. Populated for `debit` rows from 2026-05-18 onward (issue #260). Null on `refund` / `subscription_refill` / `signup_grant` / `manual_adjustment` rows — those are bookkeeping movements, not responses to a specific API call. Null on `debit` rows that pre-date the migration (the column was just added; back-population was not attempted). After the 2026-05-18 batch-1 #254 change, any `debit` row with `response_status >= 400` is always paired with an immediate `refund` row (net-zero on the ledger), so this field is mainly an audit trail rather than a billing-correctness signal. - `count` (integer): Number of transaction rows in the response (matches `transactions.length`). Bounded by the `limit` query parameter; smaller when the account has fewer transactions than requested. - `as_of` (string): ISO-8601 UTC timestamp the snapshot was generated server-side. NOT cached — always reflects live state. **Since:** v3.16.0 **Utility:** Per-call signed-delta audit trail — every token movement on the account, ordered by `created_at DESC` (most recent first). EXEMPT (`cost: 0`) so support workflows can fetch full history without debiting. The canonical answer to 'where did my tokens go?' — each row carries the endpoint that consumed (or refunded) the tokens plus a JSONB `metadata` blob with `request_id` for cross-correlation against server logs. Different from `/api/v1/billing/history` in two ways: this endpoint is a per-call ledger (every API call shows up), the other is a per-billing-event ledger (only Stripe charges + admin grants); this endpoint uses signed deltas (negative for debits), the other uses unsigned amounts in cents with a separate `transaction_type` enum. **Use case:** Customer dashboard 'Recent transactions' card; support troubleshooting ('where did my tokens go on April 26?'); automated reconciliation scripts that need to verify a specific request_id was billed correctly. **Parameters:** - `limit` (query, optional, default: 50): Number of recent transactions to return (1-200; clamped at the bounds; non-numeric falls back to 50). 50 is the canonical dashboard default; 200 for export workflows; 20 for compact widgets. **Sample response:** ```json { "transactions": [ { "created_at": "2026-05-02T15:50:12.000Z", "delta": -5, "reason": "debit", "endpoint": "GET /api/v1/sec/filings", "metadata": { "request_id": "req_3OqK2jK9L8pQ4xZ1", "cost_tier": 5 }, "response_status": 200 }, { "created_at": "2026-05-02T15:48:01.000Z", "delta": -1, "reason": "debit", "endpoint": "GET /api/v1/stats", "metadata": { "request_id": "req_3OqK2jK9L8pQ4xZ0", "cost_tier": 1 }, "response_status": 200 }, { "created_at": "2026-04-26T08:39:00.000Z", "delta": 200000, "reason": "subscription_refill", "endpoint": null, "metadata": { "stripe_invoice_id": "in_1Pz...", "plan": "paid" }, "response_status": null } ], "count": 3, "as_of": "2026-05-02T15:51:00.000Z" } ``` ### POST /api/v1/account/billing-portal Returns a Stripe Customer Portal session URL. The frontend MUST redirect the user to this URL via `window.location.href = response.url` (Stripe disallows iframe embedding via X-Frame-Options: DENY). Free users without a Stripe customer get a 400 with `error: no_stripe_customer`. **Token cost:** 0 (EXEMPT — auth / billing / account / admin / health) **Response fields:** - `url` (string): Stripe Customer Portal session URL. Short-lived (~1 hour TTL). REDIRECT the browser to this URL via `window.location.href = response.url` — DO NOT iframe-embed (Stripe sets `X-Frame-Options: DENY` on the portal response). After the user finishes (or closes the portal), Stripe redirects back to `${FRONTEND_URL}/account` per the server-configured `return_url`. **Since:** v3.16.0 **Utility:** Single endpoint for self-serve subscription management — update card, view invoices, cancel subscription, change billing email. EXEMPT (`cost: 0`). Returns a short-lived Stripe-hosted Customer Portal session URL. The frontend MUST redirect (NOT iframe-embed) — Stripe disallows embedding via `X-Frame-Options: DENY` on the portal HTML response. Sessions expire ~1 hour after creation; if the user idle-bounces back, generate a new one. Customers without a Stripe customer record (free-tier users who never upgraded) get a 400 with `error: no_stripe_customer` — gate the 'Manage Billing' button by `plan === 'paid'` to avoid the dead-click. **Use case:** Customer clicks 'Manage Billing' on the dashboard → frontend POSTs here → redirects to Stripe-hosted portal → returns to the app on close (Stripe's `return_url` brings them back to `/account`). **Sample response:** ```json { "url": "https://billing.stripe.com/p/session/test_YWNjdF8xUXBlV0VLRzQzS3JCbnJ1eA0HDJI" } ``` --- ## Authentication Manage user sessions and credentials. ### POST /api/v1/auth/register Register a new user account. EXEMPT (`cost: 0`). INVITE-ONLY during the soft-trial — body must include a valid `invite_token` from POST /api/v1/auth/redeem-invite. **Token cost:** 0 (EXEMPT — auth / billing / account / admin / health) **Response fields:** - `message` (string): Top-level result message. `Register user successful` on success (200). On error: `Email already exists` (400), `Invalid or expired invite token` (400), `Invite code is used and cannot be used` (410), or the underlying exception message (400). UI should toast on success and gate-form on error. - `linkMsg` (string): Email-flow status message. Always `Please verify the email, I have send verification link.` on a clean 200. If verification email send failed (SMTP error), the endpoint returns 500 instead with a different message and the user is created but cannot log in until they trigger /auth/resend-verification. **Since:** v1.0.0 **Utility:** Create a new user account during the invite-only soft-trial. EXEMPT (`cost: 0`) — auth endpoints never debit tokens. Phase 64 enforces invite-token gating: the body MUST include a short-lived `invite_token` JWT issued by `/api/v1/auth/redeem-invite`; the server re-validates the token against the `invite_codes` table on every register call (never trusts the JWT alone). On success the user is created with the FREE tier (2,000 tokens/month), an API key is generated, a verification email is sent, AND a welcome email with the API key + `curl` example is sent (separate try/except so SMTP failure does NOT 500 the response). The user CANNOT log in until they click the verification link in the email — `/auth/login` returns 400 if `verify_email=false`. **Use case:** Frontend signup form: user enters email + password + redeemed invite code → POST here → backend creates the account, sends verification email, returns success message. User clicks verification link → email is verified → user can log in. Server-side onboarding scripts can also call this with a programmatically-redeemed invite token (see /auth/redeem-invite). **Parameters:** - `email` (body, required): User email address. Lowercased server-side. Must be unique across the `users` table — re-registration with an existing email returns 400 with `Email already exists`. Used as the canonical login identifier and the destination for the verification email. - `password` (body, required): User password — minimum 8 characters (validated by `@validate_register`). Hashed via bcrypt server-side; never stored in plaintext, never returned in any response. Use a strong password manager-generated value in production; the example shown above is a redaction placeholder. - `invite_token` (body, required): Short-lived JWT from POST /api/v1/auth/redeem-invite. Encodes the underlying `invite_codes.code` value; the server re-validates against the `invite_codes` table to confirm the code is still REDEEMABLE (not used, not expired, not revoked) before creating the account. Required during the Phase 64 invite-only soft-trial. **Sample response:** ```json { "message": "Register user successful", "linkMsg": "Please verify the email, I have send verification link." } ``` ### POST /api/v1/auth/login Authenticate with email + password and receive a JWT access token. EXEMPT (`cost: 0`). Bad credentials return 401 with a single generic message (anti-enumeration). Per-account brute-force lockout: 5 / 10 / 20 consecutive failures trigger a 1-min / 5-min / 1-hour lockout respectively (HTTP 429 with `Retry-After` header). **Token cost:** 0 (EXEMPT — auth / billing / account / admin / health) **Response fields:** - `message` (string): Top-level result. `User login successfully.` on 200. On error: `Invalid email or password` (401, identical for unknown email AND wrong password — anti-enumeration); `Please verify the email, I have send verification link.` (400, when email is unverified — server side-effect: re-issues a verification email if the previous link expired); `Too many failed login attempts. Try again later.` (429, after 5/10/20 consecutive wrong-password attempts — response also carries a `Retry-After` header in seconds). - `user` (object): Full user profile (same shape as GET /api/v1/user/ returns). Includes the user's `api_key` — surface in the dashboard 'API Keys' page so they can copy it for server-to-server calls. Includes embedded `Userplan` (subscription state) and `credit_balance` (Stripe-paid top-up balance, in USD). - `user.uuid` (string): Stable user identifier (UUIDv4 string). Used as the JWT `sub` claim and as the foreign-key target in every per-user table (`user_token_ledger.user_id`, `token_transactions.user_id`, etc.). Treat as the canonical user PK for client-side state. - `user.api_key` (string): API key for server-to-server calls (32-char concatenation of two UUIDs with hyphens removed). Pass as `X-API-Key: ` to billed endpoints. Distinct from the JWT `token` field — the API key has NO expiry and NO device limit, but DOES debit tokens; the JWT expires (default 1h), is bound to a session_id, but does NOT debit tokens (session cookies are exempt). - `user.verify_email` (boolean): True if the user has clicked the verification email link. Always true in a successful login response (login is gated by this flag — unverified users get a 400 instead of a 200). Use to dispatch onboarding-state UI client-side. - `user.credit_balance` (number): Stripe-paid top-up balance in USD (NOT in cents — server returns `float(credit_balance)`). Used by [POST /api/v1/billing/quota](/docs/account/billing-and-subscription/post-billing-quota) for buying additional API quota at 100 requests / $1. Distinct from `current_balance` in [GET /api/v1/account/balance](/docs/account/token-pricing/get-account-balance) — that's the token ledger; this is the dollars wallet. - `user.Userplan` (object (nullable)): Embedded subscription state (relationship-included via `to_dict(include_relations=True)`). Null for accounts where the relationship row is missing (legacy edge case). Contains `total_limit_api`, `reach_limit_api`, `plan` (`free`/`weekly`/`monthly`/`pro`/`yearly`), `status`, `current_period_end`. - `token` (string): JWT access token issued by `flask_jwt_extended.create_access_token`. Pass as `Authorization: Bearer ` on protected endpoints. Embeds the user's `uuid` as `sub` and a `session_id` claim used for device-limit eviction. Default expiry is the Flask app's `JWT_ACCESS_TOKEN_EXPIRES` (typically 1 hour). Re-issue by re-calling /auth/login (no separate refresh-token endpoint in v1). **Since:** v1.0.0 **Utility:** Exchange email + password for a JWT access token (`flask_jwt_extended`-issued). EXEMPT (`cost: 0`). The returned token is a Bearer JWT — pass as `Authorization: Bearer ` to protected dashboard endpoints (NOT to API-key endpoints; those use the `X-API-Key` header from the user's `api_key` field). Each successful login appends a session_id to the user's `user_login_device` array and is bounded by `device_limit` (default 2 — third login evicts the oldest session). Email must be verified (`verify_email=true`) before login succeeds; otherwise 400 with a re-send-verification message. The user object returned in the body includes the API key — surface this in the dashboard 'API Keys' page so the user can copy it for server-to-server calls. **Lockout (issue #272):** five consecutive wrong-password attempts triggers a 60s lockout; tenth a 5-min lockout; twentieth a 1-hour lockout. During lockout every login returns HTTP 429 with a `Retry-After` header — even calls supplying the correct password (intentional, so an attacker can't fish for whether they've guessed right). A successful login resets the counters. **Use case:** Frontend login form posts here → receives JWT + user object → stores JWT in cookie or localStorage → uses Bearer auth on protected endpoints (`/api/v1/account/*`, `/api/v1/user/*`, `/api/v1/billing/*`). Server-side automation should NOT call this; use the user's `api_key` directly instead (no expiry, no device limit, no brute-force lockout). On 429 the SPA should honour `Retry-After` and surface a 'Too many attempts, retry in N seconds' toast; do NOT auto-retry inside the lockout window. **Parameters:** - `email` (body, required): Registered email address. Lowercased server-side before lookup. Returns 401 `Invalid email or password` whether the email is unregistered or the password is wrong (anti-enumeration since the 2026-05-18 hardening pass). - `password` (body, required): User password (6+ chars). Compared against the bcrypt hash stored in `users.password`. Returns 401 `Invalid email or password` on mismatch (same generic message as unknown-email so an attacker cannot distinguish the two). Never logged, never echoed in responses. **Sample response:** ```json { "message": "User login successfully.", "user": { "id": 42, "uuid": "0f14ed05-3a2e-4b76-9c11-1a7c8b3f6de2", "email": "user@example.com", "usertype": "user", "api_key": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", "verify_email": true, "is_online": true, "has_uat_access": false, "billing_admin": false, "credit_balance": 0, "notify_email": true, "notify_browser": true, "webhook_url": null, "created_at": "2026-04-15T10:00:00.000Z", "updated_at": "2026-05-02T15:51:00.000Z", "Userplan": { "id": 42, "userId": "0f14ed05-3a2e-4b76-9c11-1a7c8b3f6de2", "total_limit_api": 100, "reach_limit_api": 12, "plan": "free", "status": "active", "current_period_end": null, "created_at": "2026-04-15T10:00:00.000Z", "updated_at": "2026-05-02T15:51:00.000Z" } }, "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwZjE0..." } ``` ### POST /api/v1/auth/forgetpassword Initiate the password-reset flow. Sends a one-time reset link to the registered email. EXEMPT (`cost: 0`). Token expires after 24 hours. Always returns 200 with a generic message regardless of whether the email is registered (anti-enumeration). **Token cost:** 0 (EXEMPT — auth / billing / account / admin / health) **Response fields:** - `message` (string): Generic status message. Always identical across the registered / unregistered / already-sent / SMTP-failed cases to prevent enumeration. UI should show a 'check your email' toast on every 200. **Since:** v1.0.0 **Utility:** Send a password-reset email containing a one-time JWT token. EXEMPT (`cost: 0`). Tokens expire after 24 hours; if a previous unexpired token already exists for the user, the existing token is re-used (no duplicate email). The link in the email points at GET /api/v1/auth/verifyPasswordLink which 302-redirects to the FinRadar frontend's `/reset-password?token=…` page; the frontend then PUTs `{token, newpassword}` to /api/v1/auth/newpassword to finalize. The redirect target is server-controlled — there is no caller-supplied redirect-URL parameter (the previous `weblink` field was removed in the 2026-05-18 security hardening because it enabled an open-redirect that could exfiltrate the user UUID). **Use case:** Frontend 'Forgot password?' form: user enters email → POST here → generic 'check your email' toast (always 200, never 400) → user checks email → clicks link → 302 to frontend `/reset-password?token=…` page → user enters new password → frontend PUTs to /api/v1/auth/newpassword to finalize. **Parameters:** - `email` (body, required): Email address. Lowercased server-side. The endpoint ALWAYS returns 200 with a generic message regardless of whether the email is registered — this is intentional and prevents account enumeration. Only a registered user actually receives an email. **Sample response:** ```json { "message": "If that email is registered, a password-reset link has been sent." } ``` --- ## User Data Manage user profile and settings. ### GET /api/v1/user/ Get the authenticated user's profile, API key, and embedded subscription state. EXEMPT (`cost: 0`). JWT auth required. **Token cost:** 0 (EXEMPT — auth / billing / account / admin / health) **Response fields:** - `user` (object): Authenticated user profile (matches the `user` field in the /auth/login response). Built by `User.to_dict(include_relations=True)` — includes the three relationship rows (`Userplan`, `UserDocumentLimit`, `UserDeviceLimit`) inline. - `user.uuid` (string): Stable user identifier (UUIDv4 string). Foreign-key target in every per-user table; treat as the canonical user PK. - `user.email` (string): Lowercased registration email. Unique across `users`. Used as the login identifier. - `user.usertype` (string): Role enum value — `user` (default) or `admin`. Use to gate admin-only UI surfaces. Distinct from `billing_admin` (a finer-grained flag for billing-operator dashboard access). - `user.api_key` (string): API key for server-to-server billed calls (32-char concatenation of two UUIDs with hyphens removed). Pass as `X-API-Key: ` header on `/api/v1/sec/*`, `/api/insiders/*`, etc. Distinct from the JWT `token` (issued by /auth/login) — JWT is for browsing the dashboard, API key is for server-to-server calls. - `user.verify_email` (boolean): True if the user has clicked the verification email link. Always true here — login is gated by this flag, so the JWT bearer must already be verified. - `user.has_uat_access` (boolean): True if the user is allowed onto the UAT environment (separate cohort gate from the production access flag). Use to render UAT-environment banners or to gate UAT-only feature flags. - `user.billing_admin` (boolean): Phase 56-02 RBAC flag — true if the user can access the billing-operator dashboard (`/admin/billing/*` routes). Distinct from `usertype === 'admin'` (which grants full admin access). - `user.credit_balance` (number): Stripe-paid top-up balance in USD (not in cents). Used by [POST /api/v1/billing/quota](/docs/account/billing-and-subscription/post-billing-quota) for buying additional API quota at 100 requests / $1. NOT the same as the token balance (see [GET /api/v1/account/balance](/docs/account/token-pricing/get-account-balance) for that). - `user.webhook_url` (string (nullable)): User-configured HTTPS URL for sniper-hit webhook callbacks. Null when the user has not configured one (most users). PUT /api/v1/user/ with `webhook_url` to update. - `user.notify_email` (boolean): True if email notifications are enabled. Default true. Toggle via PUT /api/v1/user/. - `user.notify_browser` (boolean): True if in-browser push notifications are enabled. Default true. Toggle via PUT /api/v1/user/. - `user.created_at` (string): ISO-8601 UTC timestamp the user account was created (set by /auth/register). - `user.updated_at` (string): ISO-8601 UTC timestamp the user row was last modified (`onupdate=datetime.utcnow`). Bumped on profile edits, login (last-seen), and admin role changes. - `user.Userplan` (object (nullable)): Embedded subscription state — `total_limit_api` (monthly request quota), `reach_limit_api` (consumed this cycle), `plan` (`free`/`weekly`/`monthly`/`pro`/`yearly`), `status` (`active`/`canceled`/`past_due`), `current_period_end`. Null in legacy edge cases where the relationship row was never seeded; /auth/register seeds this for new users. - `user.UserDocumentLimit` (object (nullable)): Embedded document-quota state for the (legacy) RAG document-upload feature — `total_limit_GB` and `reach_limit_GB`. Most users do not consume document quota. - `user.UserDeviceLimit` (object (nullable)): Embedded device-limit state — `device_limit` (max concurrent JWT sessions, default 2) and `user_login_device` (JSON-serialized array of active session_ids). When a third login lands the oldest session_id is evicted from the array. **Since:** v1.0.0 **Utility:** Authenticated 'who am I?' probe — returns full user profile plus embedded subscription (`Userplan`), document-quota (`UserDocumentLimit`), and device-limit (`UserDeviceLimit`) relationship rows. EXEMPT (`cost: 0`). Identical shape to the `user` field in the /auth/login response (both invoke `User.to_dict(include_relations=True)`). Use to bootstrap dashboard state on app load — the JWT cookie is presumed valid; if it expired the endpoint returns 401 and the SPA should redirect to /auth/login. Includes `api_key` (32-char) for the dashboard's 'API Keys' page; the user's `credit_balance` (USD top-up wallet, NOT tokens); subscription `plan` (`free` / `weekly` / `monthly` / `pro` / `yearly`); and notification preferences (`webhook_url`, `notify_email`, `notify_browser`). **Use case:** Dashboard SPA bootstraps on load by calling this endpoint to populate user state. Server-side automation should never call this — use the user's `api_key` directly for billed endpoints, no profile fetch needed. **Sample response:** ```json { "user": { "id": 42, "uuid": "0f14ed05-3a2e-4b76-9c11-1a7c8b3f6de2", "email": "user@example.com", "usertype": "user", "api_key": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", "verify_email": true, "is_online": true, "has_uat_access": false, "billing_admin": false, "credit_balance": 12.5, "notify_email": true, "notify_browser": true, "webhook_url": "https://your-app.example.com/webhooks/finradar", "created_at": "2026-04-15T10:00:00.000Z", "updated_at": "2026-05-02T15:51:00.000Z", "Userplan": { "id": 42, "userId": "0f14ed05-3a2e-4b76-9c11-1a7c8b3f6de2", "total_limit_api": 100, "reach_limit_api": 12, "plan": "free", "status": "active", "current_period_end": null, "created_at": "2026-04-15T10:00:00.000Z", "updated_at": "2026-05-02T15:51:00.000Z" }, "UserDocumentLimit": { "id": 42, "userId": "0f14ed05-3a2e-4b76-9c11-1a7c8b3f6de2", "total_limit_GB": 5, "reach_limit_GB": "0.42", "created_at": "2026-04-15T10:00:00.000Z", "updated_at": "2026-05-02T15:51:00.000Z" }, "UserDeviceLimit": { "id": 42, "userId": "0f14ed05-3a2e-4b76-9c11-1a7c8b3f6de2", "device_limit": 2, "user_login_device": "[\"sess-abc-123\",\"sess-def-456\"]", "created_at": "2026-04-15T10:00:00.000Z", "updated_at": "2026-05-02T15:51:00.000Z" } } } ``` ### PUT /api/v1/user/ Update the authenticated user's notification preferences (`webhook_url`, `notify_email`, `notify_browser`). EXEMPT (`cost: 0`). Other profile fields are immutable via this endpoint. **Token cost:** 0 (EXEMPT — auth / billing / account / admin / health) **Response fields:** - `message` (string): Top-level result. `Profile updated successfully` on 200. On error (400): the underlying exception message. On 404: `User not found` (defensive — JWT was valid but user was deleted between auth and now). - `user` (object): Full updated user profile (same shape as GET /api/v1/user/'s `user` field). Reflects the post-update state of the three mutable fields plus the bumped `updated_at`. Use to patch SPA state without re-fetching. - `user.webhook_url` (string (nullable)): Updated webhook URL. Null if the request body cleared it (passed empty string) or if the user had never set one and this PUT did not include `webhook_url`. - `user.notify_email` (boolean): Updated email-notification preference. Reflects the body's `notify_email` if present; otherwise unchanged from the prior value. - `user.notify_browser` (boolean): Updated in-browser push-notification preference. Same merge semantics as `notify_email`. - `user.updated_at` (string): Bumped to the current UTC timestamp on every successful PUT (`onupdate=datetime.utcnow` on the User model). **Since:** v1.0.0 **Utility:** Update the authenticated user's notification preferences. EXEMPT (`cost: 0`). Only three fields are mutable here: `webhook_url`, `notify_email`, `notify_browser` — all other user fields (email, password, api_key, role, plan) are immutable via this endpoint by design (the handler hard-codes the allowlist). To change email/password use the dedicated /auth/* flows; to change subscription tier use [POST /api/v1/payment/create-checkout-session](/docs/account/billing-and-subscription/post-payment-create-checkout-session). The endpoint returns the full updated user object (same shape as GET /api/v1/user/) so dashboards can patch local state from the response without re-fetching. **Use case:** Dashboard 'Settings → Notifications' page submit handler: user toggles email/browser notifications + pastes their webhook URL → PUT here → backend persists + returns updated profile → SPA patches React state from the response. **Parameters:** - `webhook_url` (body, optional): HTTPS URL for POST callbacks when sniper targets fire. Pass an empty string to clear. Validated client-side as `^https://`; server stores as-is up to 512 chars. Webhook payload shape is documented at the Webhooks section. - `notify_email` (body, optional): Boolean — true to enable email notifications, false to disable. Default true on signup. Affects sniper-hit emails, billing-event emails, and product-update emails. - `notify_browser` (body, optional): Boolean — true to enable in-browser push notifications via the Web Push API, false to disable. Default true on signup. Browser must have granted notification permission separately for this to take effect. **Sample response:** ```json { "message": "Profile updated successfully", "user": { "id": 42, "uuid": "0f14ed05-3a2e-4b76-9c11-1a7c8b3f6de2", "email": "user@example.com", "usertype": "user", "api_key": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", "verify_email": true, "is_online": true, "has_uat_access": false, "billing_admin": false, "credit_balance": 12.5, "notify_email": false, "notify_browser": true, "webhook_url": "https://your-app.example.com/webhooks/finradar", "created_at": "2026-04-15T10:00:00.000Z", "updated_at": "2026-05-02T15:51:00.000Z" } } ``` --- ## Webhooks Automated server-to-server notifications. ### POST {your_webhook_url} Payload sent when a Sniper Target is hit. **Token cost:** 0 (EXEMPT — auth / billing / account / admin / health) **Response fields:** - `event_type` (string): Discriminator field. For sniper webhooks: typically `sniper.filing_hit`. Receivers should switch on this field to route to the appropriate handler. - `occurred_at` (string): ISO 8601 UTC timestamp of the underlying SEC event. - `delivery_id` (string): Unique webhook-delivery identifier — use for idempotency-key on the receiver (in-flight retries reuse the same delivery_id). - `target_id` (string (nullable)): Sniper target identifier (when event_type=sniper.*). - `payload` (object): Event-specific payload. The fields below describe the `sniper.filing_hit` shape. - `payload.ticker` (string): Canonical hyphen-form ticker the sniper was registered for. - `payload.issuer_cik` (string): 10-character zero-padded SEC CIK of the filing issuer. - `payload.accession_number` (string): SEC accession number in canonical `XXXXXXXXXX-YY-NNNNNN` format. Use to pull the full filing detail via [GET /api/v1/sec/filings/{accession_number}](/docs/sec-filings/main-api/get-sec-filings-accession-number). - `payload.form_type` (string): SEC form type (e.g. `10-K`, `10-Q`, `8-K`, `4`, `13F-HR`, `SC 13D`). Will match one of the `form_types` filter values from the original sniper-target creation. - `payload.filed_at` (string): ISO 8601 UTC SEC filing-acceptance timestamp (the moment EDGAR accepted the filing). - `payload.filing_url` (string): Direct URL to the filing on EDGAR — `https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=...&filenum=...&accession_number=...`. Useful for opening the filing in a browser tab from a notification system. **Since:** v1.0.0 **Utility:** DIRECTIONAL FLIP: this is NOT an endpoint FinRadar serves — it is a CONTRACT for events FinRadar will SEND to the customer's URL when a configured sniper target hits a matching SEC filing. The customer registers their receiving URL via [POST /api/v1/sniper/targets](/docs/sniper/sniper-module/post-sniper-targets) (`webhook_url` field) or as the account-level default via `/account/settings/webhooks`. FinRadar then makes an HTTP POST to that URL with the event payload documented below. The receiver MUST respond 200 within 10 seconds — non-200 / timeout / connection-error responses trigger up to 3 exponential-backoff retries (10s, 60s, 300s) before the event is dropped (with logged failure, surfaced in [GET /api/v1/account/usage](/docs/account/main-api/get-account-usage)). For high-availability event delivery, prefer the WS streaming endpoints ([/ws/main-api](/docs/main-api/main-api-stream/ws-main-api), [/ws/insider-api](/docs/insiders/insider-api-stream/ws-insider-api), [/ws/13f-live](/docs/13f/13f-live-stream/ws-13f-live)) — push delivery without the customer needing to operate a public-internet webhook receiver. **Use case:** Automated trading bot execution: customer's sniper target on TSLA fires when a 10-Q lands at SEC EDGAR -> FinRadar POSTs the event payload to the customer's `https://customer.example.com/sniper-callback` -> customer's webhook-handler parses the payload, runs the trading-rule logic (e.g. 'if revenue beats consensus by >5%, place market buy'), and submits the order via their broker API. Also useful for ops dashboards / Slack alerts / CRM events / any 'when X happens, do Y server-side' workflow that needs SEC-filing event triggers. **Parameters:** - `event_type` (body, required): Event type discriminator — one of `sniper.filing_hit` (sniper target matched a filing), `sniper.window_expired` (sniper window ended without a hit; only sent when configured), `insider.transaction.created` (Form 4 push, requires WS subscription topic mapping), `13f.holdings.updated` (13F-HR processed; requires WS topic mapping). The bulk of webhook deliveries are `sniper.filing_hit`. - `occurred_at` (body, required): ISO 8601 UTC timestamp of the underlying SEC event (filing acceptance for `sniper.filing_hit`, NOT the moment FinRadar dispatched the webhook). Use for chronological ordering on the receiver side. - `delivery_id` (body, required): Unique webhook-delivery identifier — distinct from event_id. Used by FinRadar to track retries; receiver should idempotency-key on this value to dedupe retries (in-flight retries land with the same delivery_id but new HTTP POST). - `target_id` (body, optional): Sniper target identifier when `event_type=sniper.*`. Echoed from the original [POST /api/v1/sniper/targets](/docs/sniper/sniper-module/post-sniper-targets) creation. Null on non-sniper event types. - `payload` (body, required): Event-specific payload object. For `sniper.filing_hit`: `{ ticker, issuer_cik, accession_number, form_type, filed_at, filing_url }`. For other event types, see the per-type payload contract below. **Sample response:** ```json { "event_type": "sniper.filing_hit", "occurred_at": "2026-05-02T20:14:32Z", "delivery_id": "wd_3K8mPq2nQ4xZ1Y9b", "target_id": "tgt_abc123def456", "payload": { "ticker": "TSLA", "issuer_cik": "0001318605", "accession_number": "0001318605-26-000038", "form_type": "10-Q", "filed_at": "2026-05-02T20:14:32Z", "filing_url": "https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=0001318605&accession_number=0001318605-26-000038" } } ``` --- ## Main API General platform statistics and reference data. ### GET /api/v1/stats Get database and ingestion statistics. **Token cost:** 0 (EXEMPT — auth / billing / account / admin / health) **Response fields:** - `total_filings` (integer): Cumulative count of SEC filings successfully accepted and persisted to the `filings` table. Covers Forms 3/4/5, 13F-HR/NT, 13D/G, 8-K, 10-K/Q, S-1, 20-F, and ~340 other root form types. Increments roughly +5K/day off-season and +50K/day during 13F deadline windows. - `total_insiders` (integer): Distinct count of Section 16 insider entities (officers, directors, 10%+ owners) in the `insiders` table. Typically ~85K-100K active insiders. - `total_holdings_13f` (integer): Total row count in the `form_13f_holdings` table — every (filer, issuer, period) row across all 13F filings. Typical magnitude: ~300M-400M rows across the full historical window. - `total_tickers` (integer): Distinct ticker count in the canonical catalog. Typical: ~26K post v3.11.0 catalog expansion (was ~7,968 prior). - `total_endpoints_documented` (integer): Count of distinct endpoints documented in apiDocs.js — the value surfaced on the docs page as 'X endpoints documented'. v5.0 milestone target: 156+ endpoints. - `last_filing_at` (string): ISO 8601 UTC timestamp of the most-recent successfully-ingested filing. Lag should be < 60s during ET market hours; values older than ~10 minutes outside the SEC's overnight quiet-window (10pm-6am ET) indicate an ingestion-pipeline alert. - `last_13f_quarter` (string): Most-recent calendar quarter-end (`YYYY-MM-DD`) for which 13F-HR filings have been processed. Updated within ~1h of the SEC's 45-day-after-quarter-end filing deadline. Use to surface 'as of Q1 2026' on holdings drill-downs. - `request_id` (string): Server-generated request identifier for distributed-tracing / support-ticket attribution. - `timestamp` (string): ISO 8601 UTC timestamp of the response generation. **Since:** v1.0.0 **Utility:** Lightweight platform-level metrics endpoint — total filings tracked, total endpoints documented, total tickers in the catalog, last-ingestion timestamp. EXEMPT (`cost: 0`). The cheapest 'is FinRadar alive AND has fresh data?' probe — pair with [GET /api/v1/status](/docs/resources/system-status/get-status) for service-level health rollups and [GET /api/v1/facts](/docs/home/main-api/get-facts) for the per-form-type 90-day breakdown. Designed to be safe to poll at 60-second cadence from dashboard widgets without affecting plan budget. **Use case:** Dashboard widget showing total platform metrics (X filings tracked across Y companies, last ingested Z minutes ago). Also useful as a marketing-page counter ('FinRadar tracks 2.4M+ SEC filings across 26K+ securities') and for client-side staleness detection ('last ingestion was 47 minutes ago — show a warning'). **Sample response:** ```json { "total_filings": 2420187, "total_insiders": 92418, "total_holdings_13f": 348290612, "total_tickers": 26080, "total_endpoints_documented": 156, "last_filing_at": "2026-05-02T20:14:32Z", "last_13f_quarter": "2025-12-31", "request_id": "req_5pQ9LmN8K2vXwY3z", "timestamp": "2026-05-02T20:15:00Z" } ``` ### GET /api/v1/facts Get system-wide facts. **Token cost:** 10 tokens per call **Response fields:** - `total_filings` (integer): Cumulative count of SEC filings successfully accepted and persisted to the `filings` table since 2000 — covers Forms 3/4/5, 13F-HR/NT, 13D/G, 8-K, 10-K/Q, S-1, 20-F, and ~340 other root form types. Increments roughly +5K/day off-season and +50K/day during 13F deadline windows. - `last_filing_at` (string): ISO-8601 UTC timestamp of the most recent successful filing acceptance (the moment the filing landed in our DB, NOT the filed_at on EDGAR which is timezone-loose). Lag should be < 60s during ET market hours; values older than ~10 minutes outside the SEC's overnight quiet-window (10pm-6am ET) are an ingestion-pipeline alert. - `queue_depth` (integer): Live count of accepted-but-not-yet-parsed filings in the Celery `sec_13f` + `sec_filings` queues. Steady-state < 50; values 100-500 indicate filing-season pressure (normal at 10-Q peak); values > 1,000 indicate worker backlog and merit a sec_watchdog inspection. - `by_form_type` (object): Map of form_type → count for the last 90 days (e.g. `{"4": 28432, "13F-HR": 9617}`). Only forms with non-zero activity in the window appear. Use this to confirm expected magnitudes per form type before kicking off heavy historical queries; sudden zeros on a form type usually indicate a parser regression rather than an SEC issuance pause. - `as_of` (string): ISO-8601 UTC timestamp the snapshot was generated server-side (cached up to 60 seconds). Treat any consumer-side `now() - as_of > 90s` as stale-cache contention rather than a stale source — re-poll once before alerting. **Since:** v1.0.0 **Utility:** A lightweight platform health probe that returns the cumulative SEC filing count, the most-recent acceptance timestamp, the live parsing-queue depth, and a per-form-type breakdown of the last 90 days. Operations teams and external status-board integrations use this as a 60-second-cached canonical reference for 'is FinRadar ingesting?' — pair it with `GET /api/v1/status` to also cover service-level health. The 90-day form-type histogram is the cheapest way to confirm Form 4 / 13F-HR / 8-K / 10-K throughput is at expected magnitudes before kicking off heavier per-filing queries. **Use case:** Dashboard widgets showing total filings processed. **Sample response:** ```json { "total_filings": 2845129, "last_filing_at": "2026-05-01T20:54:12.000Z", "queue_depth": 14, "by_form_type": { "4": 28432, "13F-HR": 9617, "8-K": 16244, "10-K": 4112, "10-Q": 12055, "13D": 412, "13G": 1208 }, "as_of": "2026-05-01T20:55:00.000Z" } ``` --- ## SEC Filings API Core access to 25+ years of SEC filings. ### GET /api/v1/sec/filings List filings with pagination and filtering. When querying by ticker, if no local filings exist yet, the API automatically backfills the complete filing history from SEC EDGAR (first call may take a few seconds). **Token cost:** 5 tokens per call **Response fields:** - `status` (string): Always `"success"` on 2xx. ApiResponse envelope marker — present on every endpoint that wraps via `ApiResponse.paginated()`. Use to assert the call succeeded before reading `data`. - `request_id` (string (nullable)): Per-request UUID generated server-side. Surface this in support tickets — operations can grep it directly out of nginx + main_api logs to trace the exact request lifecycle (gateway routing, auth resolution, token debit, response). - `timestamp` (string): ISO-8601 UTC timestamp the response was generated server-side (suffix `Z`). Use as the freshness marker — auto-backfill calls that take a few seconds will surface here as a measurable lag from your client-side `Date.now()`. - `data` (array): Array of filing rows matching the query, sorted by `filed_date DESC` by default (newest first; configurable via `sort` param). Empty array when no rows match — never null. One element per filing — see `data[].*` for shape. - `data[].id` (integer): Internal `sec_filing_entries.id` row ID (auto-increment primary key). Stable across calls but not portable across environments — always join via `accession_number` for cross-environment work. - `data[].accession_number` (string): SEC accession number in the canonical 18-char dashed format `XXXXXXXXXX-YY-NNNNNN` (e.g. `0000320193-25-000123`). Globally unique across EDGAR; the natural primary key for any filing. Pass this to `GET /api/v1/sec/filings/{accession_number}` to retrieve metadata and document URLs. - `data[].ticker` (string (nullable)): Resolved ticker from the filer CIK via the `ticker_norm_aliases` lookup. Null when the filer has no public-equity ticker (private funds, individuals filing Form 4, foreign issuers without ADRs, dissolved entities). Multi-class issuers return the primary share class (e.g. BRK-A for Berkshire Hathaway). - `data[].form_type` (string): Canonical SEC form type as filed (e.g. `10-K`, `10-Q`, `8-K`, `4`, `13F-HR`, `13F-NT`, `13D`, `13G`, `S-1`, `20-F`, `144`, `DEF 14A`). Amendment variants append `/A` (e.g. `13D/A`, `10-K/A`). NT-prefixed and -NT-suffixed forms (`NT 10-K`, `13F-NT`) appear verbatim — these have no information table to drill into. Reference [SEC EDGAR form types](https://www.sec.gov/forms) for the full catalog. - `data[].cik` (string (nullable)): Filer CIK as a zero-padded 10-character string (e.g. `0000320193` for Apple). Note: SEC EDGAR's submissions API returns the unpadded form; FinRadar normalizes to 10-char padded form everywhere. Stable across decades — same CIK refers to the same legal entity. - `data[].entity_name` (string (nullable)): Filer's legal entity name as registered with SEC (e.g. `Apple Inc.`, `BERKSHIRE HATHAWAY INC`). Casing is whatever EDGAR provides — typically uppercase for institutional filers, mixed-case for issuers. For display, prefer the `company_name` alias which is the same value. - `data[].company_name` (string (nullable)): Frontend-friendly alias of `entity_name`. Same value, kept for backwards compatibility with older client code that queried `company_name`. - `data[].role` (string (nullable)): Role of the filer in this filing — relevant for insider filings (Form 4) and institutional filings (13F). Null for issuer filings (10-K/10-Q/8-K). Common values: `Reporting Owner`, `Issuer`, `Filer`. - `data[].title` (string): Free-text title from the EDGAR submissions feed (e.g. `8-K - APPLE INC (0000320193) (Filer)`). Useful for full-text search; for structured fields prefer `form_type` + `entity_name`. - `data[].summary_html` (string (nullable)): Optional HTML summary block from EDGAR (typically empty on Form 4 and 8-K; populated on 13F-HR with structured holdings summary). Treat as untrusted HTML — sanitize before rendering in a browser. - `data[].filing_size` (string (nullable)): Filing size as a human-readable string (e.g. `"245 KB"`, `"2.1 MB"`). NOT a numeric byte count — for byte-level work parse the string client-side or fetch the document directly. - `data[].filed_date` (string (nullable)): ISO date `YYYY-MM-DD` of the filing's SEC acceptance date in ET (NOT UTC; matches the date EDGAR's index pages show). For intra-day ordering use `updated_at` which carries a full timestamp. - `data[].filed_at` (string (nullable)): Frontend-friendly alias of `filed_date`. Same value, kept for backwards compatibility. - `data[].updated_at` (string): ISO-8601 UTC timestamp of the last server-side update to this row (e.g. on a backfill refresh). Use as the cursor for incremental sync — sort `updated_at:desc` and persist the max value as your watermark. - `data[].primary_link` (string (nullable)): Canonical SEC EDGAR URL for the filing's index page (lists all documents in the filing package). Use this as the human-readable link in dashboards. - `data[].primary_html_url` (string (nullable)): Direct URL of the filing's primary HTML document (the actual 10-K/8-K/13F body, not the index page). For programmatic access use this with `GET /api/v1/sec/document?url=...` or with `GET /api/v1/sec/filings/{accession_number}/html`. - `data[].document_files` (array (nullable)): JSON array of structured document descriptors `[{type, document, description, size}, ...]` extracted from the filing's index page. Useful for filtering specific exhibits (e.g. EX-99.1, EX-10.1) before fetching. - `data[].data_files` (array (nullable)): JSON array of XBRL/data file descriptors `[{type, document, description, size}, ...]`. Populated on 10-K/10-Q/8-K filings with embedded XBRL; null on filings without XBRL (e.g. older Form 4s). - `data[].items` (array (nullable)): Array of 8-K item codes disclosed in this filing (e.g. `["2.02", "9.01"]` for an earnings-release 8-K). Null on non-8-K forms. Use for material-event surveillance — filter on specific item codes (1.01 = Material Definitive Agreement, 2.02 = Earnings, 5.02 = Officer Change, 8.01 = Other). - `data[].period_of_report` (string (nullable)): ISO `YYYY-MM-DD` period the filing covers (the fiscal-period-end date for 10-K/10-Q, the trade date for Form 4, the report date for 13F-HR). Null for filings without a meaningful period (e.g. some 8-Ks). Differs from `filed_date` — `period_of_report` is the as-of date, `filed_date` is the disclosure date. - `data[].sic` (string (nullable)): Standard Industrial Classification code (4-digit string, e.g. `3674` = Semiconductors). Null when not assigned. For richer industry categorization, join against the Sharadar SF1 industry table via the resolved ticker. - `data[].all_links` (array (nullable)): Array of every document URL listed on the filing's index page — exhibits, XBRL data, images, the primary document. Pass any element to `GET /api/v1/sec/document?url=...` to fetch its content. - `data[].first_seen_at` (string): ISO-8601 UTC timestamp when FinRadar first ingested this filing (typically within seconds of SEC acceptance for filings caught by the RSS poller; longer for backfilled rows). Use for ingest-latency analytics. - `data[].last_seen_at` (string): ISO-8601 UTC timestamp of the last time the RSS poller observed this filing on EDGAR. Updated on every poll cycle for filings still in the active feed; frozen for older filings. - `meta.pagination` (object): Offset-based pagination metadata: `{total, limit, offset, has_more}`. Use `has_more` as the canonical end-of-iteration signal rather than comparing `data.length` to `limit`. - `meta.pagination.total` (integer): Exact total count of filings matching the query (full-table count, not just the current page). Computed via a parallel `COUNT(*)` query — reliable for UI pagers but adds ~50-200ms latency on broad queries. - `meta.pagination.has_more` (boolean): `true` when more pages exist beyond this one (`total > offset + data.length`). Canonical end-of-iteration signal — when `false`, stop paginating. - `meta.backfilled_from_edgar` (boolean (nullable)): Present and `true` ONLY when this call triggered an auto-backfill from SEC EDGAR (i.e. the ticker had no local data or only recent RSS-captured data). Absent on the typical hot-path call. Use to surface a UX hint ("first call took a moment to fetch full history"). - `meta.requested_ticker` (string (nullable)): Echo of the resolved ticker after Phase-53 normalization (e.g. `BRK.A` becomes `BRK-A`). Present only when the request included a `ticker` param. - `meta.primary_ticker` (string (nullable)): Canonical primary share class for the filer (e.g. for Berkshire Hathaway, returns `BRK-A` even if the user queried `BRK.B`). Use to pivot to the multi-class top-of-book. - `meta.cik` (string (nullable)): 10-char zero-padded CIK of the resolved ticker. Use as the join key against XBRL, 13F, or insider-filing tables. - `meta.company` (string (nullable)): Resolved entity name for the ticker (e.g. `Apple Inc.`). Useful as a display label when the user queried by ticker rather than entity_name. **Since:** v1.0.0 **Utility:** Canonical SEC filings list endpoint — paginated, ticker/CIK/form-type/date-range filterable, and self-healing (auto-backfills from SEC EDGAR's submissions API on first ticker query so you never see an empty result for a real issuer). Returns one row per filing with the SEC accession number, filer CIK + entity name + ticker, the form type (canonical strings: `10-K`, `10-Q`, `8-K`, `4`, `13F-HR`, `13D`, `13G`, `S-1`, `20-F`, `144`, `DEF 14A`, etc. — see [SEC EDGAR's form-type catalog](https://www.sec.gov/forms) for the full list), filing dates, and the document URL set (`primary_link`, `primary_html_url`, full `all_links` array). Pair this with `GET /api/v1/sec/filings/{accession_number}` to drill into a specific filing's documents, with `POST /api/v1/scrapping/search/` for full-text content search across filing bodies, or with `GET /api/v1/sec/filings/stats` for aggregate counts. **Use case:** Building a timeline of filings for a specific company or monitoring all new 8-K filings. Example: GET /api/v1/sec/filings?ticker=XAIR&limit=500 returns all XAIR filings, auto-fetching from EDGAR if needed. **Parameters:** - `ticker` (query, optional): Company Ticker (e.g. AAPL). Triggers auto-backfill from SEC EDGAR if no local data exists. - `form_type` (query, optional): Filter by Form Type (10-K, 4, 8-K) - `from_date` (query, optional): Start Date (YYYY-MM-DD) - `to_date` (query, optional): End Date (YYYY-MM-DD) - `sort` (query, optional, default: filed_date:desc): Sort order. Format: field:direction. Fields: filed_date, updated_at. Directions: asc, desc. Shorthand 'asc'/'desc' also accepted (defaults to filed_date). Examples: sort=desc, sort=filed_date:asc - `limit` (query, optional, default: 50): Max 500 records - `offset` (query, optional, default: 0): Pagination offset **Sample response:** ```json { "status": "success", "request_id": "0f14ed05-3a2e-4b76-9c11-1a7c8b3f6de2", "timestamp": "2026-05-02T16:30:14.122Z", "data": [ { "id": 18234567, "accession_number": "0000320193-25-000123", "ticker": "AAPL", "form_type": "10-K", "cik": "0000320193", "entity_name": "Apple Inc.", "company_name": "Apple Inc.", "role": null, "title": "10-K - APPLE INC (0000320193) (Filer)", "summary_html": null, "filing_size": "12.4 MB", "filed_date": "2025-11-01", "filed_at": "2025-11-01", "updated_at": "2025-11-01T20:14:32.000Z", "primary_link": "https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=0000320193&type=10-K&dateb=&owner=include&count=40", "primary_html_url": "https://www.sec.gov/Archives/edgar/data/320193/000032019325000123/aapl-20250928.htm", "document_files": [ { "type": "10-K", "document": "aapl-20250928.htm", "description": "10-K", "size": "12943821" } ], "data_files": [ { "type": "EX-101.INS", "document": "aapl-20250928.xml", "description": "XBRL Instance Document", "size": "412390" } ], "items": null, "period_of_report": "2025-09-28", "sic": "3571", "all_links": [ "https://www.sec.gov/Archives/edgar/data/320193/000032019325000123/aapl-20250928.htm", "https://www.sec.gov/Archives/edgar/data/320193/000032019325000123/aapl-20250928.xml" ], "first_seen_at": "2025-11-01T20:14:35.000Z", "last_seen_at": "2025-11-02T03:00:01.000Z" } ], "meta": { "pagination": { "total": 2148, "limit": 50, "offset": 0, "has_more": true }, "requested_ticker": "AAPL", "primary_ticker": "AAPL", "cik": "0000320193", "company": "Apple Inc." } } ``` ### POST /api/v1/sec/filings/backfill Explicitly backfill ALL historical filings for a ticker from SEC EDGAR. Fetches the complete filing history from data.sec.gov/submissions/ and stores it locally. Subsequent GET queries will return the full history instantly. **Token cost:** 0 (EXEMPT — auth / billing / account / admin / health) **Response fields:** - `status` (string): Always `"success"` on 2xx. ApiResponse envelope marker — wraps via `ApiResponse.success()`. - `request_id` (string (nullable)): Per-request UUID generated server-side. Surface this in support tickets — operations can grep it directly out of nginx + main_api logs. - `timestamp` (string): ISO-8601 UTC timestamp the response was generated server-side (suffix `Z`). - `data` (object): Backfill result envelope — see `data.*` for fields. Wall-clock duration scales with how many filings the issuer has — typically 2-15s for an issuer with 500-3000 historical filings; longer for very-tenured issuers (e.g. GE, IBM) which can have 10K+ filings. - `data.ticker` (string): Echoed input ticker after Phase-53 normalization (e.g. `BRK.A` → `BRK-A`). Useful for confirming the resolution before relying on it downstream. - `data.cik` (string): 10-char zero-padded CIK resolved from the input ticker. Use this as the join key for follow-up queries against XBRL, 13F, or insider-filing tables. - `data.company` (string): Resolved entity name (e.g. `Apple Inc.`). Useful as a display label for the backfilled batch. - `data.inserted` (integer): Count of NEW filings inserted into the local index by this call (rows that didn't exist before). 0 on a no-op repeat call. Compare against `total_from_edgar` to gauge completeness. - `data.already_existed` (integer): Count of EDGAR filings that were already in the local index (skipped via the `(accession_number, cik)` UNIQUE constraint). On a fresh-ticker backfill: 0. On a repeat call: equals `total_from_edgar`. - `data.total_from_edgar` (integer): Total count of filings returned by SEC EDGAR's submissions API for this CIK. Equals `inserted + already_existed + errors`. - `data.errors` (integer): Count of EDGAR rows that failed to ingest (parse errors, transient DB issues). Should typically be 0 — non-zero values indicate a parse-edge-case worth filing a support ticket for. Errors are logged server-side with the accession number. **Since:** v1.0.0 **Utility:** Explicit, free (cost: 0) trigger to pre-load the FULL submissions history for a ticker from SEC EDGAR's `data.sec.gov/submissions/CIK{cik}.json` endpoint into FinRadar's local index. Use this to warm the cache before running an analysis batch — once a ticker has been backfilled, every subsequent call to `GET /api/v1/sec/filings?ticker=X` hits the local DB instantly (no SEC round-trip). Cost is zero because the work is one-shot per-ticker — repeated backfills detect the existing rows via the `(accession_number, cik)` UNIQUE constraint and only insert deltas. Idempotent: safe to call multiple times — `inserted` will be 0 on no-op repeat calls. **Use case:** Call POST /filings/backfill?ticker=AAPL to load all ~2000 Apple filings in one shot, then query them instantly with GET /filings?ticker=AAPL. **Parameters:** - `ticker` (query, required): Stock ticker to backfill (e.g. XAIR, AAPL, TSLA) **Sample response:** ```json { "status": "success", "request_id": "0f14ed05-3a2e-4b76-9c11-1a7c8b3f6de2", "timestamp": "2026-05-02T16:30:14.122Z", "data": { "ticker": "AAPL", "cik": "0000320193", "company": "Apple Inc.", "inserted": 1843, "already_existed": 0, "total_from_edgar": 1843, "errors": 0 } } ``` ### GET /api/v1/sec/filings/{accession_number} Get filing metadata by accession number — returns filing details, document URLs (all_links), and structured data. Does NOT return the document content itself. **Token cost:** 5 tokens per call **Response fields:** - `status` (string): Always `"success"` on 2xx. ApiResponse envelope marker — wraps via `ApiResponse.success()`. - `request_id` (string (nullable)): Per-request UUID generated server-side. Surface this in support tickets — operations can grep it directly out of nginx + main_api logs. - `timestamp` (string): ISO-8601 UTC timestamp the response was generated server-side (suffix `Z`). - `data` (object): Full filing metadata record. Same shape as a single element of `GET /api/v1/sec/filings`'s `data[]` array, PLUS the `raw_entry_xml` field (which is excluded from list responses for payload-size reasons). See `data.*` for fields. - `data.id` (integer): Internal `sec_filing_entries.id` row ID (auto-increment primary key). - `data.accession_number` (string): SEC accession number in canonical 18-char dashed format `XXXXXXXXXX-YY-NNNNNN`. Echoes the input. - `data.ticker` (string (nullable)): Resolved ticker from the filer CIK via the `ticker_norm_aliases` lookup. Null when the filer has no public-equity ticker. - `data.form_type` (string): Canonical SEC form type as filed (e.g. `10-K`, `13F-HR`, `8-K`, `4`, `13D`, `13G`, `S-1`, `20-F`, `144`, `DEF 14A`). Amendment variants append `/A`. NT-prefixed forms (e.g. `NT 10-K`, `13F-NT`) appear verbatim. Reference [SEC EDGAR form types](https://www.sec.gov/forms). - `data.cik` (string (nullable)): Filer CIK as a zero-padded 10-character string (e.g. `0000320193`). - `data.entity_name` (string (nullable)): Filer's legal entity name as registered with SEC. - `data.company_name` (string (nullable)): Frontend-friendly alias of `entity_name`. - `data.role` (string (nullable)): Role of the filer in this filing (`Reporting Owner`, `Issuer`, `Filer`). Null for issuer filings. - `data.title` (string): Free-text title from the EDGAR submissions feed. - `data.summary_html` (string (nullable)): Optional HTML summary block from EDGAR. Sanitize before rendering. - `data.filing_size` (string (nullable)): Filing size as a human-readable string (e.g. `"245 KB"`, `"2.1 MB"`). NOT a numeric byte count. - `data.filed_date` (string (nullable)): ISO date `YYYY-MM-DD` of the filing's SEC acceptance date in ET. - `data.filed_at` (string (nullable)): Frontend-friendly alias of `filed_date`. - `data.updated_at` (string): ISO-8601 UTC timestamp of the last server-side update to this row. - `data.primary_link` (string (nullable)): Canonical SEC EDGAR URL for the filing's index page. - `data.primary_html_url` (string (nullable)): Direct URL of the filing's primary HTML document (the actual 10-K/8-K/13F body). For the parsed body content use the `/html` companion endpoint. - `data.document_files` (array (nullable)): JSON array of structured document descriptors `[{type, document, description, size}, ...]`. - `data.data_files` (array (nullable)): JSON array of XBRL/data file descriptors `[{type, document, description, size}, ...]`. Populated on filings with embedded XBRL. - `data.items` (array (nullable)): Array of 8-K item codes disclosed in this filing (e.g. `["2.02", "9.01"]`). Null on non-8-K forms. - `data.period_of_report` (string (nullable)): ISO `YYYY-MM-DD` period the filing covers (fiscal-period-end for 10-K/10-Q, trade date for Form 4, report date for 13F-HR). - `data.sic` (string (nullable)): Standard Industrial Classification code (4-digit string). - `data.all_links` (array (nullable)): Array of every document URL listed on the filing's index page — exhibits, XBRL data, images, the primary document. Pass any element to `GET /api/v1/sec/document?url=...` to fetch its content. - `data.raw_entry_xml` (string (nullable)): Raw XML entry from SEC EDGAR's RSS feed at the time of ingestion. Useful for forensic / audit-trail work — preserved verbatim from EDGAR for filings ingested via the RSS poller. Null for filings ingested via the submissions-API backfill path (which doesn't expose entry XML). NOT returned by the list endpoint — only the detail endpoint includes this field. - `data.first_seen_at` (string): ISO-8601 UTC timestamp when FinRadar first ingested this filing. - `data.last_seen_at` (string): ISO-8601 UTC timestamp of the last time the RSS poller observed this filing on EDGAR. **Since:** v1.0.0 **Utility:** Canonical filing-detail lookup by SEC accession number — returns the full metadata record (form type, filer CIK + entity name, filed date, period of report, SIC, summary HTML, the complete `all_links` array of every document URL in the filing package, and the `raw_entry_xml` from EDGAR's RSS feed for archival/audit). The natural "step 1" of any filing workflow: find the filing by some other route (search, RSS, ticker timeline), then call this to get the document URLs, then drill into a specific document via `/{accession_number}/html` (primary doc shortcut) or `/api/v1/sec/document?url=...` (any document by URL). Accession numbers are 18-char dashed `XXXXXXXXXX-YY-NNNNNN` (e.g. `0000320193-25-000123`); the path-parameter accepts either the dashed or the un-dashed form. **Use case:** You have an accession number (e.g. from a search) and need the filing date, form type, filer info, and links to all documents in the filing package. **Parameters:** - `accession_number` (path, required): SEC accession number in 18-char dashed format `XXXXXXXXXX-YY-NNNNNN` (e.g. `0000320193-25-000123`). Also accepts the un-dashed 18-char form (`000032019325000123`). Returns 404 when the accession is not in the local index — call `POST /api/v1/sec/filings/backfill?ticker=...` first if you suspect the ticker hasn't been backfilled yet. **Sample response:** ```json { "status": "success", "request_id": "0f14ed05-3a2e-4b76-9c11-1a7c8b3f6de2", "timestamp": "2026-05-02T16:30:14.122Z", "data": { "id": 18234567, "accession_number": "0000320193-25-000123", "ticker": "AAPL", "form_type": "10-K", "cik": "0000320193", "entity_name": "Apple Inc.", "company_name": "Apple Inc.", "role": null, "title": "10-K - APPLE INC (0000320193) (Filer)", "summary_html": null, "filing_size": "12.4 MB", "filed_date": "2025-11-01", "filed_at": "2025-11-01", "updated_at": "2025-11-01T20:14:32.000Z", "primary_link": "https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=0000320193&type=10-K", "primary_html_url": "https://www.sec.gov/Archives/edgar/data/320193/000032019325000123/aapl-20250928.htm", "document_files": [ { "type": "10-K", "document": "aapl-20250928.htm", "description": "10-K", "size": "12943821" } ], "data_files": [ { "type": "EX-101.INS", "document": "aapl-20250928.xml", "description": "XBRL Instance Document", "size": "412390" } ], "items": null, "period_of_report": "2025-09-28", "sic": "3571", "all_links": [ "https://www.sec.gov/Archives/edgar/data/320193/000032019325000123/aapl-20250928.htm", "https://www.sec.gov/Archives/edgar/data/320193/000032019325000123/aapl-20250928.xml" ], "raw_entry_xml": "\n 10-K - APPLE INC...\n \n ...\n", "first_seen_at": "2025-11-01T20:14:35.000Z", "last_seen_at": "2025-11-02T03:00:01.000Z" } } ``` ### GET /api/v1/sec/filings/{accession_number}/html Get the primary document's HTML content by accession number. Returns the full raw HTML of the main filing document (e.g. the 10-K, 8-K body, or — for `13F-HR` / `13F-HR/A` — the INFORMATION TABLE with positions, not the cover page). For XBRL filings, automatically resolves through the SEC viewer to the actual inline XBRL document. **Token cost:** 5 tokens per call **Response fields:** - `(body)` (string): Raw filing HTML — UTF-8 encoded `text/html` body. NOT a JSON envelope (this endpoint returns the document body directly). Server-side post-processing strips `