> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dataville.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Monitor SEC filings for a watchlist

> Check each company you follow for a new SEC filing and send what's new to your team.

export const emailWrapper = (String.raw)`#!/usr/bin/env bash
# Reads DATAVILLE_API_KEY and ALERT_EMAIL from the environment.
if [ -z "$ALERT_EMAIL" ]; then
  echo "Set ALERT_EMAIL to the address to send alerts to first." >&2
  exit 1
fi

# Run the watchlist and capture whatever it printed. Stop if the script failed.
output=$(python3 watchlist.py) || exit 1

# The script prints nothing when no company has a new filing, so stop here.
[ -z "$output" ] && exit 0

# Send the lines as the body of one email.
printf '%s\n' "$output" | mail -s "New SEC filings" "$ALERT_EMAIL"
`;

export const teamsWrapper = (String.raw)`#!/usr/bin/env bash
# Reads DATAVILLE_API_KEY and WEBHOOK_URL from the environment.
if [ -z "$WEBHOOK_URL" ]; then
  echo "Set WEBHOOK_URL to your Teams webhook URL first." >&2
  exit 1
fi

# Run the watchlist and capture whatever it printed. Stop if the script failed.
output=$(python3 watchlist.py) || exit 1

# The script prints nothing when no company has a new filing, so stop here.
[ -z "$output" ] && exit 0

# Teams webhooks take an Adaptive Card: a "message" with one card attached,
# and the output as a single wrapped text block inside the card.
jq -n --arg text "$output" '{
  type: "message",
  attachments: [{
    contentType: "application/vnd.microsoft.card.adaptive",
    content: {
      "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
      type: "AdaptiveCard",
      version: "1.2",
      body: [{type: "TextBlock", text: $text, wrap: true}]
    }
  }]
}' | curl -s -X POST -H "Content-Type: application/json" -d @- "$WEBHOOK_URL"
`;

export const slackWrapper = (String.raw)`#!/usr/bin/env bash
# Reads DATAVILLE_API_KEY and WEBHOOK_URL from the environment.
if [ -z "$WEBHOOK_URL" ]; then
  echo "Set WEBHOOK_URL to your Slack webhook URL first." >&2
  exit 1
fi

# Run the watchlist and capture whatever it printed. Stop if the script failed.
output=$(python3 watchlist.py) || exit 1

# The script prints nothing when no company has a new filing, so stop here.
[ -z "$output" ] && exit 0

# Build the JSON body Slack expects and post it to the webhook.
jq -n --arg text "$output" '{text: $text}' \
  | curl -s -X POST -H "Content-Type: application/json" -d @- "$WEBHOOK_URL"
`;

export const watchlistScript = (String.raw)`import csv
import json
import os
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen

API_KEY = os.environ.get("DATAVILLE_API_KEY")
if not API_KEY:
    raise SystemExit(
        "Set your Dataville API key first, for example:\n"
        "export DATAVILLE_API_KEY=dataville_xxxxxxxxxxxxxxxx"
    )

# The companies to watch, and where the script remembers the most recent
# filing it saw for each one.
WATCHLIST_FILE = "watchlist.csv"
STATE_FILE = "filings.json"


def lookup(company):
    """Return the edgar source's result for a company, as its data object."""
    request = Request(
        f"https://api.dataville.com/edgar/{quote(company, safe='')}",
        headers={"Authorization": f"Bearer {API_KEY}"},
    )
    # Stop with a one-line message for any status other than 200, such as 401
    # for a wrong API key or 429 when a limit is reached.
    try:
        with urlopen(request) as response:
            return json.load(response)["data"]
    except HTTPError as error:
        raise SystemExit(f"Dataville returned {error.code} {error.reason} for {company}.")


def describe(filing):
    """Return the filing's form and date, with its 8-K items when it has them."""
    text = f"{filing['form_type']} filed {filing['filing_date']}"
    if filing["items"]:
        text += f" (items {filing['items'].replace(',', ', ')})"
    return text


with open(WATCHLIST_FILE, newline="") as watchlist_file:
    companies = [row["company"] for row in csv.DictReader(watchlist_file)]

# Load what the last run saw. On the first run there is nothing yet.
if os.path.exists(STATE_FILE):
    with open(STATE_FILE) as state_file:
        seen_filings = json.load(state_file)
else:
    seen_filings = {}

for company in companies:
    result = lookup(company)

    # A stale result comes from Dataville's stored copy, so it can't show a
    # new filing. Check the company again on the next run.
    if "stale" in result:
        print(f"{company}: SEC EDGAR unavailable, will check again next run")
        continue

    metadata = result["metadata"]

    # When a name doesn't match an SEC filer, the edgar source returns another
    # company's filing that mentions it, marked with a search_query field.
    # Say so once, then keep later runs quiet about this company.
    if "search_query" in metadata:
        if company not in seen_filings:
            print(f"{company}: no SEC filings found, not watching")
        seen_filings[company] = None
        continue

    filing = {
        "company": metadata["company_name"],
        "accession_number": metadata["accession_number"],
        "form_type": metadata["form_type"],
        "filing_date": metadata["filing_date"],
        "items": metadata.get("items"),
        "document_url": metadata["document_url"],
    }
    previous = seen_filings.get(company)
    if company not in seen_filings:
        # First run for this company: record its filing without an alert.
        print(f"{filing['company']}: now watching, most recent is {describe(filing)}")
    elif previous is None or previous["accession_number"] != filing["accession_number"]:
        # The accession number identifies one filing, so a different one means
        # the company has filed something new since the last run.
        print(f"{filing['company']}: new {describe(filing)}: {filing['document_url']}")
    seen_filings[company] = filing

# Save what this run saw so the next run can compare against it.
with open(STATE_FILE, "w") as state_file:
    json.dump(seen_filings, state_file, indent=2)
`;

export const watchlistTemplate = `company
The Home Depot
Oracle
IQVIA Holdings Inc.
Northwind Payments LLC
`;

After you approve a loan or a policy, a company's SEC filings will tell you when something changes. A new 10-Q or 10-K reports its latest results, and an 8-K reports an event such as new debt or a change of control.

In this guide, you'll set up a Python script that checks a list of companies each weekday morning and sends any new filings to Slack, Microsoft Teams, or email.

## Before you start

You'll need:

* A Dataville account and its API key, from the [API Keys](https://app.dataville.com/api-keys) page.
* Python 3.
* `jq`, if you'll send alerts to Slack or Microsoft Teams.

## Set up the watchlist

Put every file from these steps in one folder, on the machine that will run the script.

<Steps>
  <Step title="Download the watchlist template">
    The template has a `company` column with one company per row, and four examples.

    <a href={"data:text/csv;charset=utf-8," + encodeURIComponent(watchlistTemplate)} download="watchlist.csv" className="block">
      <Card title="watchlist.csv" icon="file-csv">
        Download the template with the example companies.
      </Card>
    </a>

    The file looks like this:

    ```csv theme={null}
    company
    The Home Depot
    Oracle
    IQVIA Holdings Inc.
    Northwind Payments LLC
    ```

    Northwind Payments LLC is a made-up private company, so you can see what the script does with a business that doesn't file with the SEC. To watch your own companies, replace the examples with each company's legal name and save the file as `watchlist.csv`.
  </Step>

  <Step title="Download the watchlist script">
    The script looks up each company in the `edgar` source, compares the result with the filing it saw on the last run, and prints a line for each new filing. Download it into the same folder as `watchlist.csv`.

    <a href={"data:text/x-python;charset=utf-8," + encodeURIComponent(watchlistScript)} download="watchlist.py" className="block">
      <Card title="watchlist.py" icon="file-code">
        Download the watchlist script.
      </Card>
    </a>

    <Accordion title="View watchlist.py" icon="code">
      ```python theme={null}
      import csv
      import json
      import os
      from urllib.error import HTTPError
      from urllib.parse import quote
      from urllib.request import Request, urlopen

      API_KEY = os.environ.get("DATAVILLE_API_KEY")
      if not API_KEY:
          raise SystemExit(
              "Set your Dataville API key first, for example:\n"
              "export DATAVILLE_API_KEY=dataville_xxxxxxxxxxxxxxxx"
          )

      # The companies to watch, and where the script remembers the most recent
      # filing it saw for each one.
      WATCHLIST_FILE = "watchlist.csv"
      STATE_FILE = "filings.json"


      def lookup(company):
          """Return the edgar source's result for a company, as its data object."""
          request = Request(
              f"https://api.dataville.com/edgar/{quote(company, safe='')}",
              headers={"Authorization": f"Bearer {API_KEY}"},
          )
          # Stop with a one-line message for any status other than 200, such as 401
          # for a wrong API key or 429 when a limit is reached.
          try:
              with urlopen(request) as response:
                  return json.load(response)["data"]
          except HTTPError as error:
              raise SystemExit(f"Dataville returned {error.code} {error.reason} for {company}.")


      def describe(filing):
          """Return the filing's form and date, with its 8-K items when it has them."""
          text = f"{filing['form_type']} filed {filing['filing_date']}"
          if filing["items"]:
              text += f" (items {filing['items'].replace(',', ', ')})"
          return text


      with open(WATCHLIST_FILE, newline="") as watchlist_file:
          companies = [row["company"] for row in csv.DictReader(watchlist_file)]

      # Load what the last run saw. On the first run there is nothing yet.
      if os.path.exists(STATE_FILE):
          with open(STATE_FILE) as state_file:
              seen_filings = json.load(state_file)
      else:
          seen_filings = {}

      for company in companies:
          result = lookup(company)

          # A stale result comes from Dataville's stored copy, so it can't show a
          # new filing. Check the company again on the next run.
          if "stale" in result:
              print(f"{company}: SEC EDGAR unavailable, will check again next run")
              continue

          metadata = result["metadata"]

          # When a name doesn't match an SEC filer, the edgar source returns another
          # company's filing that mentions it, marked with a search_query field.
          # Say so once, then keep later runs quiet about this company.
          if "search_query" in metadata:
              if company not in seen_filings:
                  print(f"{company}: no SEC filings found, not watching")
              seen_filings[company] = None
              continue

          filing = {
              "company": metadata["company_name"],
              "accession_number": metadata["accession_number"],
              "form_type": metadata["form_type"],
              "filing_date": metadata["filing_date"],
              "items": metadata.get("items"),
              "document_url": metadata["document_url"],
          }
          previous = seen_filings.get(company)
          if company not in seen_filings:
              # First run for this company: record its filing without an alert.
              print(f"{filing['company']}: now watching, most recent is {describe(filing)}")
          elif previous is None or previous["accession_number"] != filing["accession_number"]:
              # The accession number identifies one filing, so a different one means
              # the company has filed something new since the last run.
              print(f"{filing['company']}: new {describe(filing)}: {filing['document_url']}")
          seen_filings[company] = filing

      # Save what this run saw so the next run can compare against it.
      with open(STATE_FILE, "w") as state_file:
          json.dump(seen_filings, state_file, indent=2)
      ```
    </Accordion>
  </Step>

  <Step title="Run the script once">
    The first run records the most recent filing for each company. Set your API key, then run the script in the same terminal:

    ```bash theme={null}
    export DATAVILLE_API_KEY=dataville_xxxxxxxxxxxxxxxx
    python3 watchlist.py
    ```

    If you installed Python from python.org on a Mac and the script stops with `CERTIFICATE_VERIFY_FAILED`, run `Install Certificates.command` from your Python folder in **Applications**, then run the script again.

    For the example companies, the first run on September 24, 2026 printed:

    ```text theme={null}
    HOME DEPOT, INC.: now watching, most recent is 10-Q filed 2026-08-25
    ORACLE CORP: now watching, most recent is 8-K filed 2026-09-14 (items 8.01, 9.01)
    IQVIA HOLDINGS INC.: now watching, most recent is 8-K filed 2026-09-23 (items 1.01, 2.03, 9.01)
    Northwind Payments LLC: no SEC filings found, not watching
    ```

    Dataville returns each company's most recent 10-K, 10-Q, or 8-K, and the script saves it to `filings.json`. Every filing has an accession number that identifies it. On later runs, the script prints a line only when a company's accession number has changed, which means the company has filed something new. If the last run had recorded IQVIA's 8-K from September 9, 2026, the next run would print:

    ```text theme={null}
    IQVIA HOLDINGS INC.: new 8-K filed 2026-09-23 (items 1.01, 2.03, 9.01): https://www.sec.gov/Archives/edgar/data/1478242/000119312526399616/d128109d8k.htm
    ```

    An 8-K's items say what it reports. Item 1.01 is a new material agreement, and item 2.03 is a new financial obligation, such as a loan.

    Dataville limits requests to 100 per 15 minutes per IP address, so a watchlist of more than 100 companies needs a pause between batches. When you reach a limit, the script stops with a line such as `Dataville returned 429 Too Many Requests for The Home Depot.` See [Usage limits](/api-reference/usage-limits) for the other limits.

    If SEC EDGAR is unavailable, Dataville answers from its stored copy and adds a `stale` object to the result. The script skips those companies and checks them again on the next run, so it never reports an old copy as a new filing. See [Response format](/response-format#stale-results) to learn more about stale results.
  </Step>

  <Step title="Send new filings to your team">
    The script prints nothing when there are no new filings, so a short wrapper script can send its output to your team only when there's something to report. The wrapper reads where to send alerts from an environment variable, so you won't need to edit it.

    <Tabs>
      <Tab title="Slack">
        Create an incoming webhook in your Slack workspace by following Slack's guide to [sending messages using incoming webhooks](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/). In the terminal where you set your API key, set `WEBHOOK_URL` to the webhook's URL:

        ```bash theme={null}
        export WEBHOOK_URL=YOUR_WEBHOOK_URL
        ```

        <a href={"data:text/x-shellscript;charset=utf-8," + encodeURIComponent(slackWrapper)} download="watchlist.sh" className="block">
          <Card title="watchlist.sh" icon="file-code">
            Download the Slack wrapper.
          </Card>
        </a>

        <Accordion title="View the Slack wrapper" icon="code">
          ```bash theme={null}
          #!/usr/bin/env bash
          # Reads DATAVILLE_API_KEY and WEBHOOK_URL from the environment.
          if [ -z "$WEBHOOK_URL" ]; then
            echo "Set WEBHOOK_URL to your Slack webhook URL first." >&2
            exit 1
          fi

          # Run the watchlist and capture whatever it printed. Stop if the script failed.
          output=$(python3 watchlist.py) || exit 1

          # The script prints nothing when no company has a new filing, so stop here.
          [ -z "$output" ] && exit 0

          # Build the JSON body Slack expects and post it to the webhook.
          jq -n --arg text "$output" '{text: $text}' \
            | curl -s -X POST -H "Content-Type: application/json" -d @- "$WEBHOOK_URL"
          ```
        </Accordion>
      </Tab>

      <Tab title="Microsoft Teams">
        In Teams, open **Workflows**, choose the **Send webhook alerts to a channel** template, pick the team and channel, and click **Save**. Copy the webhook URL from the workflow's details page. To learn more, see Microsoft's guide to [creating incoming webhooks with Workflows for Microsoft Teams](https://support.microsoft.com/en-us/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498). In the terminal where you set your API key, set `WEBHOOK_URL` to the webhook's URL:

        ```bash theme={null}
        export WEBHOOK_URL=YOUR_WEBHOOK_URL
        ```

        <a href={"data:text/x-shellscript;charset=utf-8," + encodeURIComponent(teamsWrapper)} download="watchlist.sh" className="block">
          <Card title="watchlist.sh" icon="file-code">
            Download the Microsoft Teams wrapper.
          </Card>
        </a>

        <Accordion title="View the Microsoft Teams wrapper" icon="code">
          ```bash theme={null}
          #!/usr/bin/env bash
          # Reads DATAVILLE_API_KEY and WEBHOOK_URL from the environment.
          if [ -z "$WEBHOOK_URL" ]; then
            echo "Set WEBHOOK_URL to your Teams webhook URL first." >&2
            exit 1
          fi

          # Run the watchlist and capture whatever it printed. Stop if the script failed.
          output=$(python3 watchlist.py) || exit 1

          # The script prints nothing when no company has a new filing, so stop here.
          [ -z "$output" ] && exit 0

          # Teams webhooks take an Adaptive Card: a "message" with one card attached,
          # and the output as a single wrapped text block inside the card.
          jq -n --arg text "$output" '{
            type: "message",
            attachments: [{
              contentType: "application/vnd.microsoft.card.adaptive",
              content: {
                "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
                type: "AdaptiveCard",
                version: "1.2",
                body: [{type: "TextBlock", text: $text, wrap: true}]
              }
            }]
          }' | curl -s -X POST -H "Content-Type: application/json" -d @- "$WEBHOOK_URL"
          ```
        </Accordion>
      </Tab>

      <Tab title="Email">
        This version uses the `mail` command, which is on most Linux and macOS machines. In the terminal where you set your API key, set `ALERT_EMAIL` to the address to send alerts to:

        ```bash theme={null}
        export ALERT_EMAIL=team@example.com
        ```

        <a href={"data:text/x-shellscript;charset=utf-8," + encodeURIComponent(emailWrapper)} download="watchlist.sh" className="block">
          <Card title="watchlist.sh" icon="file-code">
            Download the email wrapper.
          </Card>
        </a>

        <Accordion title="View the email wrapper" icon="code">
          ```bash theme={null}
          #!/usr/bin/env bash
          # Reads DATAVILLE_API_KEY and ALERT_EMAIL from the environment.
          if [ -z "$ALERT_EMAIL" ]; then
            echo "Set ALERT_EMAIL to the address to send alerts to first." >&2
            exit 1
          fi

          # Run the watchlist and capture whatever it printed. Stop if the script failed.
          output=$(python3 watchlist.py) || exit 1

          # The script prints nothing when no company has a new filing, so stop here.
          [ -z "$output" ] && exit 0

          # Send the lines as the body of one email.
          printf '%s\n' "$output" | mail -s "New SEC filings" "$ALERT_EMAIL"
          ```
        </Accordion>
      </Tab>
    </Tabs>

    Download the wrapper into the same folder as `watchlist.py`, and make it executable:

    ```bash theme={null}
    chmod +x watchlist.sh
    ```

    To test it, delete `filings.json` so the script starts over, then run the wrapper in the same terminal:

    ```bash theme={null}
    rm filings.json
    ./watchlist.sh
    ```

    The first-run lines from the previous step will arrive in your channel or inbox.
  </Step>

  <Step title="Run the watchlist every weekday morning">
    Companies file throughout the business day, so a run each weekday morning will report the previous day's filings. Cron doesn't use the variables you set in your terminal, so set them at the top of your crontab, above the entry. This crontab runs the wrapper at 7 AM on weekdays, in the machine's time zone:

    ```text theme={null}
    DATAVILLE_API_KEY=dataville_xxxxxxxxxxxxxxxx
    WEBHOOK_URL=YOUR_WEBHOOK_URL
    0 7 * * 1-5 cd /path/to/watchlist && ./watchlist.sh
    ```

    Replace `/path/to/watchlist` with the folder that holds your files. For the email wrapper, set `ALERT_EMAIL` in place of `WEBHOOK_URL`. If you haven't scheduled a script before, ask ChatGPT how to on [macOS](https://chatgpt.com/?q=How+do+I+run+a+shell+script+every+weekday+at+7+AM+with+cron+on+macOS%3F), [Linux](https://chatgpt.com/?q=How+do+I+run+a+shell+script+every+weekday+at+7+AM+with+cron+on+Linux%3F), or [Windows](https://chatgpt.com/?q=How+do+I+run+a+script+every+weekday+at+7+AM+with+Task+Scheduler+on+Windows%3F). Windows has no cron, so on Windows you'll use Task Scheduler instead.
  </Step>
</Steps>

## Next steps

To check a business before you approve it, or to look up what an error in the script means, see these pages.

<CardGroup cols={2}>
  <Card title="Check applications in Claude" icon="clipboard-check" href="/guides/underwrite-from-claude">
    Check a batch of applications against SEC filings before you decide.
  </Card>

  <Card title="Troubleshooting" icon="circle-exclamation" href="/api-reference/errors">
    What each error code means, and when to retry.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.