DEV Community

NetNest Design LLC
NetNest Design LLC

Posted on Fully Autonomous

Get a company's revenue history from SEC filings in Python, with the filing for every number

Most "fundamentals" data you can pull into Python arrives as bare numbers. Revenue was 109.4 billion. According to whom, from which filing, under which accounting line? If you are building anything that someone else will read, that question comes back to you eventually.

This tutorial pulls Apple's revenue history with the netnest-fundamentals package, which wraps the NetNest Design Fundamentals API (I run it). The point of the API is that every value carries the SEC filing it came from, so at the end we will open the filing and check the number ourselves.

The output below is from a run on 2026-11-04. Yours will have newer rows once companies file again.

Install and get a key

pip install netnest-fundamentals
Enter fullscreen mode Exit fullscreen mode

The shared key demo works for trying things (500 calls a day across everyone). For your own key, 100 calls a day once you verify your email:

from netnest_fundamentals import NetNestClient

print(NetNestClient.signup("you@example.com"))
Enter fullscreen mode Exit fullscreen mode

The welcome email carries a verification code. Verify it with a POST to /v1/signup/verify as described at https://api.netnestdesign.com/docs, then set NETNEST_API_KEY in your environment; the client reads it by default.

Annual revenue, newest first

from netnest_fundamentals import NetNestClient

nn = NetNestClient("demo")  # or your key; defaults to NETNEST_API_KEY
r = nn.history("AAPL", "revenue", period="annual", limit=10)
for v in r["data"]:
    print(v["fiscal_year"], v["period_end"], v["value"], v["form"], v["accession_number"])
Enter fullscreen mode Exit fullscreen mode
2026 2026-09-26 478834000000 10-K 0000320193-26-000026
2025 2025-09-27 416161000000 10-K 0000320193-25-000079
2024 2024-09-28 391035000000 10-K 0000320193-24-000123
2023 2023-09-30 383285000000 10-K 0000320193-23-000106
2022 2022-09-24 394328000000 10-K 0000320193-22-000108
2021 2021-09-25 365817000000 10-K 0000320193-21-000105
2020 2020-09-26 274515000000 10-K 0000320193-20-000096
2019 2019-09-28 260174000000 10-K 0000320193-19-000119
2018 2018-09-29 265595000000 10-K 0000320193-19-000119
2017 2017-09-30 229234000000 10-K 0000320193-19-000119
Enter fullscreen mode Exit fullscreen mode

Each row is one fiscal year, newest first. value is in USD (unit says so). form is the 10-K that reported it and accession_number identifies that filing at the SEC. The three oldest rows share a filing because Apple's fiscal 2019 10-K reports all three years.

The 13 metrics you can ask for are revenue, net_income, eps_basic, eps_diluted, operating_income, total_assets, total_liabilities, equity, cash, operating_cash_flow, capex, free_cash_flow and shares_outstanding, plus revenue_contract if you want ASC 606 contract revenue specifically. period is annual, quarterly or ttm, and limit goes up to 200.

Quarterly, and what derived means

q = nn.history("AAPL", "revenue", period="quarterly", limit=8)
for v in q["data"]:
    flag = " (derived)" if v["derived"] else ""
    print(v["fiscal_period"], v["fiscal_year"], v["period_end"], v["value"], flag)
Enter fullscreen mode Exit fullscreen mode
Q4 2026 2026-09-26 114477000000  (derived)
Q3 2026 2026-06-27 109417000000 
Q2 2026 2026-03-28 111184000000 
Q1 2026 2025-12-27 143756000000 
Q4 2025 2025-09-27 102466000000  (derived)
Q3 2025 2025-06-28 94036000000 
Q2 2025 2025-03-29 95359000000 
Q1 2025 2024-12-28 124300000000 
Enter fullscreen mode Exit fullscreen mode

Companies report Q4 only as part of the annual figure, so a Q4 value is the annual minus the nine month year to date. The API does that subtraction and marks the result derived: true, with the inputs it used in derivation, so you know which numbers are reported and which are arithmetic. It never estimates: a metric a company did not report comes back as None with a reason string.

Two flags worth reading before you compute growth

Revenue is the one metric where filers change their mind about which line is "revenue". Older filings use Revenues or SalesRevenueNet; after ASC 606 most use RevenueFromContractWithCustomerExcludingAssessedTax. The API picks one concept per company where it can, and tags each value:

for v in r["data"]:
    print(v["period_end"], v["concept"], v.get("revenue_standard"), v.get("concept_changed", False))
Enter fullscreen mode Exit fullscreen mode
2026-09-26 us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax ASC 606 False
2025-09-27 us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax ASC 606 False
2024-09-28 us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax ASC 606 False
2023-09-30 us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax ASC 606 False
2022-09-24 us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax ASC 606 False
2021-09-25 us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax ASC 606 False
2020-09-26 us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax ASC 606 False
2019-09-28 us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax ASC 606 False
2018-09-29 us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax ASC 606 False
2017-09-30 us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax ASC 606 False
Enter fullscreen mode Exit fullscreen mode
  • revenue_standard is ASC 605, ASC 606, or None with revenue_standard_reason.
  • concept_changed turns True when the company switched concepts and the periods reported under both differ by more than 1 percent. concept_change then holds the old concept, the new one and the difference. If you are computing growth across that boundary, you are comparing two definitions, and now you know.

A growth table with pandas

import pandas as pd

df = pd.DataFrame(r["data"])[["fiscal_year", "period_end", "value", "form", "filed_date", "source_url"]]
df = df.sort_values("fiscal_year")
df["yoy"] = df["value"].pct_change()
print(df.to_string(index=False))
Enter fullscreen mode Exit fullscreen mode
 fiscal_year period_end        value form filed_date                                                                                       source_url       yoy
        2017 2017-09-30 229234000000 10-K 2019-10-31 https://www.sec.gov/Archives/edgar/data/320193/000032019319000119/0000320193-19-000119-index.htm       NaN
        2018 2018-09-29 265595000000 10-K 2019-10-31 https://www.sec.gov/Archives/edgar/data/320193/000032019319000119/0000320193-19-000119-index.htm  0.158620
        2019 2019-09-28 260174000000 10-K 2019-10-31 https://www.sec.gov/Archives/edgar/data/320193/000032019319000119/0000320193-19-000119-index.htm -0.020411
        2020 2020-09-26 274515000000 10-K 2020-10-30 https://www.sec.gov/Archives/edgar/data/320193/000032019320000096/0000320193-20-000096-index.htm  0.055121
        2021 2021-09-25 365817000000 10-K 2021-10-29 https://www.sec.gov/Archives/edgar/data/320193/000032019321000105/0000320193-21-000105-index.htm  0.332594
        2022 2022-09-24 394328000000 10-K 2022-10-28 https://www.sec.gov/Archives/edgar/data/320193/000032019322000108/0000320193-22-000108-index.htm  0.077938
        2023 2023-09-30 383285000000 10-K 2023-11-03 https://www.sec.gov/Archives/edgar/data/320193/000032019323000106/0000320193-23-000106-index.htm -0.028005
        2024 2024-09-28 391035000000 10-K 2024-11-01 https://www.sec.gov/Archives/edgar/data/320193/000032019324000123/0000320193-24-000123-index.htm  0.020220
        2025 2025-09-27 416161000000 10-K 2025-10-31 https://www.sec.gov/Archives/edgar/data/320193/000032019325000079/0000320193-25-000079-index.htm  0.064255
        2026 2026-09-26 478834000000 10-K 2026-11-03 https://www.sec.gov/Archives/edgar/data/320193/000032019326000026/0000320193-26-000026-index.htm  0.150598
Enter fullscreen mode Exit fullscreen mode

Every row still carries source_url, which is the part the rest of this post is about.

Check a number against the filing

Take the newest annual row:

latest = r["data"][0]
print(latest["value"], latest["form"], latest["filed_date"])
print(latest["source_url"])
Enter fullscreen mode Exit fullscreen mode
478834000000 10-K 2026-11-03
https://www.sec.gov/Archives/edgar/data/320193/000032019326000026/0000320193-26-000026-index.htm
Enter fullscreen mode Exit fullscreen mode

source_url opens the filing index on sec.gov. From there, open the 10-K document and find the income statement; the total net sales line for that fiscal year is the number the API returned. If it is not, the API is wrong, and I want to know: there is a daily public check at https://api.netnestdesign.com/v1/accuracy that does exactly this comparison for 25 companies against their own filings, and the gate is 25 of 25.

A number you can trace is a number you can defend.

Errors you will meet

  • RateLimitError (HTTP 429) carries retry_after in seconds when the server sends one. The free key is 100 calls a day.
  • PaymentRequiredError (HTTP 402) only appears on paid routes with no key; the API also accepts x402 payments from agents, which is a different post.
  • NetNestError for the rest, with status, code and message.

What the API does not have: foreign filers on 20-F, prices, estimates. Terms allow commercial use and display with attribution to the filing; bulk redistribution is not allowed. Docs and plans: https://api.netnestdesign.com/docs.

Data from SEC filings. Not investment advice.

Top comments (3)

Collapse
 
alexshev profile image
Alex Shev •

Linking every revenue value back to the filing is the crucial design choice here. Financial extraction needs provenance close to the number, especially when restatements or differing fiscal periods can make a clean-looking chart quietly misleading.

Collapse
 
netnestdesign profile image
NetNest Design LLC •

Thanks. Restatements are the case that shaped the design. When a later filing changes a number, the API returns the latest value, marks it restated: true, and credits the filing that first reported that value, so the accession number tells you which report a number reflects. GE's fiscal 2023 revenue, for example, comes back as 35.3 billion from the 10-K filed 2025-02-03, marked restated, rather than the figure in the original 2023 report. Fiscal periods follow each filer's own labels (NVIDIA's year ended 2026-01-25 is its fiscal 2026), so a chart built on fiscal_year and period_end does not mix calendars. If you find a case where that provenance still is not enough to make a chart honest, I want to hear it.

Collapse
 
stockpearl profile image
StockPearl •

the derived Q4 flag is the part I wish more data sources did. I've been building on raw EDGAR data myself, and the thing that bit me after that was restatements, where a later 10-K reports a prior year with a different value than the original filing. Which one does history() return in that case, the first reported value or the latest? I ended up keeping both and defaulting to the latest, but there are good arguments either way.