How to Build an SEC Filing Alert System for 8-K Events

Someone asks you to alert the team when a portfolio company files something that matters — and within a day you realize that 90% of what EDGAR sends you is noise.

In this article, you will learn:

  • Why monitoring EDGAR directly breaks down at even moderate watchlist sizes
  • How to route SEC filing alerts on 8-K item codes instead of keywords or downloads
  • The complete 8-K event map — which items to act on, which to route, and which to ignore
  • How to enrich an alert with the actual press release or narrative text in one additional call
  • Poll cadence math and watchlist tiering for production systems
  • And lastly, how to build an SEC filing alert system that actually scales — with a free API

Someone on the team asks you to set up a notification — just a ping when one of the portfolio companies files something that matters. You have it running by lunch. EDGAR publishes an index, RSS exists, and the first version works in an afternoon. Then the next morning you look at what it produced: Form 4s, S-8 registrations, shelf takedowns, routine 8-K filings announcing that a shareholder meeting took place. The list is overwhelmingly noise.

The filings that do matter arrive disguised as the ones that do not. An 8-K is a container — the same form type carries “we appointed a new director” and “our previously issued financial statements can no longer be relied upon.” From the outside, both are an 8-K filed at 4:05pm. So every home-grown alerter converges on the same broken design: alert on everything, then download and read each filing to decide whether the alert was real. The alert fires before you know whether it is an alert.

Why free EDGAR monitoring breaks down

Monitoring EDGAR directly is free, and it is where I started too. 3 constraints end that experiment, usually in this order.

The rate limit is a hard ceiling. The SEC caps automated access at 10 requests per second, per IP address, across every EDGAR domain, and it counts by source IP regardless of how many machines you run. Exceed it and you receive a 403 Forbidden and an IP-level block lasting roughly 10 minutes — which, behind a corporate NAT or a shared VPC gateway, blocks your colleagues and your other services along with your alerter. A 500-name watchlist polled every 15 seconds is 33 requests per second. That is not a tuning problem. That is 3 times the ceiling, and the alerter is offline more often than it is running.

You must declare yourself, and browsers cannot. Every request needs a User-Agent header naming you with a contact address; library defaults are rejected on the first request. And because User-Agent is a forbidden header name in the Fetch specification, browser JavaScript physically cannot make a compliant EDGAR request. Any front-end alerting product needs a server in the path, always.

Nothing tells you what the filing contains. This is the real killer. EDGAR’s index gives you form type, date, and a link. To learn whether an 8-K is an earnings release or a routine exhibit filing, you must fetch the document and parse its cover page — which means downloading everything, at 10 requests per second, and classifying afterward. Your alerting latency is now bounded by your download throughput, and your download throughput is bounded by a limit you cannot raise.

Sit with that last one for a moment, because it is the one that ends the project. Having free access to filings is not the same as having timely, classified access to filings. That gap is not a detail. It is the entire product.

The 8-K item code is the event signal you need

So the classification gap is what kills the project. But the classification already exists — you just have to know where to look.

An 8-K’s cover page declares the items it reports. Item 2.02 is Results of Operations — earnings. Item 5.02 is Departure of Directors or Certain Officers. Item 4.02 is Non-Reliance on Previously Issued Financial Statements — a restatement.

Stop and appreciate what that is for a second, because it took me far too long to. This is a mandated, standardized, machine-readable classification of the event — produced by the filer, at the moment of filing, under legal obligation to get it right. It is exactly the routing signal an alerting system needs. And it exists before you have read a single word of the document.

Business Quant’s SEC Filings API returns those item codes in the search response itself. Every filing record carries an items[] array:

{
  "accession": "0001234567-26-000045",
  "publishdatetime": "2026-07-14 16:05:22",
  "formtype": "8-K",
  "items": ["Item2_02", "Item9_01"],
  "filing_exhibits": [
    { "type": "EX-99.1", "description": "Press Release", "url_doc_bq": "..." }
  ]
}

Item2_02, right there in the search response. That is an earnings release, and you knew it without a download, without a parse, without a classification pass and without a single false positive. Filings are indexed within seconds of EDGAR acceptance, and publishdatetime gives you minute-level precision to measure your latency against.

That one design detail is what turns SEC filing alerting from an intractable problem into a tractable one. Once you have that, everything else — polling, routing, enrichment — falls into place.

The complete 8-K event map

Knowing that item codes exist is step one. Knowing which ones deserve a wake-up call is step 2. Not every 8-K item is equally urgent — some require immediate action, some should be routed to a human for review, and some should be ignored entirely. Here is how I tier them.

Tier one: act immediately

Item keyEventWhy it matters
Item2_02Results of OperationsEarnings. The press release is attached as EX-99.1.
Item4_02Non-RelianceRestatement. Prior financials are being withdrawn. The strongest accounting red flag there is.
Item5_02Departure or Election of OfficersCEO and CFO turnover. Abrupt exits with no named successor are the tell.
Item1_05Material Cybersecurity IncidentMandated since 2023, on a 4-business-day clock.
Item3_01Notice of DelistingExchange compliance failure. Distress signal.
Item4_01Change in Certifying AccountantAuditor change. Read with the EX-16 letter.

Tier two: material, route to a human

Item1_01 Material Definitive Agreement · Item1_02 Termination of Agreement · Item1_03 Bankruptcy · Item2_01 Completion of Acquisition · Item2_03 Direct Financial Obligation · Item2_04 Triggering Events · Item2_05 Exit or Disposal Costs · Item2_06 Material Impairments · Item3_02 Unregistered Equity Sales · Item5_01 Change in Control · Item5_07 Submission of Matters to a Vote · Item7_01 Regulation FD Disclosure

Tier three: never route on these

Item keyWhy
Item9_01Financial Statements and Exhibits. Present on nearly every 8-K. It means “this filing has attachments,” not “an event occurred.”
SignatureStructural, not an event.

I want to flag that one loudly, because including Item9_01 in a routing table is the single most common defect I see in home-grown SEC filing alerters. It appears alongside real items constantly, so an alerter that routes on it fires on essentially every 8-K — which is precisely the noise problem you set out to solve. You will have rebuilt the thing you were replacing.

And 2 formatting rules that will cost you an afternoon each. 8-K keys are not lowercase: they are Item2_02, while 10-K keys are item1a. The separator is an underscore, not a period. My advice is to not think about this at all — pass back verbatim whatever the API returns, never normalize case, never rewrite separators.

Polling for new SEC filings

With the event map in hand, the next question is how to learn about new filings in the first place. The answer is polling — on an interval you control — which is the right design here, because filings are indexed within seconds of EDGAR acceptance. A 30-second poll sits comfortably inside the useful window for every use case short of latency arbitrage, and you decide which names get the tight interval and which do not.

from datetime import datetime, timedelta

def recent_filings(ticker, formtype="8-K", lookback_days=1):
    since = (datetime.utcnow() - timedelta(days=lookback_days)).strftime("%d-%m-%Y")
    r = requests.get("https://data.businessquant.com/secfilings", params={
        "ticker": ticker,
        "formtype": formtype,
        "from_date": since,          # DD-MM-YYYY, not ISO
        "api_key": API_KEY,
    })
    return r.json()["data"]

2 design rules here that matter far more than they look. I have been burned by both.

Query a window wider than your poll interval. Ask for the last day, not the last minute, and let deduplication filter it. If you query only “since my last successful poll” and one poll fails, that window is gone forever and you silently miss a filing. Overlapping windows with idempotent dedupe cannot drop events.

Persist your deduplication state. An in-memory set() works until the process restarts, at which point you re-alert on 24 hours of filings at 3am. Key it on accession and write it to disk.

Routing filings by event type

Polling gives you the raw filings. The next step is deciding what to do with each one as it arrives. The routing logic itself is short — a dictionary lookup against the item codes you already have in the response — but there is one design mistake here that costs people real alerts, and it is worth showing the code to explain it.

TIER_1 = {
    "Item2_02": "earnings",
    "Item4_02": "restatement",
    "Item5_02": "officer_change",
    "Item1_05": "cyber_incident",
    "Item3_01": "delisting_notice",
    "Item4_01": "auditor_change",
}
IGNORE = {"Item9_01", "Signature"}

def classify(filing):
    return [TIER_1[i] for i in filing["items"]
            if i in TIER_1 and i not in IGNORE]

One 8-K routinely reports several events. A CEO departure filed alongside a restructuring announcement is a single accession carrying both Item5_02 and Item2_05. So please do not break on the first match. In my experience the second event is very often the important one, and it is the one your break statement threw away.

Enriching the alert with the actual content

An alert reading “AAPL filed an 8-K reporting Item2_02” is a notification. An alert that arrives carrying the earnings press release is a research product. The distance between those two is one additional call — and which call you make depends on what fired.

Earnings fire, so fetch the EX-99.1 press release. The numbers are never in the 8-K body — they are in the attached exhibit. The SEC Filings API already hands you its URL in the filing_exhibits[] array of the response that triggered the alert:

for ex in filing["filing_exhibits"]:
    if ex["type"].startswith("EX-99"):
        press_release = requests.get(ex["url_doc_bq"]).text

Those url_doc_bq links point to Business Quant’s EDGAR mirror, which is not subject to the SEC’s 10-requests-per-second ceiling — so enrichment never becomes the bottleneck that delays the alert.

Anything else fires, so fetch the item’s narrative text. For a restatement, a departure, or a cyber incident, the substance is the 8-K body for that item, and the Section Extraction API returns exactly that item, pre-extracted, with nothing else in it:

r = requests.get(
    f"https://data.businessquant.com/secfilings/{accession}/content",
    params={"item_key": "Item4_02", "api_key": API_KEY},
)
restatement_text = BeautifulSoup(r.text, "html.parser").get_text("\n", strip=True)

That is the difference between an alert saying “possible restatement, go look” and an alert that quotes you the two paragraphs where the audit committee concludes the financials can no longer be relied upon. Only one of those is worth waking someone up for.

Link every alert to url_sec — the SEC’s own copy — because that is the URL a portfolio manager, compliance officer or auditor expects to click. Retrieve through the API; cite the SEC. Both URLs arrive in the same record.

Poll cadence and watchlist scale

At this point you have a working alerter: poll, classify, enrich. The remaining question is whether it stays working as the watchlist grows. The answer depends entirely on the math between your watchlist size and your poll interval.

You poll per name, which makes your request cost easy to reason about and easy to tier: watchlist size ÷ poll interval. It also means every request is scoped to a company you actually care about, rather than a market-wide stream you filter after the fact.

WatchlistIntervalRequests/secAgainst raw EDGAR
25 core names20s1.3Fine
100 names30s3.3Fine
500 names60s8.3At the edge
500 names15s33Instant block. 3× the ceiling.
3,000 names60s50Impossible

Those bottom two rows are the whole point of the table. Against raw EDGAR they are not slow, they are impossible. Against a purpose-built SEC filings API they are a for loop.

Tier your watchlist instead of polling everything at one speed — core positions every 20 seconds, candidates every 90 seconds, broad coverage every 15 minutes. Earnings filings concentrate between 4:00pm and 4:30pm ET, so tighten the core tier during that window and relax it overnight.

Pre-flight checks with the Form Types API

Here is a small optimisation that saved me a surprising amount. If you monitor 3,000 names for 13F filings, most of them have never filed one and never will. Every poll against those names is a request you paid for and learned nothing from.

The Form Types API returns the distinct form types a specific company has actually filed, so you can build your poll plan once at startup. Pass a ticker, get back the set of form types that company has on record, and intersect it with the types you care about. Any ticker that comes back without 8-K in its set never needs to be polled for 8-K filings at all.

r = requests.get("https://data.businessquant.com/secfilings/formtypes",
                 params={"ticker": "META", "api_key": API_KEY})
filed = set(r.json()["data"]["formtypes"])
# {'10-K', '10-Q', '8-K', '4', 'DEF 14A', 'S-1', 'SC 13G', ...}

poll_for = {"8-K", "10-K", "10-Q", "4"} & filed

The same endpoint accepts a date range, which makes it a filer-status detector: a company whose form mix shifts from 20-F and 6-K in 2023 to 10-K, 10-Q and 8-K in 2025 has transitioned from foreign private issuer to domestic filer — a material change in disclosure obligation, surfaced in 2 API calls.

Events that do not arrive as 8-K filings

Everything above assumes the signal you care about is an 8-K item code. Most of the time it is. But several important events live outside the 8-K entirely, and if your alerter only watches for 8-K filings it will miss them.

SignalWhere it livesHow to catch it
Auditor resignation letterEX-16 exhibit on the 8-KItem4_01 fires, then pull EX-16 from the documents manifest. The letter states whether there were disagreements.
Quarterly risk-factor updatespart2_item1a on the 10-QCompanies update risks mid-year here. Most monitoring misses this entirely.
Material weakness in controlsitem9a (10-K), part1_item4 (10-Q)Diff against the prior period; the language change is the alert.
Cybersecurity disclosureitem1c, Item16K, Item1_053 keys, one event class. Route them into a single handler.
Insider transactionsForm 4Poll formtype=4; the transaction matrix is an XML file in the manifest.
Dilution and shelf takedowns424B prospectusesPoll formgroup=Prospectus.

Edge cases to handle before production

The architecture works as described. What follows are the things that will bite you the first time you ship it to a real user — the kind of issues that only surface when the system runs unsupervised over a weekend.

8-K/A amendments carry a new accession, and only the changed files. An amended 8-K does not repeat the original exhibits. Treat the amendment as the complete record and you will show users an “earnings” alert with no press release attached, because the press release was in the original. Fetch both and merge.

3 different dates mean 3 different things. reportperiod is the fiscal period or event date. filingdate is the day EDGAR accepted it. publishdatetime is the minute it went public — the one your alerting SLA is measured against. Using reportperiod as “when the market knew” is a look-ahead bias defect that flatters a backtest right up until it trades.

After-hours is when it all happens. The 4:00–4:30pm ET window is where the interesting 8-K filings land, and they land there deliberately. A poll loop that goes to sleep at 5pm is ignoring precisely the events you built it for.

A filing with no routable items is not a failure. Plenty of 8-K filings carry only Item8_01 or Item9_01. Mark them seen and move on. Resist the urge to alert on everything and let the user filter — that is the design you are replacing.

The short version

If you take one idea from this page, make it this one. SEC filing alerting is not a delivery problem. It is a classification problem — and you do not have to solve it, because the classification already exists. The SEC requires filers to declare which items an 8-K reports. Someone has already done the hard part for you, under penalty of law, and most alerting systems throw that away and try to reconstruct it from keywords.

The Business Quant SEC Filings API returns that classification in the items[] array of every search result, so you know whether an 8-K is a restatement, an earnings release or a CEO departure before you download it. One enrichment call from the Section Extraction API turns the notification into something worth reading, and the Form Types API pre-flights your poll plan so you stop wasting requests on names that will never file.

And that 500-name watchlist on a 15-second poll — the one that gets you instantly blocked against raw EDGAR? It is just a loop. Go build it.

Frequently asked questions

What is the 8-K item code that signals an earnings release?

Item2_02, Results of Operations and Financial Condition. The Business Quant SEC Filings API returns it in the items[] array of the search response itself, so you know a filing is an earnings release before downloading it, and the press release is attached as an EX-99.1 exhibit.

Why should Item9_01 never trigger an alert on its own?

Item9_01 (Financial Statements and Exhibits) is present on nearly every 8-K because it just means the filing has attachments. Routing on it alone fires on almost every 8-K filed, reproducing the noise problem an alert system is meant to solve.

How often should I poll for new SEC filings?

Tier your watchlist rather than polling everything at one speed: core positions every 20 seconds, broader candidates every 90 seconds to 15 minutes. Filings are indexed within seconds of EDGAR acceptance, so a 30-second poll is comfortably inside the useful window for most alerting use cases.

Can I monitor SEC filings from a browser-based application?

Not directly against EDGAR. User-Agent is a forbidden header name in the Fetch specification, so browser JavaScript cannot set it, and EDGAR requires a declared User-Agent on every request. A server-side alerting layer, or an API that does not impose this requirement, is needed in the path.

Is the Business Quant SEC Filings API free for building an alert system?

Yes, it is free to use with an API key. Item codes come back directly in the search response, and filings are indexed within seconds of EDGAR acceptance, with no per-second rate limit standing between a large watchlist and a tight poll interval.