The walk
1. List filings, pick the accession
/v2/filings defaults to form=10-K,10-Q,8-K and limit=20. Pass a tighter form CSV when you only want the annual report. Here, form=10-K returns just the 10-K rows for AAPL.
- Python
- TypeScript
list_filings.py
form):
filings.data.filings[…] (one row)
size_bytes is 9.4 MB. You are not going to read it whole; sections is empty on the list because the index lives on the next call.
If the user asks for “the latest periodic filing” rather than the 10-K specifically, request
form=10-K,10-Q, then pick by filing_date and tell the reader which form you used. Calling something “the latest 10-K” after selecting a 10-Q is the most common reporting error here.2. Read the section index
GET /v2/filings/0000320193-25-000079 returns the section index with a char_count per section. That count is your budget: it tells you how much text the next call will return before you spend it.
filing-10k.data (trimmed)
sections[].name before requesting one. A 10-K carries business, risk_factors, mda. A 10-Q carries mda, risk_factors, quantitative_qualitative_disclosures, controls_and_procedures; note it has no business section. Don’t assume a name exists on one form because it appeared on another.
8-K metadata has a different shape: no sections array at all. Instead it carries items (the disclosed item codes with titles, e.g. 2.02: Results of Operations and Financial Condition) and a press_release object with available and char_count. When press_release.available is true, fetch it with /section/press_release; the risk_factors-style names do not exist on an 8-K. items and items_text on list rows are 8-K item codes too, empty arrays on 10-K/10-Q rows. Form 4 metadata carries neither sections nor items, just the filing facts; its period_of_report is usually populated but can be null.
The accession routes accept an optional ?symbol= cross-check. GET /v2/filings/0000320193-25-000079?symbol=AAPL confirms the accession belongs to AAPL; a mismatch returns PARAM_INVALID. A well-formed accession that matches no filing returns 400 PARAM_INVALID (Accession '…' was not found on SEC EDGAR…), and a malformed one is also 400 with a distinct is malformed message; neither is a 200. The symbol_status { "resolved": false, "reason": "symbol_unresolved_from_accession" } shape appears only for a real filing whose filer doesn’t map to a supported ticker, not for a missing accession. See error codes for the full set.
3. Fetch one section
GET /v2/filings/0000320193-25-000079/section/risk_factors returns the text and echoes the same char_count. Fetch only the section you intend to read.
filing-section-risk-factors.data (text trimmed)
text above is trimmed for the page; the live response returns the full 68,069 characters. Summarize from that text and nothing else.
A risk summary built only from this section:
answer
text. That is the guardrail: a SEC risk summary is a summary of one section’s words. Do not add a headline, an analyst note, a price reaction, or a remembered fact: if it is not in the section text, it does not go in the answer. This is the anti-hallucination rule from the agent instructions, applied to filings.
MCP shape
The hosted tools take the same walk, with one argument difference: RESTform is a CSV string, but the MCP forms argument is a JSON array.
Cold first call
The first read of an accession pulls from EDGAR and can take up to ~20 seconds for a large 10-K; repeat reads of the same accession return in well under a second. Listing filings is fast either way; the cold cost is per accession. Don’t block a whole UI on it. See coverage and gaps for the latency note. Filing and insider data are alsounsupported for the 1,125 foreign private issuers that file 20-F/40-F/6-K, and for ETFs; the row will carry a machine-readable reason rather than text.
Insider scan
Form 4 activity is a separate endpoint with its own rollup, not a filing section.