Home / Guides / Text extraction
Text extraction

Extract revenue and net income history from 10-K XBRL

Build a multi-year revenue, net income and margin table for one company from XBRL facts, handling revenue tag changes and repeated prior-year values.

Every 10-K and 10-Q includes XBRL: each number in the financial statements tagged with a concept, a period and a unit. /v1/xbrl/company-concept returns every value a company has reported for one concept, across all its filings. This tutorial turns those facts into a clean annual table of revenue, net income and margin.

Two things to handle

  1. Repeated values. Each 10-K reports the current year and prior years, so the same fiscal year appears in several filings. Keep one value per period end; this script takes the most recently filed one, which reflects any revision.
  2. Changing tags. Since revenue recognition rules changed in 2018, many companies report revenue as RevenueFromContractWithCustomerExcludingAssessedTax instead of Revenues, and some switch back and forth. Read both and merge.

Annual values are the facts from a 10-K whose period runs about a year (350 to 380 days, which allows for 53 week fiscal years). Quarterly facts from 10-Qs and year to date facts are dropped.

The code

import os
from datetime import date

import requests

BASE = "https://secfeedapi.com"
HEADERS = {"Authorization": f"Bearer {os.environ['SECFEED_KEY']}"}


def concept(ticker, tag):
    r = requests.get(f"{BASE}/v1/xbrl/company-concept", params={"ticker": ticker, "concept": tag}, headers=HEADERS, timeout=120)
    if r.status_code == 404:
        return []
    r.raise_for_status()
    return r.json()["units"].get("USD", [])


def annual(facts):
    """One value per fiscal year: full-year facts from 10-Ks, latest filing wins (each 10-K repeats prior years)."""
    out = {}
    for f in facts:
        days = (date.fromisoformat(f["end"]) - date.fromisoformat(f["start"])).days if f.get("start") else 0
        if f["form"] in ("10-K", "10-K/A") and 350 <= days <= 380:
            if f["end"] not in out or f["filed"] > out[f["end"]]["filed"]:
                out[f["end"]] = f
    return out


ticker = "COST"
# companies switch revenue tags over time, so read both and keep the most recently filed value per year
revenue = annual(concept(ticker, "Revenues") + concept(ticker, "RevenueFromContractWithCustomerExcludingAssessedTax"))
net = annual(concept(ticker, "NetIncomeLoss"))

print(f"{ticker} annual results from XBRL (USD millions)")
print(f"{'Fiscal year end':16}{'Revenue':>12}{'Net income':>13}{'Margin':>9}  Source 10-K filed")
for end in sorted(revenue)[-8:]:
    rev, ni = revenue[end]["val"], net.get(end, {}).get("val")
    margin = f"{ni / rev:.2%}" if ni else ""
    print(f"{end:16}{rev / 1e6:>12,.0f}{(ni or 0) / 1e6:>13,.0f}{margin:>9}  {revenue[end]['filed']}")

Output

Run on 2026-10-13 for Costco (fiscal years end around the start of September).

COST annual results from XBRL (USD millions)
Fiscal year end      Revenue   Net income   Margin  Source 10-K filed
2019-09-01           152,703        3,659    2.40%  2021-10-06
2020-08-30           166,761        4,002    2.40%  2022-10-05
2021-08-29           195,929        5,007    2.56%  2023-10-11
2022-08-28           226,954        5,844    2.57%  2024-10-09
2023-09-03           242,290        6,292    2.60%  2025-10-08
2024-09-01           254,453        7,367    2.90%  2026-10-07
2025-08-31           275,235        8,099    2.94%  2026-10-07
2026-08-30           303,154        9,226    3.04%  2026-10-07
200 25 22.6
225 25 21.1

Reading the result

  • Costco's fiscal year ending 2023-09-03 had 53 weeks, which the 350 to 380 day window keeps.
  • The source column shows which 10-K supplied each value. Years reported again in a later 10-K take the later filing.
  • Margin here is net income over revenue. Swap in OperatingIncomeLoss or GrossProfit for other margins, checking the company uses that tag.

Variations

  • Several companies: loop over tickers. To compare one concept across all companies for one period, use /v1/xbrl/frames (see Compare companies with frames).
  • Quarterly: keep 10-Q facts with a period of about 90 days.
  • Full statements: /v1/financial-statements returns the income statement, balance sheet and cash flow for the latest filings without choosing tags.

Next steps

More text extraction guides

Run this guide with your own key

500 free API calls a month. Every example on this page works on the free plan.

Get free API key →