Balance Sheets for Every SEC Filer: Frames API, Financial Statement Data Sets or companyfacts.zip?
The SEC does not publish a file of every company's balance sheet. It publishes three free things you can build one from, and each fails in a different way. We pulled the Frames API and the latest Data Set on 6 October 2026, read the size of the third, and tested what came out.
The three routes
| Route | One download gives you | Which value you get |
|---|---|---|
| Frames API | one tag, one period, every filer, as JSON | the last filed value that fits the period |
| Financial Statement Data Sets | the numbers on the face statements of a quarter's filings. The 2026q1 file is 85 MB zipped, 559 MB unzipped | what each filing reported, once per filing |
| companyfacts.zip | every fact of every filer, 1.41 GB, rebuilt nightly | all of them, from every filing |
The SEC describes the first as returning "one fact for each reporting entity that is last filed that most closely fits the calendrical period requested", and the second as "without change from the 'as filed' financial reports". Those two sentences are most of the difference. The rest of this page is what they cost you in practice.
Frames: one request, but the value is the latest, not the filed one
A frame is the fastest way to a cross-section. One request per tag gives
you every filer that reported it for a period, as {cik, entityName,
end, val, accn}:
import requests
# the SEC requires a real contact address in the User-Agent
H = {"User-Agent": "Your Name you@example.com"}
def frame(tag, period="CY2025Q4I"):
base = "https://data.sec.gov/api/xbrl/frames/us-gaap"
url = f"{base}/{tag}/USD/{period}.json"
rows = requests.get(url, headers=H, timeout=60).json()["data"]
return {r["cik"]: r["val"] for r in rows}
A = frame("Assets")
print(len(A)) # 6157 filers on 6 October 2026
What it gives you is the value from the last filing that mentioned the
period. Nike's balance sheet at 31 May 2013 is a worked example. Its 10-K,
filed 23 July 2013, reported total assets of
$17,584 million. The frame for that date,
CY2013Q2I, returns $17,545 million, which is the
figure in the 10-K filed a year later. Both are Nike's numbers. Only one was
public in July 2013, and a screen or backtest built on frames cannot tell you
which. The Frames API has no way to ask for the earlier value.
Frames: a frame is not a single date
Asking for CY2025Q4I returned 6,157 filers that reported
Assets. They did not all report it for the same day:
| Balance sheet date | Filers |
|---|---|
| 2025-12-31 | 5,490 |
| 2026-01-31 | 253 |
| 2025-11-30 | 137 |
| 2025-12-27 | 73 |
| 2025-12-28 | 58 |
| 2026-01-03 and other dates | 146 |
The SEC's own documentation says to "be mindful different reporting start
and end dates for facts contained in a frame". The retailers with a January
year-end and the 52-week filers ending on a Saturday land in the same frame
as December filers. Keep the end field and treat it as the
balance sheet date, never the frame label.
Add the tags up and a quarter of them fail
The reason to pull Assets, Liabilities and equity together is to check one against the others. Run that on the raw tags and it fails more often than anything about a filer's arithmetic would explain:
L = frame("Liabilities")
E = frame("StockholdersEquity")
EN = frame("StockholdersEquityIncludingPortionAttributableToNoncontrollingInterest")
LE = frame("LiabilitiesAndStockholdersEquity")
# within half a percent of assets
def close(x, y, tol=0.005):
return abs(x - y) <= tol * max(abs(x), 1)
naive = [c for c in A if c in L and c in E]
print(sum(close(A[c], L[c] + E[c]) for c in naive), "of", len(naive))
better = [c for c in A if c in L and (c in EN or c in E)]
print(sum(close(A[c], L[c] + EN.get(c, E.get(c, 0))) for c in better), "of", len(better))
stated = [c for c in A if c in LE]
print(sum(close(A[c], LE[c]) for c in stated), "of", len(stated))
| Test on the raw tags (CY2025Q4I) | Pass |
|---|---|
| Assets = Liabilities + StockholdersEquity | 3,906 of 5,147, about three in four |
| same, using the equity tag that includes noncontrolling interests where the filer has one | 4,693 of 5,364, about seven in eight |
| Assets = LiabilitiesAndStockholdersEquity, the filer's own stated total | 6,029 of 6,029, every one |
The third line is the one to look at. Every filer that stated its own
total agreed with its own assets, to within half a percent, so the filings are fine and the sum is the
problem. StockholdersEquity is the parent's equity only. Of the
1,241 filers that failed the first test, 614 were fixed by switching to the
equity tag that includes noncontrolling interests. (The second test also
admits 217 filers that tag only the inclusive figure, which is why its
population is larger.) 671 filers still fail it. Two more tags, minority
interest where there is no inclusive tag and temporary equity (which is
mezzanine), account for 306 of those. For the other 365 we did not find a
cause among the tags we tested.
It is not rounding. At a tolerance of zero 4,517 pass; at five percent of
assets, 4,874. 490 still fail. And 638 of the 6,157 filers have no
Liabilities fact at all, because their balance sheet never prints
the subtotal (that
problem has its own note). The practical rule: when a filer states
LiabilitiesAndStockholdersEquity, use it as the referee, and
do not assume a failure means a bad filing. The
five reasons a balance sheet
fails the check are all in that residue.
Data Sets: as filed, with many rows per company
The Financial Statement Data Sets are the opposite trade. Nothing is replaced: the 2026q1 file holds 6,169 submissions (4,262 10-Ks, 972 10-Qs, 372 20-Fs, 112 40-Fs, and S-1s, 6-Ks and other forms) and 3,690,955 numeric rows. The SEC says the sets are "as filed" and also that it "cannot guarantee the accuracy of the data sets", which is worth reading as written.
For consolidated Assets in US dollars at 31 December 2025 the file has 5,027 rows, from 4,948 filers. The gap between those numbers, and what you have to filter before the count is right:
- 72 filers appear in more than one filing for that date (a 10-K and a later 10-K/A, for instance). You choose.
- 177 are tagged on the IFRS taxonomy (116 20-Fs, 50 40-Fs, 11 6-Ks). A request to the us-gaap Frames endpoint cannot see them.
- 62 come from forms that are not periodic reports (S-1, POS AM, 6-K and similar).
- Dimensional rows have to be dropped first. Only rows
with empty
segmentsandcoregare the consolidated figure. JPMorgan's FY2025 10-K has 23 Assets rows at one date and the first one in the file is EMEA's, $641.19 billion, not the bank's $4,424.9 billion (the long version).
companyfacts.zip: everything, and the choosing is yours
The bulk file is 1.41 GB, was rebuilt at 04:23 GMT on the day we measured, and contains every fact from every filing, comparatives and restatements included. Nothing is wrong with it. It simply hands you the selection problem: Apple's total assets at 28 September 2024 appear under five different filings, and the field labels are not what they look like (how to pick the right value). The per-company alternative is one request per filer: 6,157 of them is 10.3 minutes at the SEC's limit of ten requests a second, before you have parsed any of it.
None of the three carries a ticker. They are keyed by CIK, so you need the SEC's ticker file as well, and it lists current tickers only.
Which one to use
- A screen as of the latest filings. Frames, with the
stated total as the referee. Keep
end, and accept that restated values replace originals. - A backtest, or anything that needs the original number and the
day it was filed. The Data Sets (filing date is in
sub.txt), or companyfacts with itsaccnandfiledfields. Not frames. - Every period for every company, and you want to own the logic. companyfacts.zip, with a selection rule written and tested before you trust a number.
If you would rather not build this
BalanceProof is built on the Financial Statement Data Sets, as reported, and runs the check above on every filing: the filer's stated total is the referee, noncontrolling interests and four kinds of mezzanine equity are read when the filing uses them, and a filing that still does not reconcile is flagged with the reason instead of adjusted (how the check works). It covers 6,000+ US companies' periodic reports, not every filer. Every company page is free to read, the API gives a free key 1,000 calls a month, and the full dataset is one as-reported CSV of 1.3M rows, each dated by the filing that reported it, $79.99 once, built from the database each time you download it.