SEC EDGAR API in Python: free financial data for your AI agent
Updated on
Educational only, not investment advice.
This guide shows how to use the SEC EDGAR API from Python to pull filings and XBRL financials, and how to hand that data to an AI research agent with the source attached.
Before you start
You need Python 3 and the requests library (pip install requests).
No account and no API key. The data.sec.gov APIs "do not require any authentication or API keys" (EDGAR Application Programming Interfaces (APIs)).
Use a User-Agent string with your name and email. SEC asks automated tools to declare a user agent. Its sample is a company name plus an admin contact email (Accessing EDGAR Data).
Cost: SEC says anyone can access and download EDGAR information for free (Accessing EDGAR Data).
The endpoints
| Endpoint | What it returns |
|---|---|
https://data.sec.gov/submissions/CIK##########.json |
A company's filing history and metadata (name, former names, exchanges and tickers). The recent array holds at least one year or 1,000 filings, whichever is more, with older filings in extra files. |
https://data.sec.gov/api/xbrl/companyconcept/CIK##########/us-gaap/AccountsPayableCurrent.json |
Every XBRL disclosure of one concept for one company, grouped by unit. |
https://data.sec.gov/api/xbrl/companyfacts/CIK##########.json |
All concepts for one company in one call. |
https://data.sec.gov/api/xbrl/frames/us-gaap/AccountsPayableCurrent/USD/CY2019Q1I.json |
One fact per company for one concept and calendar period (CY#### annual, CY####Q# quarterly, CY####Q#I instantaneous). |
https://www.sec.gov/files/company_tickers.json |
Ticker, CIK and company name associations. SEC updates this file periodically and does not guarantee its accuracy or scope (open the file; Accessing EDGAR Data). |
Described on the EDGAR Application Programming Interfaces (APIs) page. ########## is the 10-digit CIK including leading zeros. The JSON updates in real time as filings are disseminated, and nightly bulk ZIP files (companyfacts.zip, submissions.zip) exist for large downloads (EDGAR Application Programming Interfaces (APIs)). data.sec.gov does not support CORS, so call it from a script or server, not from browser JavaScript (EDGAR Application Programming Interfaces (APIs)).
Step by step
Each step is one part of the script. The full script and a run from 2026-10-20 follow.
1. Set a User-Agent and a polite request helper
Every automated request should declare who you are. Set a User-Agent with your name and email, then pause after each call. The helper waits 0.2 seconds, which stays under the fair-access guideline of no more than 10 requests per second.
import time
import requests
# SEC asks every automated request to declare who you are.
# Replace with your own name and email before running.
HEADERS = {"User-Agent": "Your Name your-email@example.com"}
def get_json(url):
"""GET a SEC JSON file politely: declared User-Agent, short pause."""
r = requests.get(url, headers=HEADERS, timeout=30)
r.raise_for_status()
time.sleep(0.2) # stays well under SEC's 10 requests/second limit
return r.json()
2. Map a ticker to a zero-padded CIK
company_tickers.json is an object keyed "0", "1", and so on, with fields cik_str, ticker, and title. On 2026-10-20 it had about 10,000 entries. Find the row for the ticker and zero-pad cik_str to 10 digits.
# 1. Ticker -> CIK (zero-padded to 10 digits)
tickers = get_json("https://www.sec.gov/files/company_tickers.json")
row = next(v for v in tickers.values() if v["ticker"] == "AAPL")
cik = str(row["cik_str"]).zfill(10)
print("1.", row["title"], "CIK", cik)
3. Read the filing history and find the latest 10-K
Top-level submissions keys include cik, name, tickers, exchanges, sic, sicDescription, fiscalYearEnd, formerNames, and filings. filings.recent is columnar: parallel arrays such as accessionNumber, filingDate, reportDate, form, primaryDocument, and isXBRL. filings.files lists older filing batches. The document URL pattern is https://www.sec.gov/Archives/edgar/data/<CIK without leading zeros>/<accession number without dashes>/<primaryDocument>.
# 2. Filing history: latest 10-K filings
subs = get_json(f"https://data.sec.gov/submissions/CIK{cik}.json")
recent = subs["filings"]["recent"]
print("2.", subs["name"], "| recent filings:", len(recent["form"]))
for i, form in enumerate(recent["form"]):
if form == "10-K":
acc = recent["accessionNumber"][i]
doc = recent["primaryDocument"][i]
url = (f"https://www.sec.gov/Archives/edgar/data/{int(cik)}/"
f"{acc.replace('-', '')}/{doc}")
print(" ", form, recent["filingDate"][i], acc, url)
break
4. Pull one concept with companyconcept
companyconcept keys are cik, taxonomy, tag, label, description, entityName, and units. Facts are grouped by unit. Each fact has accn, end, filed, form, fp, fy, and val. frame is present only on some facts: 69 of 142 in this example.
# 3. One concept for one company
concept = get_json(
f"https://data.sec.gov/api/xbrl/companyconcept/CIK{cik}/us-gaap/AccountsPayableCurrent.json")
usd = concept["units"]["USD"]
print("3.", concept["tag"], "| units:", list(concept["units"]), "| facts:", len(usd))
print(" last fact keys:", sorted(usd[-1].keys()))
5. Pull every concept with companyfacts
companyfacts returns all concepts for one company in one call. On 2026-10-20 the facts object listed taxonomies dei and us-gaap, and us-gaap held 503 concepts.
# 4. All concepts for one company
facts = get_json(f"https://data.sec.gov/api/xbrl/companyfacts/CIK{cik}.json")
print("4. taxonomies:", list(facts["facts"]),
"| us-gaap concepts:", len(facts["facts"]["us-gaap"]))
6. Compare one concept across companies with frames
frames keys include ccp (the period) and data. Each row has accn, cik, end, entityName, loc, and val. CY2019Q1I is the instantaneous frame for the first quarter of calendar 2019.
# 5. One concept, one period, every company that reported it
frame = get_json(
"https://data.sec.gov/api/xbrl/frames/us-gaap/AccountsPayableCurrent/USD/CY2019Q1I.json")
print("5.", frame["ccp"], "| companies:", len(frame["data"]))
print(" row keys:", sorted(frame["data"][0].keys()))
Full script
The comments inside the script number the five data calls. Step 1 above is the helper, which the script does not number. Replace the placeholder User-Agent before you run it.
import time
import requests
# SEC asks every automated request to declare who you are.
# Replace with your own name and email before running.
HEADERS = {"User-Agent": "Your Name your-email@example.com"}
def get_json(url):
"""GET a SEC JSON file politely: declared User-Agent, short pause."""
r = requests.get(url, headers=HEADERS, timeout=30)
r.raise_for_status()
time.sleep(0.2) # stays well under SEC's 10 requests/second limit
return r.json()
# 1. Ticker -> CIK (zero-padded to 10 digits)
tickers = get_json("https://www.sec.gov/files/company_tickers.json")
row = next(v for v in tickers.values() if v["ticker"] == "AAPL")
cik = str(row["cik_str"]).zfill(10)
print("1.", row["title"], "CIK", cik)
# 2. Filing history: latest 10-K filings
subs = get_json(f"https://data.sec.gov/submissions/CIK{cik}.json")
recent = subs["filings"]["recent"]
print("2.", subs["name"], "| recent filings:", len(recent["form"]))
for i, form in enumerate(recent["form"]):
if form == "10-K":
acc = recent["accessionNumber"][i]
doc = recent["primaryDocument"][i]
url = (f"https://www.sec.gov/Archives/edgar/data/{int(cik)}/"
f"{acc.replace('-', '')}/{doc}")
print(" ", form, recent["filingDate"][i], acc, url)
break
# 3. One concept for one company
concept = get_json(
f"https://data.sec.gov/api/xbrl/companyconcept/CIK{cik}/us-gaap/AccountsPayableCurrent.json")
usd = concept["units"]["USD"]
print("3.", concept["tag"], "| units:", list(concept["units"]), "| facts:", len(usd))
print(" last fact keys:", sorted(usd[-1].keys()))
# 4. All concepts for one company
facts = get_json(f"https://data.sec.gov/api/xbrl/companyfacts/CIK{cik}.json")
print("4. taxonomies:", list(facts["facts"]),
"| us-gaap concepts:", len(facts["facts"]["us-gaap"]))
# 5. One concept, one period, every company that reported it
frame = get_json(
"https://data.sec.gov/api/xbrl/frames/us-gaap/AccountsPayableCurrent/USD/CY2019Q1I.json")
print("5.", frame["ccp"], "| companies:", len(frame["data"]))
print(" row keys:", sorted(frame["data"][0].keys()))
Output from a run on 2026-10-20
1. Apple Inc. CIK 0000320193
2. Apple Inc. | recent filings: 1001
10-K 2025-10-31 0000320193-25-000079 https://www.sec.gov/Archives/edgar/data/320193/000032019325000079/aapl-20250927.htm
3. AccountsPayableCurrent | units: ['USD'] | facts: 142
last fact keys: ['accn', 'end', 'filed', 'form', 'fp', 'frame', 'fy', 'val']
4. taxonomies: ['dei', 'us-gaap'] | us-gaap concepts: 503
5. CY2019Q1I | companies: 3390
row keys: ['accn', 'cik', 'end', 'entityName', 'loc', 'val']
We ran this script against the live API on 2026-10-20 with our own User-Agent. Counts and the latest filing change as new filings arrive. Apple Inc. (CIK 0000320193) is used only as a well-known example filer; nothing here is a view on the company.
Feeding it to an AI research agent
Give the agent the SEC JSON, or a trimmed extract of it, not a summary from memory. Keep the source URL and the accession number with every number so each figure can be traced.
Ask the agent to quote the tag, the period (end date, fy and fp), and the form for every figure, and to say "not found" rather than estimate. If the agent fetches data itself, give it the User-Agent and the rate-limit rules. Then check a few figures against the filing document.
See how we plan to grade an AI analyst's calls and how we plan to compare two agents on the same research job. The rules for those jobs are on how we test.
Troubleshooting
403 Forbidden
Requests with no User-Agent, or with the default python-requests one, were refused with 403 in our run on 2026-10-20. The same request with a declared User-Agent returned 200. Set a User-Agent with your name and email (Accessing EDGAR Data).
Rate limits
SEC's fair-access guideline is no more than 10 requests per second per user, regardless of how many machines you use. SEC may block IP addresses that send excessive requests (Developer Resources, Accessing EDGAR Data). The script pauses 0.2 seconds between calls. Download only what you need. For bulk work, use the nightly ZIP files (EDGAR Application Programming Interfaces (APIs)).
404 on a CIK that exists
The CIK must be 10 digits with leading zeros. CIK320193 returned 404, and CIK0000320193 returned 200 in our run. Use str(cik).zfill(10). For Archives URLs, use the CIK without leading zeros.
404 or missing data for a concept
XBRL tags differ between companies and over time. A tag the company never used returns 404 from companyconcept. Example from the 2026-10-20 run: this filer has facts under Revenues (11), RevenueFromContractWithCustomerExcludingAssessedTax (117) and SalesRevenueNet (210), so a single tag can miss years. Check companyfacts for the tags a company actually uses. The XBRL APIs only include non-custom taxonomies (us-gaap, ifrs-full, dei, srt) and facts that apply to the whole entity (EDGAR Application Programming Interfaces (APIs)).
Frames don't line up with the fiscal year
Frames align facts to calendar periods, and companies' reporting start and end dates differ (EDGAR Application Programming Interfaces (APIs)).
Browser fetch fails
There is no CORS on data.sec.gov, so a fetch from browser JavaScript fails (EDGAR Application Programming Interfaces (APIs)).
FAQ
Is the SEC EDGAR API free?
Yes. SEC says anyone can access and download EDGAR information for free, and the data.sec.gov APIs do not require authentication or an API key.
What is the SEC EDGAR API rate limit?
SEC's current fair-access guideline is no more than 10 requests per second per user, regardless of the number of machines.
Why does the SEC API return 403?
Usually because the request has no declared User-Agent. SEC asks automated tools to send one with a name and contact email.
Related on bests.one
- Can AI pick stocks? How we grade an AI analyst
- Grok Bot vs Claude Code
- How we test
- Grok Bot templates for research and data
- How Grok Bot routines run
Sources: EDGAR Application Programming Interfaces (APIs), Developer Resources, Accessing EDGAR Data. Checked on 2026-10-20.