Downloading a company’s annual report manually is straightforward. Repeating that process across companies and reporting periods requires a more reliable workflow.
If you are learning how to download files from SEC EDGAR, start with three actions: identify the company, find the filing’s metadata, and request the actual document from its archive location.
This guide connects those actions in a Python example that finds Apple’s most recent 10-K in the SEC’s recent filing history, downloads its primary HTML document, and saves identifying information alongside it. You can adapt the same workflow to download 10-K and 10-Q filings.
How SEC EDGAR Filing Downloads Work
EDGAR is the SEC’s Electronic Data Gathering, Analysis, and Retrieval system. Its public data resources support filing discovery and document retrieval, but those are separate operations.
Filing metadata tells you where a document is; downloading that metadata does not download the filing itself.
Four identifiers connect the workflow:
- Ticker: A market symbol, such as AAPL, useful for finding a company.
- CIK: The Central Index Key identifying an EDGAR filer. Apple’s CIK is 320193.
- Accession number: The identifier for an accepted submission. Preserve its leading zeros.
- Primary document filename: The filename of the main report within that submission.
Use the company’s CIK for its archive directory. Do not infer that directory from the accession number’s first ten digits, which can identify a filing agent. The SEC explains these relationships in its EDGAR archive documentation.
A submission can contain the primary report, exhibits, images, and supporting files. Downloading the primary document alone does not collect every attachment.
What You Need Before You Start
Use Python 3 and the requests library. Install the dependency:
python -m pip install requests
This installs the HTTP client used by the script below.
The public SEC submissions API requires no API key. However, automated requests should declare an organization or application and a real contact email in the User-Agent header.
Set that identification before running the script. Replace the example with your own details.
For macOS or Linux:
export SEC_USER_AGENT=”Your Organization [email protected]”
For PowerShell on Windows:
$env:SEC_USER_AGENT = “Your Organization [email protected]”
The script reads this environment variable and stops if it is missing.
The SEC currently limits each user to no more than ten requests per second across machines. The example runs sequentially and pauses before each request. Multiple workers need a shared rate limiter. See the SEC’s Developer Resources for its fair-access guidance.
How to Download Files from SEC EDGAR: Step by Step
Resolve the ticker to a CIK
The script requests the SEC’s company ticker mapping at:
https://www.sec.gov/files/company_tickers.json
It finds the entry whose ticker matches AAPL and reads its cik_str value.
Ticker mappings are a starting point for identification. Confirm the company name before using the downloaded report in research; the SEC does not guarantee the mapping’s accuracy or completeness. Its Webmaster FAQ describes the mapping files.
Find a matching filing
Next, the script requests:
https://data.sec.gov/submissions/CIK0000320193.json
The submissions endpoint uses a ten-digit, zero-padded CIK. Its filings.recent object contains parallel arrays: values at the same position describe the same filing.
The script uses form, filingDate, accessionNumber, and primaryDocument to select a 10-K and locate its report. It chooses the most recently filed matching entry, rather than relying on array order.
This searches recent history, not every historical filing. For older records, inspect filings.files, which lists additional submissions JSON files. The endpoint and history structure are documented in the SEC’s EDGAR API guide.
Build the source-document URL
For modern filings, the document location follows this pattern:
https://www.sec.gov/Archives/edgar/data/{CIK}/{ACCESSION_WITHOUT_DASHES}/{PRIMARY_DOCUMENT}
The archive CIK is unpadded. Remove dashes from the accession number only for the directory component, and retain the primary filename returned by the metadata.
For example, Apple’s 2025 annual report is available as an HTML source document. The script discovers its own matching filing, so its selected report can change as new filings become available.
Download, save, and verify the document
Save the following complete example as download_filing.py. It includes request timeouts, status checking, bounded retries, safe local paths, and basic HTML validation.
import json
import os
import re
import time
from pathlib import Path
from urllib.parse import quote
import requests
TICKER = "AAPL"
FORM = "10-K"
user_agent = os.environ.get("SEC_USER_AGENT", "").strip()
if not user_agent:
raise SystemExit(
"Set SEC_USER_AGENT to your organization and real contact email."
)
session = requests.Session()
session.headers.update({
"User-Agent": user_agent,
"Accept-Encoding": "gzip, deflate",
})
def get(url):
"""Make a paced GET request, with at most four attempts."""
for attempt in range(4):
time.sleep(0.5)
try:
response = session.get(url, timeout=(10, 60))
except (requests.Timeout, requests.ConnectionError):
if attempt == 3:
raise
time.sleep(2 ** attempt)
continue
retryable = response.status_code in (429, 500, 502, 503, 504)
if retryable and attempt < 3:
retry_after = response.headers.get("Retry-After", "")
response.close()
if retry_after.isdigit():
delay = int(retry_after)
if delay > 60:
raise RuntimeError(
"Retry-After exceeds 60 seconds; stop and reschedule."
)
elif retry_after:
raise RuntimeError(
"Server requested a dated retry; stop and reschedule."
)
else:
delay = 2 ** attempt
time.sleep(delay)
continue
response.raise_for_status()
return response
raise RuntimeError("Request attempts exhausted.")
# Resolve the ticker.
companies = get(
"https://www.sec.gov/files/company_tickers.json"
).json()
company = next(
(
item for item in companies.values()
if item["ticker"].upper() == TICKER
),
None,
)
if company is None:
raise RuntimeError(f"Ticker not found: {TICKER}")
cik = int(company["cik_str"])
# Retrieve filing metadata.
submissions = get(
f"https://data.sec.gov/submissions/CIK{cik:010d}.json"
).json()
recent = submissions["filings"]["recent"]
matches = [
index for index, form in enumerate(recent["form"])
if form == FORM
]
if not matches:
raise RuntimeError(
"No matching recent filing; inspect filings.files for older history."
)
index = max(
matches,
key=lambda n: (
recent["filingDate"][n],
recent["accessionNumber"][n],
),
)
accession = recent["accessionNumber"][index]
document = recent["primaryDocument"][index]
# Validate metadata before using it in URLs and paths.
if not re.fullmatch(r"\d{10}-\d{2}-\d{6}", accession):
raise RuntimeError("Unexpected accession number.")
if (
not document
or document != document.split("/")[-1]
or "\\" in document
):
raise RuntimeError("Unexpected primary-document filename.")
if Path(document).suffix.lower() not in (".htm", ".html"):
raise RuntimeError("This example expects an HTML primary document.")
url = (
f"https://www.sec.gov/Archives/edgar/data/{cik}/"
f"{accession.replace('-', '')}/"
f"{quote(document, safe='')}"
)
# Download and perform basic response checks.
response = get(url)
content = response.content
if not content or b"<html" not in content[:8192].lower():
raise RuntimeError(
"Response does not look like the expected HTML document."
)
if (
b"undeclared automated tool" in content.lower()
or b"request rate threshold exceeded" in content.lower()
):
raise RuntimeError(
"SEC access restriction returned instead of a filing."
)
# Save through a temporary file.
folder = Path("filings") / f"{cik:010d}" / accession
folder.mkdir(parents=True, exist_ok=True)
safe_name = re.sub(r"[^A-Za-z0-9._-]", "_", document)
path = folder / safe_name
temporary = path.with_suffix(path.suffix + ".part")
temporary.write_bytes(content)
temporary.replace(path)
if path.stat().st_size != len(content):
raise RuntimeError(
"Saved file size differs from downloaded content."
)
# Retain identifying metadata beside the report.
metadata = {
"ticker": TICKER,
"company": submissions["name"],
"cik": f"{cik:010d}",
"form": FORM,
"filingDate": recent["filingDate"][index],
"accessionNumber": accession,
"primaryDocument": document,
"sourceUrl": url,
"bytes": len(content),
}
(folder / "metadata.json").write_text(
json.dumps(metadata, indent=2),
encoding="utf-8",
)
print(f"Company: {metadata['company']} (CIK {cik:010d})")
print(
f"Filing: {FORM}, filed {metadata['filingDate']}, "
f"accession {accession}"
)
print(f"Source: {url}")
print(f"Saved: {path.resolve()} ({path.stat().st_size:,} bytes)")Run the saved script:
python download_filing.pyOn success, it prints the company, filing date, accession number, source URL, saved location, and byte count. Those values come from the live response; they are not fixed sample output.
The report is saved under filings/{CIK}/{accession}/, with metadata.json beside it. Open the HTML file and check its company name, form, and reporting period. The automated checks confirm a nonempty HTML response and matching saved byte count, but they do not fully validate the report’s contents.
To retrieve a quarterly report, change FORM to “10-Q”. The exact comparison excludes amendments such as 10-Q/A; include those separately when your workflow requires them.
Using SEC Filing Data’s API
Direct SEC access fits workflows that can manage company lookup, filing selection, document retrieval, and access policies themselves. A provider may be useful when you want filing search and document processing within one integration.
The SEC Filings API service page introduces SEC Filing Data’s offering.
For discovery, its SEC Query API documents POST /v1/filings/query, an API token supplied directly in the Authorization header, and JSON requests using query_type and query_value. Documented pagination controls include from and size, with a maximum size of 100.
Its SEC Filing Download and PDF Generator API documentation describes original-format retrieval and PDF generation. However, the published page lists conflicting HTTP methods, while its Python example demonstrates a PDF-generation request. Confirm the applicable download contract before implementing a product-specific source-file downloader.
The main example therefore uses direct SEC requests. SEC Filing Data is a third-party provider; its credentials, endpoints, and service limits are separate from the SEC’s public resources.
Choosing the Right Filing Document and Format
Choose the file that answers your question before selecting a parser or converting its format.
| Resource | What it contains | How to use it |
| Submissions JSON | Filing identifiers, dates, forms, and filenames | Locate reports; it is not the report itself |
| Primary HTML document | The main report; it may contain inline XBRL | Read or parse the filing downloaded by this example |
| Exhibits and supporting files | Separate contracts, attachments, images, or data files | Inspect the filing’s document list and retrieve the required files |
| Complete submission .txt | Disseminated submission content with headers and document sections | Use a submission-aware parser rather than treating it as clean prose |
| XBRL financial-data JSON | Extracted financial facts | Analyze structured values alongside the source report |
The SEC describes complete submission files and component-document directories in its archive access guidance. Its API documentation explains extracted XBRL data.
Saving an HTML report does not automatically save its linked images or exhibits. A fully self-contained archive requires collecting those dependencies separately.
Do not assume every filing has a native PDF version. This example preserves the HTML source. A generated PDF is a conversion output, and renaming an .htm file to .pdf does not convert it.
Common Download Errors and How to Handle Them
Access denied or undeclared automated tool
Check that SEC_USER_AGENT contains accurate identification, stop repeated requests, and review your overall traffic. The script raises an error for HTTP 403 rather than repeatedly retrying it.
If access remains blocked, follow the SEC’s access-denied guidance. Do not rotate addresses or identities to evade restrictions.
Rate limiting or temporary server failures
The script retries HTTP 429 and selected server errors with increasing delays, up to four attempts. It honors short numeric Retry-After values and stops when the server requests a longer or dated retry.
Persistent failures should trigger a later scheduled run. Adding parallel requests can make the problem worse.
Missing filing or HTTP 404
Check the company CIK, accession directory, and exact primary filename. Preserve leading zeros in accession numbers.
If the desired report is absent from recent history, inspect the additional files listed under filings.files. The SEC history endpoint does not use the provider’s from and size pagination controls.
Browser requests fail although Python requests work
The SEC states that data.sec.gov does not support CORS, the browser mechanism controlling cross-origin requests. Use a backend workflow such as this Python script for retrieval. See the SEC API documentation.
Making Downloads Repeatable
A repeatable download process needs a stable identity for each saved document. Use the company CIK, accession number, and original document filename together; a ticker alone is insufficient.
For recurring jobs:
- Cache company mappings instead of fetching them for every document.
- Skip verified files already recorded in your download manifest.
- Retain source URLs, retrieval timestamps, byte counts, and checksums.
- Log failures and reschedule them within your access policy.
- Track amendments explicitly when maintaining a reporting history.
The example supplies the folder structure and metadata sidecar. Duplicate prevention and checksums are useful next steps for a production workflow.
Conclusion
You can now identify a company, select a filing, construct its archive URL, and save the primary document with supporting metadata. Keep the original format and verify the saved report before building analysis around it.
When evaluating a provider-based workflow, the SEC Filing Data API site is the next place to review available services and their documentation.
