DEV Community

Cover image for Pull any public company's SEC financials in 3 lines of Python (with source filing URL)
Hudson Enterprises
Hudson Enterprises

Posted on

Pull any public company's SEC financials in 3 lines of Python (with source filing URL)

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"
Enter fullscreen mode Exit fullscreen mode

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 filingrail
from 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 requests so 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
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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']}")
Enter fullscreen mode Exit fullscreen mode

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'))}")
Enter fullscreen mode Exit fullscreen mode

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'))}")
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)