# State Data Breach Notices (`morehavoc/state-breach-notices`) Actor

Data-breach notices filed with US state attorneys general (California, Washington), normalised into one feed: organisation, reported and breach dates, people affected, data types and the notice link.

- **URL**: https://apify.com/morehavoc/state-breach-notices.md
- **Developed by:** [Christopher Moravec](https://apify.com/morehavoc) (community)
- **Categories:** Lead generation, News
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 notice returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## State Data Breach Notices

Data-breach notices filed with US state attorneys general, normalised into one dataset.

### States covered (v0.4)

| State | Status | Source | `affected` means | Data types | Notice link |
|---|---|---|---|---|---|
| CA | current | official CSV export (one request) | not published (always `null`) | no | no |
| WA | current | AG table, paged newest-first | Washington residents | yes | PDF |
| VT | current | AGO category page: recent table (one request) + archive pages | Vermont residents (recent table); `null` for archive rows | yes (recent table) | archive rows only |
| OR | current, from 2015 | DOJ table (one request) | the number reported to Oregon DOJ; the list does not say whether Oregon-only or total | no | no |
| MA | current, from 2024 | OCABR yearly report PDFs, one per year | Massachusetts residents | five Yes/No categories | no |
| NH | current, from 2006 | DOJ document list (the page's own JSON), newest first | not published (always `null`) | no | PDF |
| IA | current (years 2020, 2023 on) | AG yearly pages | not published (always `null`) | no | PDF |
| TX | current, rolling (~18 months) | OAG Salesforce page, **headless browser** | Texas residents | yes | no |
| MT | current but **months behind** (newest 2026-06-15 on 2026-10-24), from 2015 | OCP table page, **headless browser** (Cloudflare) | Montana residents | no | letter PDF |
| DE | **archive**: newest report 2025-07-11 | DOJ database page (one request) | Delaware residents (only breaches of 500+ DE residents are filed) | no | sample notice PDF |
| MD | **archive**: newest report 2025-03-21 | OAG's public SharePoint lists, one JSON request per year (2022 on) | Maryland residents | free text, one string | notice PDF |

DE and MD still serve their lists but stopped adding to them in 2025 (checked 2026-10-24). They return nothing for a
recent `daysBack`; use `daysBack: 0` or an older `since` for history. If either state resumes publishing, the new rows
are picked up with no code change (MD reads a new year's list as soon as it exists).

TX and MT are the only states that start a browser (one headless Chromium per run, closed at the end). A run without
them never loads Playwright.

#### Per-state notes

- **CA:** `breachStart`/`breachEnd` come from the free-text "Date(s) of Breach"; no counts, types or links.
- **WA:** `affected` is WA residents only; `dataTypes` split from the AG's semicolon list.
- **VT:** the recent table (from 2026-04-17) gives VT-resident counts and the AGO's data categories (comma list, e.g.
  "Social Security Numbers"), but no notice link and no breach dates. Older notices come from the archive, where each
  notice is a document titled "YYYY-MM-DD Organisation - Data Breach Notice to Consumers": `reportedDate` is that title
  date, `organization` is parsed from the title (older slug-style titles lose any hyphens in the name), `noticeUrl` is
  the document page, and `affected`/`dataTypes` are `null`. The archive is only fetched when `since` reaches back before
  the table's first date.
- **OR:** has breach, discovery and notice-sent dates. `affected` is the "Number Affected" column, scope unstated. A
  breach-date cell of `1/1/2001` is Oregon's placeholder for unknown: the dates are `null`, `breachDatesRaw` keeps it.
- **MA:** read from the yearly PDF (the office prints its spreadsheet), so text is placed by position. `affected` is MA
  residents. `dataTypes` lists the categories marked "Yes" out of five (Social Security numbers, medical records,
  financial account numbers, driver's license numbers, credit/debit card numbers); all "No" gives `null` (something
  else was exposed, not nothing). Voided rows ("-") and rows marked "DUPLICATE OF …" are dropped. 2023 and earlier PDFs
  use different layouts and are not read. The same organisation filing twice on the same day collapses to one notice
  (shared `id`); this is common in MA (about 2-5% of a year's rows, e.g. repeated card-issuer filings), so counts for
  those are the first row's. A few dates are the office's own typos (one 2024 row reads 09-Feb-14) and are kept.
- **NH:** `reportedDate` is the date DOJ posted the notice; a handful of old items have no posting date (`null`).
  The organisation is the document title (supplements are separate items). No counts or breach dates.
- **IA:** a notice card can list related entities that reported the same breach; each is returned as its own notice.
  Year pages the AG links for 2011-2019, 2021 and 2022 answer 404 and are skipped. A date the AG mistyped (`4-23-025`)
  is `null`.
- **TX:** `reportedDate` is the date published on the OAG site. `affected` is Texas residents as listed; the state
  shows `0` for a few notices, which is kept. Breach start/end and discovery dates come from the report.
  `dataTypes` split from the OAG's semicolon list. Texas removes reports from the list after about 18 months.
- **MT:** "Date Reported" is sometimes prose for supplemented notices ("First reported 06/02/2023 … additional residents
  on 07/10/2023"); `reportedDate` is the first date. `affected` is Montanans; "Unknown" and blanks are `null`.
- **DE:** a few rows have a non-date in "Reported Date" (e.g. "Awaiting supplemental response from counsel"); their
  `reportedDate` is `null` and they appear only when no date filter is set. "Addendum"/"Supplemental" labels are
  dropped from the organisation name. Two-digit years (`10/08/21`) are read as 20xx.
- **MD:** `dataTypes` is the state's free-text sentence as a single string, not split. One 2023 row reports `0` Maryland
  residents; that is the state's figure and is kept. The same organisation filing twice on the same day collapses to one
  notice (shared `id`).

#### Not covered, and why (checked 2026-10-24)

- **Maine:** the AG has taken the public breach database offline ("The public-facing database will remain offline")
  after abuse of the reporting system; still offline on 2026-10-24.
- **Indiana:** PDF year-to-date reports only.
- **Hawaii:** clean table, but it holds only 2023-11 to 2024-04 notices.
- **Wisconsin:** a short prose list (about 20 entries since 2024) with no date reported to the state.
- **North Dakota:** no public list found on attorneygeneral.nd.gov.

### Fields

`id` (stable hash of state + organisation + reported date), `state`, `organization`, `reportedDate`, `breachStart`, `breachEnd`,
`breachDatesRaw`, `discoveredDate`, `noticeSentDate`, `affected`, `affectedScope`, `dataTypes`, `noticeUrl`, `sourceUrl`.
Unknown is `null`, never 0 (a 0 appears only where the state itself printed 0). `affected` is in the state's own scope (`affectedScope` says which).

### Input

`states` (default all supported), `daysBack` (default 30; 0 = all), `since` (YYYY-MM-DD, overrides), `search`, `maxItems`.

### Pricing

Pay per event: `notice`, $0.002 per notice returned.

### Politeness and access

CA: one CSV request per run (its robots.txt asks for a 10 s crawl delay). WA: 2 s between pages, stops at the first page
older than `since`. VT: one request unless `since` predates the recent table, then 2 s between archive pages, stopping at
the first page older than `since`. OR: one request. MA: one PDF (about 1.4 MB) per year from the current year back to
`since`'s year (2024 at most), 2 s apart. NH: 100 notices per request, 2 s apart, stopping at the first page older than
`since` (the whole record is about 100 requests). IA: one page per year back to `since`'s year, 5 s apart (its
robots.txt Crawl-delay). TX and MT: one page load each in a headless browser. DE: one request. MD: one JSON request per
year from the current year back to `since`'s year (at least one list must answer), 1 s apart. The actor is meant to run
at most daily.

These are public government records with no login. Oregon's robots.txt is `Disallow: /`; per the owner's access rule
(2026-10-24) that alone is not a reason to skip a public record. MT's page sits behind a Cloudflare JavaScript check,
which the headless browser passes by itself; nothing solves CAPTCHAs, no accounts are used, and TX's data is the
response the page itself receives (no token is copied or replayed). A bot check that does not clear on its own fails
that state with a message saying so. The plain-HTTP user agent avoids the word "collector", which Iowa's firewall blocks.
A state whose page layout changes fails loudly in the run log; it never returns a silent empty list.

### Run locally

`npm install`, `npx playwright install chromium` (for TX and MT), put input in
`storage/key_value_stores/default/INPUT.json`, `npm start`. `npm test` runs a live check against every registered state
(60 days for current states, 180 for IA, 365 for MT, the whole record for DE and MD). The Docker image is
`apify/actor-node-playwright-chrome:22-1.64.1`; keep `playwright` in package.json at the same version.

### Next states

Add one file in `src/sources/` exporting `collect({since})` and `parse()`, register it in `src/collect.js`, and check the
site's terms first. Use `src/browser.js` (`withPage`, `gotoPastChallenge`) only when plain HTTP cannot get the data.

# Actor input Schema

## `states` (type: `array`):

Two-letter codes. Supported: CA, WA, VT, OR, MA, NH, IA, TX, MT (current lists; MT lags months behind), DE, MD (archives: those states stopped updating their public lists in 2025, so they return nothing for recent windows; use Days back 0 or an older Reported on or after). Empty = all supported.

## `daysBack` (type: `integer`):

Only notices reported in the last N days. 0 = everything on record.

## `since` (type: `string`):

Overrides Days back.

## `search` (type: `string`):

Case-insensitive filter on the organisation name.

## `maxItems` (type: `integer`):

Stop after this many notices (each notice is one charge).

## Actor input object example

```json
{
  "states": [
    "CA",
    "WA",
    "VT"
  ],
  "daysBack": 30,
  "maxItems": 1000
}
```

# Actor output Schema

## `notices` (type: `string`):

One row per breach notice: state, organization, dates, people affected, data types and the notice link.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "states": [
        "CA",
        "WA",
        "VT"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("morehavoc/state-breach-notices").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "states": [
        "CA",
        "WA",
        "VT",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("morehavoc/state-breach-notices").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "states": [
    "CA",
    "WA",
    "VT"
  ]
}' |
apify call morehavoc/state-breach-notices --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,morehavoc/state-breach-notices"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/bwtG6KmkRGhbddIIF/builds/NIRpfS6O2G9PEs9mr/openapi.json
