The SEC Submissions API: What It Returns, and What It Cannot Reconcile
There are two EDGAR endpoints people reach for first, and they answer different questions. Confusing them costs an afternoon, and the confusion is reasonable, because both are described as giving you a company's filings.
https://data.sec.gov/submissions/CIK##########.json
https://data.sec.gov/api/xbrl/companyfacts/CIK##########.json
The first is an index of documents. The second is a bag of numbers. Neither one, on its own, lets you reconcile a balance sheet, and the reason is worth understanding before you build around either.
What submissions actually returns
Submissions is the filing history. For a given CIK you get identity fields
(name, SIC, exchange, former names) and then a filings object whose
recent member holds parallel arrays, one per column, not a list of
records:
import requests
HEADERS = {"User-Agent": "YourCompany yourname@example.com"}
def submissions(cik: int) -> dict:
url = f"https://data.sec.gov/submissions/CIK{cik:010d}.json"
return requests.get(url, headers=HEADERS, timeout=30).json()
def recent_filings(cik: int, forms=("10-K", "10-Q")):
doc = submissions(cik)
r = doc["filings"]["recent"]
rows = zip(r["accessionNumber"], r["form"], r["filingDate"],
r["reportDate"], r["primaryDocument"])
return [
{"accession": a, "form": f, "filed": fd, "period": rd, "doc": pd}
for a, f, fd, rd, pd in rows
if f in forms
]
Two things about that response catch people out.
The arrays are columnar. You have to zip them back into records yourself. Index drift between columns is silent and produces filings whose form type belongs to one document and whose date belongs to another.
recent is not everything. It holds at least a
year of filings or the latest thousand, whichever is more; anything older is paged out into separate files listed
under filings.files. For an established filer that window may not
reach back as far as you assume. Fetch the additional files and concatenate
before you claim to have a company's history.
What you will not find anywhere in that response is a number off the balance sheet. There are no assets, no liabilities, no equity. Submissions tells you that a 10-Q exists, when it was filed, what period it covers, and where the document lives. It does not tell you what the document says.
Why reconciliation needs the second call
Reconciliation is a statement about figures: assets on one side, liabilities and equity on the other, agreeing within tolerance. Submissions has no figures, so there is nothing to reconcile. The facts live in company facts, or in the narrower company concept endpoint if you know exactly which element you want:
https://data.sec.gov/api/xbrl/companyconcept/CIK##########/us-gaap/Assets.json
So the shape of any real pipeline is a join. Submissions establishes which filings exist and what periods they cover. Company facts supplies the values. The join key is where the work is.
Joining on period, not on filing date
The instinct is to join on filingDate. It does not work, and the
failure is quiet.
A fact in company facts carries end (the balance sheet date),
fy and fp (the fiscal year and period it was reported
under), form, filed, and usually accn, the
accession number. A filing in submissions carries reportDate and
filingDate. The pair that means the same thing is
reportDate and end. filingDate is when
the document was transmitted, which may be weeks later and which changes on
amendment while the period does not.
Coca-Cola is a clean example. Its first-quarter
2024 10-Q, for the period ended 29 March 2024, was filed on 2 May 2024. A 10-Q/A
for the same period followed on 30 May 2024. Same reportDate, two
filing dates, two accession numbers. A join on filing date treats them as two
unrelated quarters.
def facts_for_period(f: dict, tag: str, period: str, unit: str = "USD"):
node = f["facts"].get("us-gaap", {}).get(tag)
if not node:
return []
return [r for r in node["units"].get(unit, []) if r["end"] == period]
Call that and you will often get more than one row back for a single period. That is not a bug either.
The same period, reported more than once
A balance sheet date appears in company facts once for every filing that
reported it. A figure as of the end of Q2 shows up in the Q2 10-Q, again as a
comparative in the Q3 10-Q, again in the annual report, and again in any
amendment to any of those. The values are usually identical. When they are not,
the difference is the thing you actually wanted to know, and taking
max over the list throws it away.
Use accn to keep the provenance:
def by_filing(rows):
out = {}
for r in rows:
out.setdefault(r.get("accn"), []).append(r)
return out
Reconcile within a single accession number. Assets from the original 10-Q against liabilities and equity from the amendment is not a reconciliation of anything; it is two filings averaged into a number that appears in neither. If the identity closes on the original and fails on the amendment, that is a real finding about the company. If you mixed them, you have destroyed the evidence and produced a figure you cannot defend to anyone who asks where it came from.
This is the same discipline that applies to the individual claim lines. I set out the five mechanisms that make a filing fail the identity check, and what the correct response is to each, in The Five Ways a Balance Sheet Fails the Identity Check.
What submissions is genuinely good for
Having said what it cannot do, it does three things nothing else does as well.
Knowing what to fetch. Company facts returns a company's entire tagged history in one object, which for a large filer is a substantial download. If you only need the most recent annual figures, submissions tells you which period that is before you commit to the larger call.
Detecting amendments. 10-K/A and
10-Q/A appear in the form column. An amendment means a figure you
already stored may have been restated. Submissions is where you learn that
cheaply, on a schedule, without re-pulling facts for companies that have not
filed anything.
Resolving identity. formerNames carries prior
names with the dates they applied. Meta still lists
Facebook Inc there, and Block lists Square, Inc. up
to December 2021, a rename later followed by a ticker change. Renames and
reverse mergers are the ordinary reason a company appears to vanish from a
universe between quarters, and the mapping is right there.
Between the two endpoints, and the rate discipline in SEC EDGAR API Rate Limits: A Python Pipeline Under 10 RPS, you have everything EDGAR offers for free. What remains is the reconciliation itself, the extension tags no standard mapping reaches, and the long tail of filers who present a right-hand side shaped like nobody else's.
That is the layer BalanceProof is. Every balance sheet across 6,238 companies is reconciled within a single accession before it is served, and the ones that do not close carry the reason rather than a patched number. The free tier covers the full universe.