Strasmore Research
Learn Matt ConnorBy Matt Connor · data as of October 16, 2026 · refreshed weekly

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

  1. https://www.sec.gov/files/company_tickers.json maps every listed ticker to a CIK, the Central Index Key that identifies a filer. One file, under a megabyte as of October 2026.
  2. https://data.sec.gov/submissions/CIK##########.json returns one company's filing history: the index of what it filed and when.
  3. https://www.sec.gov/Archives/edgar/data/<cik>/<accession>/ holds the documents themselves, the annual report and every exhibit submitted with it.
  4. https://efts.sec.gov/LATEST/search-index is 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.

QueryFilings on record by form type, trailing year
form_typefilingsfilers
434851557602
424B2191134906
1447192715438
8-K634666555
NPORT-P531212015
34442124909
D3986337593
13F-HR346919364
SCHEDULE 13G/A339976862
6-K274301341
FWP263371095
D/A2287619706
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 12
Run this yourself

Form 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.

QueryTicker to CIK, with the ten-digit padded form
tickerissuercikcik_zero_paddedfilings_on_record
AAPLApple Inc.000032019300003201932081
JNJJOHNSON & JOHNSON000020040600002004063103
KOCOCA COLA CO000002134400000213443119
MSFTMICROSOFT CORP000078901900007890194302
NVDANVIDIA CORP000104581000010458102332
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 ticker
Run this yourself

AAPL 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.

QuerySection sizes inside one AAPL annual report
sectionkb_of_text
risk_factors66.5
business15.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 15
Run this yourself

The 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.

Query10-K filings by calendar month, complete years since 2021
monthmonth_nameannual_reports
1January510
2February9538
3March16333
4April2465
5May740
6June781
7July488
8August638
9September867
10October518
11November762
12December680
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 month
Run this yourself

February 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.

QueryCompanies using the phrase 'artificial intelligence' in their annual report
yearcompanies_filingcompanies_using_phrase
202011612
202120329
202237866
2023389134
2024426226
2025572359
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 year
Run this yourself

In 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.