How to Download 10-K Filings from EDGAR
Download 10-K filings from EDGAR with curl and jq: the User-Agent header that avoids a 403, the ticker to CIK map, and the path to the document itself.
Everything in EDGAR is free, unauthenticated JSON and static files, and pulling a 10-K out of it takes two things: the company's CIK number and a User-Agent header that says who you are. Downloading 10-K filings from EDGAR fails for most people on the second one, with a 403 instead of a document. The four access paths below, with the exact requests that work, take you from a ticker symbol to an annual report on disk in about six lines of shell. For what to do with the document once you have it, read how to read a 10-K.
Why curl gets a 403 from sec.gov
The SEC fair access policy requires every automated request to identify itself. A request that declares no agent, or that sends the default curl agent string, comes back as HTTP 403 with a page titled "Your Request Originates from an Undeclared Automated Tool". No amount of retrying changes it.
# 403: no declared agent
curl -sS -o /dev/null -w '%{http_code}\n' \
https://www.sec.gov/files/company_tickers.json
One header fixes it, and the same URL returns 200.
UA='Strasmore Research [email protected]'
# 200: a name plus a contact address
curl -sS -A "$UA" --compressed -o /dev/null -w '%{http_code}\n' \
https://www.sec.gov/files/company_tickers.json
The sample header on the SEC access page is a company name followed by a contact email, and it asks for Accept-Encoding: gzip, deflate as well, which curl --compressed sends for you. Put a real address in that string: it is the only route the SEC has to reach you before it blocks an IP. Nothing else is required. No key, no token, no account.
Four ways out of EDGAR
https://www.sec.gov/files/company_tickers.jsonmaps every listed ticker to a CIK, the Central Index Key that identifies a filer. One file, under a megabyte as of October 2026.https://data.sec.gov/submissions/CIK##########.jsonreturns one company's filing history: the index of what it filed and when.https://www.sec.gov/Archives/edgar/data/<cik>/<accession>/holds the documents themselves, the annual report and every exhibit submitted with it.https://efts.sec.gov/LATEST/search-indexis the full-text search endpoint behind the EDGAR search page, covering filings from 2001 onward.
Annual reports are a thin slice of what moves through those paths. A company files one 10-K a year, while an insider transaction report arrives with every trade an officer makes. The panel below counts filings by form type over the trailing year.
| form_type | filings | filers |
|---|---|---|
| 4 | 348515 | 57602 |
| 424B2 | 191134 | 906 |
| 144 | 71927 | 15438 |
| 8-K | 63466 | 6555 |
| NPORT-P | 53121 | 2015 |
| 3 | 44421 | 24909 |
| D | 39863 | 37593 |
| 13F-HR | 34691 | 9364 |
| SCHEDULE 13G/A | 33997 | 6862 |
| 6-K | 27430 | 1341 |
| FWP | 26337 | 1095 |
| D/A | 22876 | 19706 |
The exact SQL behind every number
SELECT
form_type,
count() AS filings,
uniqExact(cik) AS filers
FROM global_markets.stocks_sec_edgar_index
WHERE filing_date >= today() - 365
AND filing_date < today()
GROUP BY form_type
ORDER BY filings DESC
LIMIT 12Form 4 leads the trailing year with 348515 filings from 57602 distinct filers. Those are the insider forms covered in how to read a Form 4, and they are the reason a naive crawler of "everything new" drowns. Pick the form type first. For the shape of the rest of the list, see the most common SEC filings.
Step 1: turn a ticker into a CIK
The CIK is the identifier EDGAR actually uses, and every API path wants it in place of the ticker. The map is one JSON object keyed by position, with cik_str, ticker and title in each value.
| ticker | issuer | cik | cik_zero_padded | filings_on_record |
|---|---|---|---|---|
| AAPL | Apple Inc. | 0000320193 | 0000320193 | 2081 |
| JNJ | JOHNSON & JOHNSON | 0000200406 | 0000200406 | 3103 |
| KO | COCA COLA CO | 0000021344 | 0000021344 | 3119 |
| MSFT | MICROSOFT CORP | 0000789019 | 0000789019 | 4302 |
| NVDA | NVIDIA CORP | 0001045810 | 0001045810 | 2332 |
The exact SQL behind every number
SELECT
ticker,
issuer,
toString(cik) AS cik,
leftPad(toString(cik), 10, '0') AS cik_zero_padded,
filings_on_record
FROM (
SELECT
ticker,
any(issuer_name) AS issuer,
any(cik) AS cik,
count() AS filings_on_record
FROM global_markets.stocks_sec_edgar_index
WHERE ticker IN ('AAPL', 'MSFT', 'NVDA', 'KO', 'JNJ')
GROUP BY ticker
)
ORDER BY tickerAAPL files under CIK 0000320193, which the data.sec.gov endpoints want zero-padded to ten digits as 0000320193. The Archives path takes the unpadded number instead, and mixing the two up is the next thing that breaks after the missing header. Apple Inc. alone has 2081 filings on record in this index.
One line of jq does the lookup, and printf does the padding:
curl -s -A "$UA" --compressed \
https://www.sec.gov/files/company_tickers.json > tickers.json
CIK=$(jq -r '.[] | select(.ticker == "AAPL") | .cik_str' tickers.json)
PAD=$(printf '%010d' "$CIK")
Step 2: read the filing index from the submissions JSON
The submissions file is columnar. Under filings.recent sit parallel arrays named form, filingDate, reportDate, accessionNumber, primaryDocument, primaryDocDescription, isXBRL and size, all the same length, one position per filing. It carries at least a year of history or the 1,000 most recent filings, whichever is larger. Anything older is listed under filings.files, each chunk with a name, filingCount, filingFrom and filingTo.
The jq transpose filter turns those parallel arrays back into rows:
curl -s -A "$UA" --compressed \
"https://data.sec.gov/submissions/CIK$PAD.json" > subs.json
read -r FORM FDATE ACC DOC <<<"$(jq -r '
.filings.recent
| [.form, .filingDate, .accessionNumber, .primaryDocument]
| transpose
| map(select(.[0] == "10-K"))
| sort_by(.[1])
| last
| @tsv' subs.json)"
Match "10-K" exactly. An amendment files as 10-K/A and occupies its own row, so select(.[0] | startswith("10-K")) is the version that catches both.
Step 3: build the document URL
The accession number prints with dashes in the JSON and appears without them in the folder path. That one substitution is the whole trick:
URL="https://www.sec.gov/Archives/edgar/data/$CIK/${ACC//-/}/$DOC"
echo "$URL"
curl -s -A "$UA" --compressed -o "AAPL-$FDATE-10K.htm" "$URL"
Swap the document name for index.json in that same folder and you get a listing of every file in the submission, exhibits and XBRL instance included. The primary document is the readable annual report; the rest is machinery.
The download is long, and the length is unevenly spread across it. The panel below measures the text of the sections this index holds for the most recent annual report on file for the same company.
| section | kb_of_text |
|---|---|
| risk_factors | 66.5 |
| business | 15.7 |
The exact SQL behind every number
SELECT
section,
round(max(length(text)) / 1024, 1) AS kb_of_text
FROM global_markets.stocks_10k_sections
WHERE ticker = 'AAPL'
AND period_end = (
SELECT max(period_end)
FROM global_markets.stocks_10k_sections
WHERE ticker = 'AAPL'
)
GROUP BY section
ORDER BY kb_of_text DESC
LIMIT 15The heaviest of the 2 sections stored for that filing, risk_factors, runs 66.5 KB of text, against 15.7 KB for business at the bottom of the panel. Section coverage in this index is partial, so read the panel as a size comparison between the items it holds rather than a map of the whole document.
Ten requests per second
The SEC access page states a current maximum of 10 requests per second (checked October 2026) and reserves the right to lower it. A 0.15 second pause between calls keeps you under that with headroom, which is what matters when walking a list rather than fetching one file:
for T in AAPL MSFT NVDA KO JNJ; do
CIK=$(jq -r --arg t "$T" '.[] | select(.ticker == $t) | .cik_str' tickers.json)
curl -s -A "$UA" --compressed \
"https://data.sec.gov/submissions/CIK$(printf '%010d' "$CIK").json" \
| jq -r --arg t "$T" '[$t, .filings.recent.form[0], .filings.recent.filingDate[0]] | @tsv'
sleep 0.15
done
The same pacing habit applies to any free market endpoint, as our walkthrough of a free stock API with curl and jq shows.
When 10-K filings actually land
Annual reports bunch at the front of the calendar year, following the December fiscal year ends that most large filers keep. The panel counts 10-K filings by calendar month across several complete years.
| month | month_name | annual_reports |
|---|---|---|
| 1 | January | 510 |
| 2 | February | 9538 |
| 3 | March | 16333 |
| 4 | April | 2465 |
| 5 | May | 740 |
| 6 | June | 781 |
| 7 | July | 488 |
| 8 | August | 638 |
| 9 | September | 867 |
| 10 | October | 518 |
| 11 | November | 762 |
| 12 | December | 680 |
The exact SQL behind every number
SELECT
toMonth(filing_date) AS month,
any(monthName(filing_date)) AS month_name,
count() AS annual_reports
FROM global_markets.stocks_sec_edgar_index
WHERE form_type = '10-K'
AND filing_date >= '2021-01-01'
AND filing_date < toStartOfYear(today())
GROUP BY month
ORDER BY monthFebruary and March carry 9538 and 16333 filings in this sample, against 488 in July. A job that polls EDGAR daily will find most of its annual-report work inside those few weeks. Quarterly deadlines stack differently again, as the 13F filing deadlines show.
Finding language across filings
Full-text search is the fourth path and the one most people miss. It covers filings from 2001 onward, caps out at 10,000 hits per query, and pages ten results at a time by default:
curl -s -A "$UA" --compressed -G 'https://efts.sec.gov/LATEST/search-index' \
--data-urlencode 'q="supply chain disruption"' \
--data-urlencode 'forms=10-K' \
--data-urlencode 'startdt=2024-01-01' \
--data-urlencode 'enddt=2024-12-31' \
| jq -r '.hits.total.value,
(.hits.hits[] | [._source.file_date, ._source.display_names[0], ._id] | @tsv)'
The response is Elasticsearch-shaped: hits.total.value is the match count, and every hit carries _source.file_date, _source.display_names, _source.adsh and an _id built from the accession number and the file name joined by a colon, which is everything the Archives path needs. The /LATEST/ segment is case-sensitive. The SEC does not document this endpoint; it is what the search page calls, and the parameters can change without notice.
Counting a phrase across many filings is where that path pays off. The panel below tracks one phrase through annual reports over several years.
| year | companies_filing | companies_using_phrase |
|---|---|---|
| 2020 | 116 | 12 |
| 2021 | 203 | 29 |
| 2022 | 378 | 66 |
| 2023 | 389 | 134 |
| 2024 | 426 | 226 |
| 2025 | 572 | 359 |
The exact SQL behind every number
SELECT
toYear(period_end) AS year,
uniqExact(ticker) AS companies_filing,
uniqExactIf(ticker, positionCaseInsensitive(text, 'artificial intelligence') > 0) AS companies_using_phrase
FROM global_markets.stocks_10k_sections
WHERE period_end >= '2020-01-01'
AND period_end < toStartOfYear(today())
GROUP BY year
ORDER BY yearIn 2025, 359 of the 572 companies with an annual report in this sample used that phrase somewhere in the document, against 12 of 116 in 2020. Phrase frequency is a reading aid, nothing more: the same words appear in a risk factor and in a growth story.
Bulk options when you want history
Tens of thousands of small requests at ten per second take hours. Two nightly archives replace them. https://www.sec.gov/Archives/edgar/daily-index/bulkdata/submissions.zip holds the filing history of every filer, the same JSON the submissions endpoint serves one company at a time. https://www.sec.gov/Archives/edgar/daily-index/xbrl/companyfacts.zip holds every XBRL fact the SEC has extracted. Both are republished around 3:00 a.m. ET, and both run to gigabytes zipped. Against that, one company's submissions JSON is a couple of hundred kilobytes and a single 10-K document is a few hundred kilobytes.
There is a middle option. The quarterly index files under https://www.sec.gov/Archives/edgar/full-index/ list every accession number filed in a quarter, a few megabytes per quarter, which is enough to build a download queue without touching either multi-gigabyte archive.
Data notes for the panels on this page
The form-type and monthly panels count filings in an EDGAR index, keyed on filing date, and the trailing-year panel moves as new filings arrive. The section-size and phrase panels key on period_end, the fiscal period a filing covers, rather than the filing date, and both exclude the current incomplete year. Coverage of section text begins well after 2001 and is partial inside a given filing, so the section-size panel shows only the items this index stores for that report, and the phrase panel is a sample of filers rather than the whole market. Section labels come from the filing structure, and a company that merges two items into one heading shows up as one row.
FAQ
Do I need an API key to download SEC filings?
No. EDGAR is free and unauthenticated. The only requirement is a User-Agent header naming you with a contact email. A request without one returns 403.
Why does curl return 403 from sec.gov?
The fair access policy rejects undeclared automated tools, and the default curl agent string counts as one. Pass -A with a name and an email address and the same request succeeds.
How do I find a company's CIK from its ticker?
Fetch company_tickers.json and select the entry whose ticker matches, then read cik_str. Zero-pad it to ten digits for data.sec.gov endpoints and leave it unpadded in Archives paths.
How many EDGAR requests per second are allowed?
Ten, as stated on the SEC access page in October 2026. A 0.15 second sleep between calls stays inside that limit, and a bulk archive is the better tool once you want tens of thousands of filings.
Can I search the full text of 10-K filings?
Yes. EDGAR full-text search covers filings from 2001 onward and returns up to 10,000 hits per query. Quote a phrase for an exact match and pass forms=10-K to limit the form type.
Every panel above ships with the exact SQL underneath it, so you can see how each count was made. To run these questions over filings yourself, ask them in plain English on the Strasmore terminal.