Every number a company reports to the SEC is public. The bottleneck isn't access — EDGAR is free and open. The bottleneck is the parser, the XBRL normalizer, the rate-limit governor, and the deduplication layer you have to write before you get to the one number you actually wanted.
This tutorial skips all of that. By the end you'll have working Python that returns Apple's income statement, balance sheet, and cash flow from a live 10-Q — plus the exact sec.gov URL for every number, so you can verify any figure at its source.
Everything here runs against Filingrail, a REST API on RapidAPI that normalizes SEC EDGAR filings into clean JSON. Free tier: 50 calls/day, no credit card. I built it; the tutorial is honest about what it can and can't do.
What you need
- Python 3.8+ with
requests(pip install requests) - A free RapidAPI account → subscribe to Filingrail on the Free tier → copy your
X-RapidAPI-Key
export RAPIDAPI_KEY="your_key_here"
That's it. No EDGAR User-Agent configuration, no local XBRL parser, no rate-limit governor to wire up.
Prefer a typed client over raw HTTP? There's an official Python SDK:
pip install filingrailfrom filingrail import Filingrail client = Filingrail(api_key="your_rapidapi_key") print(client.financials("AAPL").meta.source_filing_url)Sync and async clients, typed dataclasses, all seven endpoints. PyPI page has the full reference. The rest of this tutorial uses plain
requestsso the HTTP shape stays visible — but the SDK is the shorter path for production code.
Step 1 — Resolve a ticker to its SEC CIK
The SEC's Central Index Key (CIK) is the canonical identifier for every registered issuer. Filingrail's search endpoint handles the ticker-to-CIK lookup, so you don't maintain a mapping file.
curl --silent \
"https://filingrail.p.rapidapi.com/v1/search/companies?q=apple&limit=3" \
-H "X-RapidAPI-Key: $RAPIDAPI_KEY" \
-H "X-RapidAPI-Host: filingrail.p.rapidapi.com" \
| python -m json.tool
Response (abbreviated):
{
"data": [
{
"cik": 320193,
"ticker": "AAPL",
"name": "Apple Inc.",
"sic_code": null,
"sic_description": null,
"exchange": null,
"state_of_inc": null
}
],
"meta": {
"ticker": null,
"cik": null,
"as_of": "2026-06-03T11:20:39",
"source_filing_url": null,
"source_filing_date": null
}
}
A note on the nulls: sic_code, exchange, and state_of_inc are sparsely populated in the index — the search response's meta.source_filing_url is also null because the company record (not a filing) is the source. You'll see that field populated on every financial, insider-trade, and 8-K response.
8,005 SEC-registered issuers are indexed. Search accepts a ticker symbol, a CIK number, or a name fragment. Postgres trigrams handle fuzzy matching — "appel" finds Apple.
Step 2 — Pull the financials
import os
import requests
RAPIDAPI_KEY = os.environ["RAPIDAPI_KEY"]
BASE_URL = "https://filingrail.p.rapidapi.com"
HEADERS = {
"X-RapidAPI-Key": RAPIDAPI_KEY,
"X-RapidAPI-Host": "filingrail.p.rapidapi.com",
}
resp = requests.get(
f"{BASE_URL}/v1/companies/AAPL/financials",
headers=HEADERS,
timeout=15,
)
resp.raise_for_status()
body = resp.json()
meta = body["meta"]
print(f"Source filing: {meta['source_filing_url']}")
print(f"Filed: {meta['source_filing_date']}")
The meta.source_filing_url field is the one worth understanding. It's the direct URL to the SEC filing that produced the data in this response — not a database reference, a live sec.gov link. Every response carries it. If you're building anything where a user might ask "where does this number come from," you have the answer in the response envelope.
Step 3 — Print the headline numbers
def fmt_usd(n):
if n is None:
return "—"
if abs(n) >= 1_000_000_000:
return f"${n / 1_000_000_000:,.2f}B"
if abs(n) >= 1_000_000:
return f"${n / 1_000_000:,.2f}M"
return f"${n:,.0f}"
for statement in body["data"]:
stype = statement["statement_type"]
period = statement["period_end"]
items = statement["line_items"]
print(f"\n--- {stype} ({period}) ---")
if stype == "income_statement":
print(f" Revenue: {fmt_usd(items.get('revenue'))}")
print(f" Operating income: {fmt_usd(items.get('operating_income'))}")
print(f" Net income: {fmt_usd(items.get('net_income'))}")
print(f" Diluted EPS: {items.get('eps_diluted', '—')}")
elif stype == "balance_sheet":
print(f" Total assets: {fmt_usd(items.get('total_assets'))}")
print(f" Cash + equiv.: {fmt_usd(items.get('cash_and_equivalents'))}")
print(f" Long-term debt: {fmt_usd(items.get('long_term_debt'))}")
elif stype == "cash_flow":
print(f" Operating CF: {fmt_usd(items.get('operating_cash_flow'))}")
print(f" Capex: {fmt_usd(items.get('capex'))}")
Three statements, headline fields, source filing URL. Swap AAPL for MSFT, NVDA, BRK-B, or any of the 8,005 tickers in the index.
What the response shape looks like
/v1/companies/{ticker}/financials returns the most recent 10-K or 10-Q filing. The data is backed by 1.85M+ rows of XBRL-normalized financial data going back to 2006.
The ~30 canonical fields per statement are normalized across the multi-tag drift that makes raw XBRL painful. The same economic concept — revenue, for instance — gets tagged as us-gaap:Revenues, us-gaap:SalesRevenueNet, or us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax depending on the filer and the year. Filingrail resolves that before it reaches your response.
Getting historical quarters
/financials/history returns a time series. Up to 20 periods per call (roughly 5 years quarterly):
resp = requests.get(
f"{BASE_URL}/v1/companies/AAPL/financials/history",
params={"period": "Q", "limit": 8},
headers=HEADERS,
timeout=15,
)
history = resp.json()
for entry in history["data"]:
if entry["statement_type"] != "income_statement":
continue
items = entry["line_items"]
print(f"{entry['period_end']} revenue={fmt_usd(items.get('revenue'))}")
Monitoring the filings stream
If you want to watch what a company files — rather than pull normalized data — use /v1/filings/recent. 237,000+ filings indexed, filterable by CIK, form type, or date range:
# Most recent filings for Apple, 10-Ks only
curl "https://filingrail.p.rapidapi.com/v1/filings/recent?cik=320193&form_type=10-K&limit=5" \
-H "X-RapidAPI-Key: $RAPIDAPI_KEY" \
-H "X-RapidAPI-Host: filingrail.p.rapidapi.com"
Each record includes the filing date, accession number, and a filing_url pointing directly to the document on sec.gov. The same source-traceability, just at the filing level rather than the data level.
What this is not
Honest caveats, because senior developers will ask:
- Not real-time. New filings appear within ~24 hours of EDGAR acceptance. Sub-minute polling would violate EDGAR's fair-access policy.
- Not Bloomberg. No intraday prices, no options chains, no non-US issuers, no analyst estimates.
- Not a research platform. No AI summaries, no commentary. Structured data from SEC filings, traced back to source.
- Not a replacement for EdgarTools if you're already running your own pipeline. EdgarTools is excellent and free. Filingrail is the option for developers who'd rather pay $9/month than maintain the plumbing.
The other endpoints
Beyond financials, v1.0 includes:
| Endpoint | Data | Volume |
|---|---|---|
/v1/companies/{ticker}/insider-trades |
Form 4 transactions | 63,000+ rows, daily refresh |
/v1/companies/{ticker}/8k-events |
Material events with SEC item codes (1.01, 2.01, 5.02, etc.) | 7,500+ rows, daily refresh |
/v1/institutions/{cik}/13f-holdings |
Form 13F positions in whole-dollar USD | 222,019 holdings across 66 managers incl. Berkshire Hathaway |
meta.source_filing_url is present on every response across all endpoints.
Pricing
Free tier: 50 calls/day, no credit card. Subscribe on the RapidAPI listing and get your key within seconds.
| Tier | Price | Calls/month |
|---|---|---|
| Free | $0 | 1,500 (50/day) |
| Pro | $9/mo | 5,000 |
| Ultra | $49/mo | 50,000 |
| Mega | $199/mo | 500,000 |
RapidAPI handles auth — every call needs X-RapidAPI-Key and X-RapidAPI-Host: filingrail.p.rapidapi.com. That's the full auth story.
Questions or issues
support@hudsonenterprisesllc.com — same-business-day response. Include the endpoint, parameters, and error response if applicable.
Built by Hudson Enterprises LLC, an Indiana software studio.
Top comments (0)