SEC EDGAR API: clean JSON isn't correct JSON (2026)
Every SEC data API returns clean JSON now. Yours could too, in an afternoon.
The formatting was never the hard part. Whether the number is right is.
SEC EDGAR hands you the raw facts for free. Turning them into flat, well-typed JSON is easy. Turning them into JSON that is actually correct comes down to four specific judgment calls, and if you miss any of them your data looks perfect and reads wrong.
/v1/fundamentals and /v1/holdings make all four calls, so the JSON is correct, which clean formatting alone never guarantees.Clean data and correct data are not the same thing
Clean data is well-formatted: flat fields, consistent types, valid JSON you can parse without a fight. Correct data is right: revenue that is not null because the filer used a different tag, a 13F total that is not off by a factor of a thousand, a position that is not counted twice. The SEC gives you the raw facts. Clean formatting is an afternoon. Correctness is four decisions.
Here is the uncomfortable part.
Your parser cannot tell the difference. A wrong revenue figure and a right one are both just numbers in a field. The JSON validates either way. That is exactly why these traps ship to production: nothing looks broken.
Now that hosted, free SEC APIs are everywhere, clean JSON is table stakes. The four calls below are what separate data you can build on from data that quietly lies to you.
Why is your SEC revenue null for half the companies you check?
Because the same line item is filed under different XBRL tags across companies and years. Apple reports revenue as RevenueFromContractWithCustomerExcludingAssessedTax. Another company files it as Revenues, a third as SalesRevenueNet. Read one tag and you get a number for Apple and null for everyone else. The us-gaap taxonomy has thousands of overlapping concepts, and a typical 10-K carries six to ten thousand facts, per the StockFit XBRL notes.
This is the trap under every "just read companyfacts" tutorial.
It works on Apple, so it looks done. Then you run it across a watchlist and half the revenue fields are empty, because those filers tagged the same line differently. Tesla does not even use a us-gaap tag for it; it files custom concepts like tsla:AutomotiveSalesRevenue and tsla:ServicesAndOtherRevenue.
The fix is a per-field list of candidate tags, tried in order, first one with data wins. Edgrapi keeps that list: revenue resolves across five tags, net income across two, and so on, so a single /v1/fundamentals call returns a number for a company whether it tags revenue the modern way or the old way. We go deeper on this in why pulling clean fundamentals is harder than it looks.
Why is a fund's 13F 1,000 times too big or too small?
Because the value column changed units. For years, 13F value was reported in thousands of dollars. The SEC's 2022 amendment to Form 13F switched it to whole dollars for periods filed after January 3, 2023. So a 2021 filing that reads 1,743,219 means about 1.74 billion dollars; a 2024 filing that reads the exact same number means about 1.7 million. Read both the same way and one is wrong by three orders of magnitude.
Nothing in the number tells you which era it is from.
The unit depends on the filing date, not the value, so a parser that treats every filing identically silently mixes billions and millions in the same table. Assume the wrong era and a fund looks a thousand times bigger or smaller than it is.
The fix is to detect pre-2023 filings and multiply their value by a thousand, so everything you return is in whole dollars and comparable across quarters. Edgrapi does this before it hands back a single position, which the 13F holdings guide covers in full.
Why does a 13F position look double-counted?
Because one manager can report the same holding several times, once per sub-adviser. A large firm splits its book across internal managers, each files its slice, and the raw information table lists the same CUSIP on several rows. Sum the rows and the position, the total, and the holder count are all inflated. The fix is to aggregate by CUSIP, combining shares and value into one row per security before you rank anything.
This is the single most common bug in homemade 13F pipelines.
You see a fund holding what looks like 400 million shares of one name, and half of it is the same block reported twice under two sub-managers. The cover page lists them as other included managers, per the SEC's Form 13F FAQ, but the information table just repeats the CUSIP. Aggregate first, then read.
Edgrapi collapses those rows to one position per CUSIP as it parses, which is why its Apple line matches the fund's actual stake instead of a sum of duplicate slices.
Is a 13F put a long position?
No. The 13F information table has a put-or-call column, and a put is a bearish bet, not ownership of the shares. Fold puts into the share totals and a fund's "position" in a stock can actually be a short against it. The fix is to read the put-or-call field and keep options separate from long shares, never summing a put into a holding.
Most raw parsers ignore that column entirely.
So a fund shows a large, confident-looking "holding" in a stock that is really a put wagering the stock falls. If your screen is looking for conviction, you just found the opposite of it and labeled it a buy.
Edgrapi keys every row by CUSIP and by put or call, so a put is tracked as its own line and never merges into the long position. A put reads as a put.
How do you get correct JSON out of EDGAR?
Make these four judgment calls every time, or use an endpoint that already does. Edgrapi's /v1/fundamentals resolves each metric across its candidate XBRL tags, so revenue is never null over a tag mismatch. Its /v1/holdings aggregates a 13F by CUSIP, applies the right units by filing date, and flags puts. Same clean JSON any API returns, with the four decisions already made.
| The trap | What breaks | The fix |
|---|---|---|
| XBRL tag drift | Revenue null for filers who tag it differently | Candidate-tag list per field, first with data wins |
| 13F units switch (2023) | Values off by 1,000x depending on filing date | Detect pre-2023, multiply by 1,000, report whole dollars |
| CUSIP double-count | Positions and totals inflated by sub-manager rows | Aggregate one row per CUSIP |
| Put counted as a holding | A bearish bet read as ownership | Keep put/call separate from long shares |
These four are not the whole list. XBRL also mixes year-to-date and quarterly facts, hides the fourth quarter behind a full-year-minus-nine-months subtraction, and carries restated numbers for the same period. But the shape of the problem is always the same: correctness is a series of decisions, not a file format.
Start: check one tag
Pick a company that is not Apple, pull its revenue straight from companyfacts under a single tag, and see whether your code returns a number or a null. If it is null, you have met the first trap, and the other three are waiting in the 13F.
Then point /v1/fundamentals or /v1/holdings at the same company and compare. The free tier is 100 credits, no card. Same clean JSON, four fewer ways to be wrong. Point it at https://api.edgrapi.com and start with the company whose revenue just came back null.
Frequently asked questions
What is the difference between clean and correct SEC data?
Clean data is well-formatted: flat fields, consistent types, valid JSON. Correct data has the right values. SEC filings can be perfectly clean and still wrong, because the hard part is the judgment calls: resolving XBRL tags that differ by company, handling the 13F units change, aggregating duplicate positions, and telling a put from a holding. Clean is formatting; correct is those decisions.
Why is my SEC revenue null for some companies?
Because the same line item is filed under different XBRL tags. Apple uses RevenueFromContractWithCustomerExcludingAssessedTax; others use Revenues or SalesRevenueNet, and Tesla uses custom tags. Read a single tag and you get a number for one company and null for the rest. The fix is a candidate list per field, tried in order, so revenue resolves whichever tag the filer chose.
Why is a fund's 13F value off by 1,000x?
Because 13F value changed units. It was reported in thousands of dollars until the SEC's 2022 amendment, then whole dollars for periods filed after January 3, 2023. The same number means about 1.74 billion on a 2021 filing and 1.7 million on a 2024 one. The unit depends on the filing date, so a parser that treats every filing the same is off by three orders of magnitude.
Why does a 13F position look double-counted?
Because one manager can report the same holding under several sub-advisers, so the raw information table lists the same CUSIP on multiple rows. Sum them and the position, total, and holder count are all inflated. The fix is to aggregate by CUSIP into one row per security before you rank or total anything.
Is a 13F put a long position?
No. The 13F information table has a put-or-call column, and a put is a bearish bet, not ownership of the shares. Fold puts into share totals and a fund's apparent holding can actually be a short. Read the put-or-call field and keep options separate from long shares, so a put reads as a put.
Does the SEC EDGAR API normalize XBRL tags for you?
The SEC's own companyfacts endpoint does not; it returns raw XBRL where revenue sits under whatever tag the filer used, so normalizing is on you. Edgrapi's /v1/fundamentals resolves each metric across a candidate-tag list, so revenue is never null over a tag mismatch, and /v1/holdings handles the 13F units, CUSIP aggregation, and puts.