SEC EDGAR API in Python: pull company financials in a few lines (2026)
You want a company's revenue and net income in a Python script. Sounds like a one-liner. It usually isn't.
The data is free, the SEC publishes all of it, but the raw route hands you a 403 before you start, a CIK you have to look up, and XBRL tags that change name between companies. Plenty of people give up around the third gotcha.
So here's the Python version done two ways: the free SEC route with every trap called out, and the shortcut when the parsing stops being worth your time.
requests and call it. The free SEC route (data.sec.gov) needs a descriptive User-Agent header, a zero-padded CIK, and your own XBRL parsing. For clean statement JSON, one call to api.edgrapi.com/v1/fundamentals/AAPL with an X-API-Key header returns income statement, balance sheet, and cash flow as flat fields. The free tier is 100 credits a month, no card.Can you use the SEC EDGAR API from Python?
Yes, with nothing but the requests library. The SEC serves company data as JSON at data.sec.gov, no key required, so a Python script can pull filings and financial facts for free. The catch is that "free" means you also handle the parts the SEC leaves raw: the ticker-to-CIK lookup, the User-Agent rule, and the XBRL structure.
Start with the pieces that are genuinely easy. Resolving a ticker to its CIK is one file:
import requests
HEADERS = {"User-Agent": "Your Name you@example.com"}
tickers = requests.get(
"https://www.sec.gov/files/company_tickers.json", headers=HEADERS
).json()
cik = next(v["cik_str"] for v in tickers.values() if v["ticker"] == "AAPL")
print(cik) # 320193
That gets you a CIK. Getting clean financials out of it is the part that grows.
Why does the raw SEC route get painful in Python?
Because the numbers come back as XBRL, and XBRL doesn't agree with itself across companies. The same line item is tagged differently by different filers and changes across years, so revenue might be Revenues on one company and RevenueFromContractWithCustomerExcludingAssessedTax on the next. You end up maintaining a lookup of tag aliases per field, plus dedupe logic for restated periods.
Two of those gotchas will bite before you even reach the tags. The CIK has to be zero-padded to 10 digits in the data URLs (CIK0000320193, not 320193). And per the SEC's fair-access rules, a request without a descriptive User-Agent gets a 403 and a short IP block, and going over 10 requests a second gets you a 429.
None of it is hard on its own. It's just five small chores stacked on top of what you thought was one HTTP call.
How do you get clean financials in Python in a few lines?
You point requests at a hosted endpoint that has already done the CIK lookup, the User-Agent handling, and the tag normalization. With Edgrapi that's one GET to /v1/fundamentals/{ticker} with your key in the X-API-Key header, and the statements come back as flat named fields.
import requests
KEY = "edgr_your_key"
r = requests.get(
"https://api.edgrapi.com/v1/fundamentals/AAPL",
headers={"X-API-Key": KEY},
params={"period": "annual", "limit": 1},
)
data = r.json()
print(data["income_statement"]["revenue"]) # 416161000000
print(data["income_statement"]["net_income"]) # 112010000000
No CIK, no User-Agent dance, no tag map. You send a ticker and read named fields.
To be fair to the free route: if you're pulling one or two companies for a script you run once, the raw companyfacts JSON is fine, and we cover it in the why-it's-harder-than-it-looks piece. The hosted call earns its keep once you're doing this across many tickers on a schedule.
How do you pull ratios, filings, and 10-K text?
Same key, same requests.get, different path. The API has five endpoints, and they all follow one pattern, so once you've called fundamentals you've effectively called all of them. Ratios come pre-computed, filings come with a link to each document, and sections return 10-K narrative text.
import requests
KEY = {"X-API-Key": "edgr_your_key"}
BASE = "https://api.edgrapi.com/v1"
ratios = requests.get(f"{BASE}/ratios/AAPL", headers=KEY).json()
filings = requests.get(f"{BASE}/filings/AAPL", headers=KEY, params={"form": "10-K"}).json()
risk = requests.get(f"{BASE}/sections/AAPL", headers=KEY,
params={"form": "10-K", "item": "1A"}).json()
print(ratios["net_margin"], ratios["roe"])
print(filings["filings"][0]["url"]) # link to the latest 10-K
print(risk["text"][:200]) # Risk Factors, as text
Ratios are covered in the ratios guide, and the 10-K text in the sections guide. The point here is that Python-side, they're all the same three lines.
How do you load SEC financials into a pandas DataFrame?
You loop over your tickers, call the fundamentals endpoint for each, collect the results into a list of dicts, and hand it to pd.DataFrame. Because the response is flat named fields and not nested XBRL, it loads into a DataFrame with no reshaping, ready to screen or sort.
import requests, pandas as pd
KEY = {"X-API-Key": "edgr_your_key"}
def snapshot(ticker):
inc = requests.get(f"https://api.edgrapi.com/v1/fundamentals/{ticker}",
headers=KEY, params={"period": "annual", "limit": 1}
).json()["income_statement"]
return {"ticker": ticker, "revenue": inc["revenue"], "net_income": inc["net_income"]}
rows = [snapshot(t) for t in ["AAPL", "MSFT", "NVDA"]]
df = pd.DataFrame(rows).set_index("ticker")
print(df)
The API handles rate limits on its side, so the loop just makes calls without you sleeping between them. If you were looping against data.sec.gov directly, you'd add a delay to stay under the SEC's 10-per-second limit.
Start with one ticker in Python
Grab a free key, pip install requests, and paste the fundamentals snippet with a company you follow. You'll have real numbers in a Python variable before you've finished reading the SEC's User-Agent policy.
The free tier is 100 credits a month, no card, enough to pull a whole watchlist into a DataFrame. Point it at https://api.edgrapi.com, and if you want the full endpoint reference it's in the docs, with the rest of the API in the complete SEC EDGAR API guide. Wiring it into an AI agent instead? The agents guide covers the MCP server.
Frequently asked questions
How do I use the SEC EDGAR API in Python?
With the requests library. The SEC's own data lives at data.sec.gov and is free, but you send a descriptive User-Agent header, resolve the ticker to a zero-padded CIK, and parse XBRL yourself. For clean statement JSON, one GET to a hosted endpoint like api.edgrapi.com/v1/fundamentals/AAPL returns income statement, balance sheet, and cash flow.
Can I get SEC financial data in Python without parsing XBRL?
Yes. The XBRL parsing is the hard part of the raw SEC route, because filers tag the same line item differently. A hosted API does that normalization server-side, so a single requests.get returns the statements as flat named fields you can drop straight into pandas.
Why do I get a 403 from the SEC EDGAR API in Python?
Because you didn't send a User-Agent header, or you sent the default requests one. Since 2021 the SEC requires a descriptive User-Agent with your name and email, and rejects requests without it with a 403 plus a short IP block. Set headers={'User-Agent': 'Name you@email.com'} and stay under 10 requests a second.
How do I load SEC financials into a pandas DataFrame?
Loop over your tickers, call the fundamentals endpoint for each, collect the JSON into a list of dicts, and pass it to pd.DataFrame. Because the response is already flat named fields rather than nested XBRL, it loads into a DataFrame with no reshaping.
Is there a free SEC EDGAR API for Python?
The SEC's data.sec.gov endpoints are free with no key. Edgrapi's free tier is 100 credits a month, no card, which wraps that data as clean JSON so you skip the CIK lookup, the User-Agent handling, and the XBRL parsing. Use the raw SEC route for one-off scripts and the hosted one when the parsing starts costing you time.
How do I avoid getting rate-limited by SEC EDGAR?
The SEC allows up to 10 requests per second per IP; over that you get a 429 and a temporary block. If you loop over many tickers against data.sec.gov, add a small delay between calls. A hosted API handles the rate limiting on its side, so your loop just makes calls.