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.

See it on a real filing

Every figure on these pages is as reported, drawn at true proportion. No account needed.