← Blog

Get SEC filings pushed to a webhook instead of polling EDGAR

If your code reacts to SEC filings, you have probably written the same loop twice: hit EDGAR on a timer, diff against what you saw last time, fire on anything new. It works, and it quietly costs you. The latency floor is your poll interval, the SEC's fair-access limit caps how tight that interval can get, and the dedup bookkeeping is yours to maintain forever. A webhook moves all of that off your box.

Why polling EDGAR is a tax

EDGAR gives you two ways in, and both pull you toward a polling loop. The per-company submissions feed at data.sec.gov is a JSON blob per CIK, so watching forty tickers means forty requests every cycle. The full-text index at efts.sec.gov is a search box that returns ten document hits per page. Neither pushes. To catch an 8-K within a few seconds of it clearing, you poll hard, and the SEC asks you to stay under 10 requests a second with a declared User-Agent or it starts returning 403s.

Then there is the part nobody enjoys: the same filing shows up on consecutive polls, so you track a high-water mark on accession or acceptanceDateTime, persist it across restarts, and handle the case where two filings share a second. None of this is the thing you actually wanted to build.

Register once, receive forever

A webhook inverts it. You describe the filings you care about, the service watches EDGAR for you, and it POSTs a signed JSON body to your URL when something matches.

curl -s -X POST -H "X-API-Key: $EDGAR_KEY" -H "Content-Type: application/json" \
  -d '{"event_types":["8-K"],"tickers":["NVDA","TSLA"],"material_only":true,"url":"https://your-app.com/hooks/edgar"}' \
  "https://api.edgarevents.com/webhooks"

The response hands back the subscription and, once, the signing secret:

{
  "webhook": {
    "id": "wh_X6lJO_ed8M4Ip6ge",
    "url": "https://your-app.com/hooks/edgar",
    "secret": "whsec_DAWIt4WDjcgLqPvCXOyuxTYFj5s",
    "event_types": ["8-K"],
    "tickers": ["NVDA", "TSLA"],
    "forms": null,
    "material_only": true,
    "active": true,
    "created_at": "2026-06-27T08:55:10.724912+00:00",
    "last_delivery": null,
    "delivery_count": 0,
    "fail_count": 0
  },
  "note": "store the secret; used to verify X-Edgar-Signature"
}

Store secret now. It is shown on creation and used to authenticate every later delivery. The filter fields stack: event_types selects the typed stream (8-K, activist_stake, and the form families), tickers scopes to companies, forms filters by raw form string, and material_only drops the routine 8-Ks that carry nothing but an exhibit index or a shareholder-vote result.

What lands on your endpoint

Each matching filing arrives as a POST whose body wraps the same event record the REST endpoints return:

{
  "type": "filing.created",
  "event": {
    "event_type": "8-K",
    "form": "8-K",
    "ticker": "NVDA",
    "company": "NVIDIA CORP",
    "cik": "0001045810",
    "filed_date": "2026-06-18",
    "accepted": "2026-06-18T20:00:24.000Z",
    "items": [
      { "code": "8.01", "label": "other events", "material": true },
      { "code": "9.01", "label": "financial statements and exhibits", "material": false }
    ],
    "material": true,
    "accession": "0001193125-26-275783",
    "filing_url": "https://www.sec.gov/Archives/edgar/data/1045810/000119312526275783/d48176d8k.htm"
  }
}

The request carries an X-Edgar-Signature: sha256=... header. It is an HMAC-SHA256 of the raw request body keyed by your webhook secret, so verify it before you act on anything:

import hmac, hashlib

def verify(secret: str, raw_body: bytes, header: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header)

Sign the bytes you received, not a re-serialized dict. Round-tripping the JSON through your parser can reorder keys and break the comparison.

Make the handler idempotent

Two facts decide the shape of a good handler. A delivery can repeat, because a non-2xx response (your app restarting, a slow database) earns a retry, and the fail_count on the subscription is there for exactly this. And EDGAR itself amends: a 13D becomes a 13D/A, an 8-K is corrected by another 8-K. So treat event.accession as the dedup key, write it down before you do real work, and return a 2xx fast. Push the heavy lifting (parsing the prospectus, sizing a position, sending a Slack message) onto a queue, then acknowledge. A handler that blocks on downstream work is a handler that times out and gets re-delivered.

Managing subscriptions

List what you have running and tear one down when a strategy retires:

curl -s -H "X-API-Key: $EDGAR_KEY" "https://api.edgarevents.com/webhooks"

curl -s -X DELETE -H "X-API-Key: $EDGAR_KEY" \
  "https://api.edgarevents.com/webhooks/wh_X6lJO_ed8M4Ip6ge"

Delete responds with {"deactivated": "wh_..."} and the subscription stops firing.

When polling is still fine

If you run a nightly research batch and a few minutes of lag costs you nothing, keep polling /filings and skip the endpoint you have to expose to the internet. Webhooks earn their keep when latency is the product: a catalyst that moves on the 8-K, an activist stake you want to read before the tape does, an IPO prospectus the moment it prices. For those, a signed POST within seconds beats any loop you can run politely against the SEC.

Free tier to try it, then $29/month, self-serve, cancel anytime. Grab a key at edgarevents.com; the interactive reference is at api.edgarevents.com/docs.

EDGAR Events is an independent service and is not affiliated with or endorsed by the U.S. Securities and Exchange Commission. Data comes from the public EDGAR system (data.sec.gov, efts.sec.gov).

SEC filings, already parsed.

Typed JSON for 8-K item codes, SC 13D activist stakes, IPO forms and merger proxies. $29/mo, self-serve, cancel anytime.

Get an API key