SEC companyfacts: Why fy == 2024 Returns the Wrong Balance Sheet, and What to Filter On
Ask SEC companyfacts for Apple's total assets with
fy == 2024 and fp == "FY" and two numbers come back.
The first one is not fiscal 2024's.
What came back
| end | val | form | fy | fp | filed |
|---|---|---|---|---|---|
| 2023-09-30 | 352,583,000,000 | 10-K | 2024 | FY | 2024-11-01 |
| 2024-09-28 | 364,980,000,000 | 10-K | 2024 | FY | 2024-11-01 |
fy and fp describe the filing, not the
period the number is about. Apple's fiscal 2024 10-K contains two balance
sheets, this year's and last year's, so two facts carry its label. Code that
takes the first match (next(...), [0],
drop_duplicates) gets fiscal 2023. We checked the last eight
fiscal years of Apple, Walmart and Nike: the first match was the wrong year
24 times out of 24. Taking the last match was right 24 of 24, but only
because the facts arrive sorted by end date, which is not a documented
promise.
One balance sheet date, five filings
The same figure also appears in later filings as a comparative. Apple's balance sheet at 28 September 2024, $364,980 million, is in five of them:
| form | fy | fp | filed | frame |
|---|---|---|---|---|
| 10-K | 2024 | FY | 2024-11-01 | none |
| 10-Q | 2025 | Q1 | 2025-01-31 | none |
| 10-Q | 2025 | Q2 | 2025-05-02 | none |
| 10-Q | 2025 | Q3 | 2025-08-01 | none |
| 10-K | 2025 | FY | 2025-10-31 | CY2024Q3I |
So a filter on fy == 2025 and fp == "Q1" returns this
number as well as the quarter you wanted (28 December 2024, $344,085
million). A quarter's filing always carries the prior fiscal year-end next to
it.
frame is the Frames API's pick, not a fiscal label
The frame column above is the part that surprises people.
It is not a label for the period. It marks one fact per period, the
one the Frames API serves, and the SEC defines that as the last filed fact
that fits. It sits on the newest filing to repeat the date, and moves when a
newer one does:
| Balance sheet date | frame | carried by the fact in |
|---|---|---|
| Apple, 2024-09-28 | CY2024Q3I | the fiscal 2025 10-K, filed 2025-10-31 |
| Walmart, 2026-01-31 | CY2025Q4I | its fiscal 2027 Q2 10-Q, filed 2026-08-28 |
| Nike, 2013-05-31 | CY2013Q2I | the fiscal 2014 10-K, filed 2014-07-25 |
Apple's fiscal year ends in late September, so its year-end balance sheet
is calendar "Q3". Walmart's fiscal 2026 ends on 31 January 2026 and the frame
calls it calendar "Q4" of 2025. Nike's May year-end is "Q2". None of that is
a bug, and none of it is the fiscal year. Do not use frame to
label a fiscal year, and do not read it as "the original".
The same date can have different numbers
Nike's balance sheet at 31 May 2013 appears in five filings with three different totals:
| form | fy / fp | filed | Assets at 2013-05-31 |
|---|---|---|---|
| 10-K | 2013 FY | 2013-07-23 | $17,584M |
| 10-Q | 2014 Q1 | 2013-10-07 | $17,584M |
| 10-Q | 2014 Q2 | 2014-01-07 | $17,584M |
| 10-Q | 2014 Q3 | 2014-04-07 | $17,680M |
| 10-K | 2014 FY | 2014-07-25 | $17,545M |
We have not traced why each filing's figure differs. The point is that companyfacts keeps all five, so a rule has to choose. "As reported" is the earliest filing; "latest" is what the Frames API serves. A backtest needs the first, and the dates problem is bigger than it looks.
A rule that picks one value per fiscal year
Do not trust the labels; use what the filing itself says. A 10-K's balance sheet date is the latest date among its own balance sheet facts, so group by accession number and take that:
import collections
def annual_balance(facts, tag="Assets", forms=("10-K", "10-K/A", "10-KT")):
rows = facts["facts"]["us-gaap"][tag]["units"]["USD"]
by_filing = collections.defaultdict(list)
for f in rows:
if f["form"] in forms:
by_filing[f["accn"]].append(f)
out = {}
for accn, fs in by_filing.items():
# this filing's own balance sheet date
period_end = max(f["end"] for f in fs)
fact = next(f for f in fs if f["end"] == period_end)
keep = out.get(period_end)
# earliest filing = as reported
if keep is None or fact["filed"] < keep["filed"]:
out[period_end] = {"end": period_end, "val": fact["val"],
"filed": fact["filed"], "accn": accn}
return sorted(out.values(), key=lambda r: r["end"])
Flip the < to > for the latest value
instead. We ran it on Apple, Walmart and Nike, 17 fiscal year-ends each. The
last four for Apple:
| Fiscal year end | Total assets | 10-K filed |
|---|---|---|
| 2022-09-24 | $352,755M | 2022-10-28 |
| 2023-09-30 | $352,583M | 2023-11-03 |
| 2024-09-28 | $364,980M | 2024-11-01 |
| 2025-09-27 | $359,241M | 2025-10-31 |
It is not tested on filers that change their year-end, and a transition report (10-KT) is included in the forms list without having been exercised. Check those before relying on them.
Income and cash flow are worse
A balance sheet fact is a point in time. Income and cash-flow facts cover a
span, and a 10-K reports three. Apple's fiscal 2025 revenue under
fy == 2025 and fp == "FY":
| start | end | days | Revenue |
|---|---|---|---|
| 2022-09-25 | 2023-09-30 | 370 | $383,285M |
| 2023-10-01 | 2024-09-28 | 363 | $391,035M |
| 2024-09-29 | 2025-09-27 | 363 | $416,161M |
Three years carry the same label, and the 53-week year is 370 days, so
"about 365" is not a rule. Match the span to the filing's period end:
end == period_end and a duration of roughly a year. Quarterly
cash flow has its own trap, because a 10-Q reports it year to date; that and
the missing fourth quarter are covered in the
point-in-time note.
When you would rather not do the selecting
BalanceProof returns each figure with the period it describes and the day
it was filed, so there is nothing to disentangle. The keyless demo shows the
shape, and total_assets and liabilities_and_equity
come back side by side, because the filer's stated total is the referee:
curl https://balanceproof.dev/api/demo/AAPL
"period_end": "2026-06-27",
"filing_date": "2026-07-31",
"total_assets": {"value": 383266000000.0, "restated": false, ...},
"liabilities_and_equity": {"value": 383266000000.0, ...}
That is Apple's 10-Q for the quarter ended 27 June 2026, as of this
writing; it moves when Apple files. A free key reads any of the 6,000+
companies and one year of history, longer series are on paid plans, and
as_of returns what had been filed by a past date (Pro and above).
Figures are as reported to the SEC. When a later filing revises a figure, the
default view shows the revised value under the later filing's date;
as_of queries return the value known on the date you ask. If you are building across all companies at once, the
comparison of the SEC's
bulk routes is the companion to this.