Python Beginner

SEC Filing Alert Email

Get email notifications when a company files a new 10-K, 10-Q, or 8-K with the SEC.

Note: The code examples in this tutorial have not yet been verified against the live API. If you encounter issues, please let us know.

What You'll Build

A Python script that monitors SEC filings for a watchlist of companies and sends you an email whenever a new 10-K (annual report), 10-Q (quarterly report), or 8-K (current event) is filed. The script tracks which filings it has already seen using a local JSON file, so you only get notified about new ones.

When you run it on a schedule (e.g., every hour via cron), you get a hands-off alert system that tells you when companies you care about file important documents with the SEC.

Prerequisites

  • Python 3.8+ installed
  • The requests library: pip install requests
  • A stockdata.dev API key (free tier works)
  • A Gmail account with an App Password (or a Resend API key — see Enhancements)

Gmail App Passwords: Go to your Google Account → Security → 2-Step Verification → App passwords. Generate one for "Mail" and use it in place of your regular password. This works even with 2FA enabled.

The Code

Create a file called filing_alerts.py and paste the following:

Python
import json
import os
import smtplib
from datetime import datetime
from email.mime.text import MIMEText
import requests

# --- Configuration ---
API_KEY = "your_api_key_here"
BASE_URL = "https://api.stockdata.dev/v1"

GMAIL_USER = "you@gmail.com"
GMAIL_APP_PASSWORD = "xxxx xxxx xxxx xxxx"
ALERT_TO = "you@gmail.com"

WATCHLIST = ["AAPL", "MSFT", "GOOGL", "TSLA"]
FORMS = ["10-K", "10-Q", "8-K"]

STATE_FILE = os.path.expanduser("~/.filing_alerts.json")

# --- State management ---
def load_state():
    if os.path.exists(STATE_FILE):
        with open(STATE_FILE) as f:
            return json.load(f)
    return {}

def save_state(state):
    with open(STATE_FILE, "w") as f:
        json.dump(state, f, indent=2)

# --- Fetch filings from API ---
def get_filings(ticker, form_type):
    resp = requests.get(
        f"{BASE_URL}/company/{ticker}/filings",
        headers={"X-API-Key": API_KEY},
        params={"form": form_type, "limit": 5}
    )
    resp.raise_for_status()
    return resp.json().get("filings", [])

# --- Send email ---
def send_email(subject, body):
    msg = MIMEText(body)
    msg["Subject"] = subject
    msg["From"] = GMAIL_USER
    msg["To"] = ALERT_TO

    with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
        server.login(GMAIL_USER, GMAIL_APP_PASSWORD)
        server.send_message(msg)
    print("Email sent.")

# --- Main logic ---
def main():
    state = load_state()
    new_filings = []

    for ticker in WATCHLIST:
        for form_type in FORMS:
            filings = get_filings(ticker, form_type)
            state_key = f"{ticker}:{form_type}"
            last_seen = state.get(state_key)

            for filing in filings:
                if last_seen and filing["filing_date"] <= last_seen:
                    break
                new_filings.append({
                    "ticker": ticker,
                    "form": form_type,
                    "date": filing["filing_date"],
                    "url": filing["edgar_url"]
                })

            # Update state to most recent filing date
            if filings:
                state[state_key] = filings[0]["filing_date"]

    save_state(state)

    if new_filings:
        body = "New SEC Filings Detected\n\n"
        for f in new_filings:
            body += f"{f['ticker']}\n"
            body += f"  {f['form']} filed {f['date']}\n"
            body += f"  {f['url']}\n\n"
        send_email(f"SEC Alert: {len(new_filings)} new filing(s)", body)
        print(f"Found {len(new_filings)} new filing(s).")
    else:
        print("No new filings.")

if __name__ == "__main__":
    main()

How It Works

The Filings Endpoint

The script calls GET /v1/company/{ticker}/filings for each ticker and form type. The endpoint accepts these parameters:

  • form — Filter by form type (e.g., 10-K, 10-Q, 8-K)
  • limit — Number of filings to return (we use 5 to catch anything missed)

A typical response looks like this:

JSON
{
  "ticker": "AAPL",
  "filings": [
    {
      "form": "10-Q",
      "filing_date": "2026-02-10",
      "report_date": "2025-12-28",
      "accession_number": "0000320193-26-000012",
      "primary_document": "aapl-20251228.htm",
      "edgar_url": "https://www.sec.gov/Archives/edgar/data/320193/000032019326000012/aapl-20251228.htm"
    }
  ]
}

State Tracking

The script stores the most recent filing date for each ticker-form combination in ~/.filing_alerts.json. On each run, it compares incoming filings against these dates. Anything newer triggers an alert. The state file looks like:

JSON
{
  "AAPL:10-K": "2025-11-01",
  "AAPL:10-Q": "2026-02-10",
  "AAPL:8-K": "2026-01-30",
  "MSFT:10-K": "2025-08-01"
}

On the very first run, the state file does not exist yet, so every filing returned by the API counts as "new." After that first run, only filings with dates newer than the last-seen date will trigger alerts.

Tip: If you want to skip the initial flood of alerts on the first run, run the script once manually and then discard the email. The state file will be populated, and subsequent runs will only alert on truly new filings.

Running It

Replace the configuration values at the top of the script with your own API key and Gmail credentials, then run it:

Shell
python filing_alerts.py

On the first run, you will see output like:

Shell
Found 7 new filing(s).
Email sent.

The email you receive will look like this:

Text
New SEC Filings Detected

AAPL
  10-Q filed 2026-02-10
  https://www.sec.gov/Archives/edgar/data/320193/...

TSLA
  8-K filed 2026-02-09
  https://www.sec.gov/Archives/edgar/data/1318605/...

On subsequent runs with no new filings:

Shell
No new filings.

Scheduling with Cron

To run the script automatically every hour during market hours (Monday through Friday, 9 AM to 6 PM Eastern), add a cron entry:

Shell
crontab -e

Add this line:

Crontab
0 9-18 * * 1-5 /usr/bin/python3 /home/you/filing_alerts.py >> /home/you/filing_alerts.log 2>&1

This runs at the top of each hour from 9 AM to 6 PM, Monday through Friday. Output is appended to a log file so you can check for errors.

Use full paths in cron. Cron does not load your shell profile, so use the absolute path to both Python and your script. Run which python3 to find the correct path on your system.

Enhancements

Use Resend Instead of Gmail

If you prefer not to use Gmail, Resend is a developer-friendly email API with a generous free tier (100 emails/day). Replace the send_email function:

Python
import requests

RESEND_API_KEY = "re_your_key_here"

def send_email(subject, body):
    requests.post(
        "https://api.resend.com/emails",
        headers={"Authorization": f"Bearer {RESEND_API_KEY}"},
        json={
            "from": "alerts@yourdomain.com",
            "to": ["you@gmail.com"],
            "subject": subject,
            "text": body
        }
    )
    print("Email sent via Resend.")

Send to Slack Instead

Post alerts to a Slack channel using an Incoming Webhook:

Python
SLACK_WEBHOOK = "https://hooks.slack.com/services/T.../B.../xxx"

def send_slack(text):
    requests.post(SLACK_WEBHOOK, json={"text": text})

Monitor More Form Types

Expand the FORMS list to catch other important SEC filings:

  • Form 4 — Insider transactions (buys and sells by executives)
  • 13F-HR — Institutional holdings (quarterly hedge fund disclosures)
  • S-1 — IPO registration statements
  • DEF 14A — Proxy statements (executive compensation, shareholder votes)
Python
FORMS = ["10-K", "10-Q", "8-K", "4", "13F-HR"]

Note: Form 4 filings are very frequent for large companies. If you add it to your watchlist, expect more emails. Consider filtering for only purchases above a certain dollar amount in a follow-up enhancement.

Ready to build?

Get your free API key and start coding in minutes.

Get Free API Key