If you want to know the moment a public company reports earnings, fires a CEO, or signs a merger, the source of truth is a Form 8-K on SEC EDGAR. There's a catch that surprises everyone who tries to consume 8-Ks programmatically: the thing you actually want — which "items" the 8-K reports — is not in the document. It's in the filing's SGML header.
I run FilingPulse, an API that normalizes EDGAR filings, so I've had to make this robust across every 8-K the SEC has accepted. Here's what the problem looks like and how the classifier works.
What an 8-K "item" is
8-K disclosures are keyed to a fixed list of item numbers. A few you'll recognize:
- 2.02 — Results of Operations (earnings)
- 5.02 — Departure/Election of Directors or Officers
- 1.01 — Entry into a Material Definitive Agreement
- 7.01 — Regulation FD Disclosure
- 8.01 — Other Events
One filing can carry several items at once. A single 8-K might be [2.02, 7.01, 9.01] — an earnings release, a Reg-FD statement, and the exhibits index. So the useful signal is which item codes are present, structured, not prose.
The trap: the body doesn't reliably contain the codes
The primary 8-K document is HTML meant for humans. Sometimes it lists "Item 2.02" as a heading; sometimes it just says "Results of Operations and Financial Condition"; sometimes the whole thing is a press release attached as an exhibit and the body barely mentions the item at all. Parsing the body for item codes is a losing game.
The fix: the index-headers file
Every filing folder on EDGAR publishes a small header file — the SGML submission header wrapped in HTML — at a predictable URL:
.../Archives/edgar/data/<CIK>/<ACCESSION_NODASH>/<ACCESSION>-index-headers.html
Inside, each reported item shows up as a line like:
ITEM INFORMATION: Results of Operations and Financial Condition
ITEM INFORMATION: Financial Statements and Exhibits
So you never parse the document at all. You pull one small header file, read the ITEM INFORMATION: lines, and map each caption back to its code:
import re, html
def item_codes(header_text):
caps = re.findall(r"ITEM INFORMATION:\s*([^\r\n<]+)", header_text)
return [CAPTION_TO_CODE.get(norm(html.unescape(c))) for c in caps]
Two quirks that will bite you
Building the caption-to-code map from the SEC's official item list almost works. Two things break it against real filings:
1. EDGAR's captions don't always match the form instructions. The official text for item 3.03 is "Material Modification to Rights of Security Holders" (singular). A large share of real headers say "Material Modifications" (plural). Match captions exactly and you silently drop 3.03 events. You need an alias table built from what EDGAR actually emits, not from the rulebook.
2. Captions are HTML-escaped. Apostrophes arrive as ', ampersands as &. "Changes in Registrant's Certifying Accountant" won't match unless you unescape first.
I found both the only way you ever really find these: by classifying a live feed and watching which items came back as unmapped.
Why headers, not full-text search
People reach for EDGAR full-text search or scraping the document because it's the obvious path. The header approach wins on three axes: it's one small request per filing instead of downloading a multi-MB exhibit bundle; it's structured (the SEC already classified it at filing time); and it's complete (every item the filer declared is there, even the ones the body forgets to mention).
If you just want the JSON
This is one endpoint in FilingPulse — /v1/events?item=2.02 returns every earnings 8-K, newest first, as normalized JSON, and webhooks push new ones within minutes of filing. Free tier, no card. But honestly the header trick above is the whole secret and you can run it against EDGAR yourself in an afternoon — the fair-access rules just want a declared User-Agent and a sane request rate.
It's data infrastructure, not investment advice — it reports what was filed, nothing else. Happy to answer EDGAR questions in the comments.
Top comments (0)