# EPA ECHO Scraper - Environmental Violations & Penalties (`groundplanes/epa-echo-facility-violations`) Actor

Scrape facilities with environmental violations or significant noncompliance under the Clean Water Act (CWA), Clean Air Act (CAA), or RCRA from EPA ECHO, with inspection history and 5-year penalties.

- **URL**: https://apify.com/groundplanes/epa-echo-facility-violations.md
- **Developed by:** [Groundplane Labs](https://apify.com/groundplanes) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.10 / 1,000 facilities

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## EPA ECHO Scraper - Environmental Violations & Penalties

Extract industrial, municipal, and commercial facilities with environmental violations or Significant Noncompliance (SNC / High Priority Violation) under the Clean Water Act (CWA), Clean Air Act (CAA), or Resource Conservation and Recovery Act (RCRA) directly from the U.S. Environmental Protection Agency (EPA) Enforcement and Compliance History Online (ECHO) web services.

### What it does

EPA ECHO aggregates compliance evaluations, quarterly violation histories, and formal enforcement actions from state environmental agencies and EPA regional offices across the United States. Querying ECHO manually requires navigating separate statute-specific search forms (`cwa_rest_services`, `air_rest_services`, `rcra_rest_services`), handling short-lived session `QueryID` tokens, and mapping inconsistent column names across statutes.

This Actor:

- Queries EPA ECHO's official REST services (`cwa_rest_services`, `air_rest_services`, or `rcra_rest_services`) one state at a time for facilities with noncompliance in the 12-quarter reporting window (`p_qiv=GE1`).
- Paginates `get_qid` immediately before the session `QueryID` expires, respecting EPA rate limits (at most 2 requests/second with exponential backoff retries).
- Normalizes statute-specific fields into a single consistent schema with facility address, NAICS/SIC codes, compliance status, quarters in violation (`quartersInViolationLast12`), `isSNC` boolean flag, last inspection date, 5-year formal enforcement action count, 5-year total penalties in USD, last penalty date, and direct Detailed Facility Report URL (`echoUrl`).
- Sorts results newest first by `lastPenaltyDate` descending, then `lastInspectionDate` descending before applying `maxItems`. Sort semantics, stated plainly: any row with a `lastPenaltyDate` ranks ahead of rows without one, even when its `totalPenalties5yrUsd` is 0 (EPA ECHO can record a penalty date outside the 5-year penalty total window, or a penalty of $0). Rows are ordered first by how recent that penalty date is, and only then by inspection recency (`lastInspectionDate` descending). So the top row is not necessarily the most recently inspected facility, and it is not necessarily a facility with a recent paid penalty.

### Who it's for

- **Environmental engineering & remediation firms** identifying industrial facilities with active NPDES, Clean Air Act, or RCRA compliance issues.
- **ESG, supply-chain & M\&A due diligence teams** screening manufacturing sites and suppliers for regulatory noncompliance and formal penalties.
- **Industrial water treatment & emissions-control equipment vendors** building targeted B2B facility lists by state and NAICS industry prefix.
- **Policy researchers & journalists** tracking enforcement actions, penalties, and inspection backlogs across states.

### Input

All filters have sensible defaults that return Clean Water Act (`CWA`) violation records in Arizona (`["AZ"]`) in a few seconds.

- **`program`** *(string enum)*: `CWA` (Clean Water Act / NPDES), `CAA` (Clean Air Act stationary sources), or `RCRA` (Resource Conservation and Recovery Act hazardous waste). Default: `"CWA"`.
- **`states`** *(array of strings, required)*: 1 to 10 two-letter US state or territory postal codes (e.g. `["AZ"]` or `["AZ", "NV", "NM"]`). Default: `["AZ"]`.
- **`onlySignificantNoncompliance`** *(boolean)*: When `true`, keeps only facilities in Significant Noncompliance (`CWA` / `RCRA` SNC) or High Priority Violation (`CAA` HPV). Default: `false`.
- **`minPenaltiesUsd`** *(integer)*: Minimum 5-year formal enforcement penalties in USD. Default: `0`.
- **`naicsPrefix`** *(string)*: Optional 1- to 6-digit NAICS industry code prefix (e.g. `"32"` or `"2213"`). Default: `""` (all industries).
- **`maxItems`** *(integer)*: Maximum number of facility rows to return. Default: `200`.

#### Example input JSON

```json
{
  "program": "CWA",
  "states": ["AZ"],
  "onlySignificantNoncompliance": false,
  "minPenaltiesUsd": 0,
  "naicsPrefix": "",
  "maxItems": 200
}
```

#### Example run options (default max charge $50)

When starting a run via the Apify API or Console, set a default maximum charge of **$50** (`"maxTotalChargeUsd": 50`) in your run options:

```json
{
  "maxTotalChargeUsd": 50
}
```

### Output

Results are deduplicated by `registryId + program` and sorted newest first by `lastPenaltyDate` descending, then `lastInspectionDate` descending. Rows with a penalty date rank first even when the 5-year penalty total is 0, followed by inspection recency for rows tied on penalty date or without a penalty date. ECHO sometimes returns several permits (SourceIDs) for one registry, and they can disagree on compliance status: in that case the row kept is the most severe record (currently in SNC first, then most quarters in violation, then the latest inspection), not the first one returned.

| Field | Type | Description |
|---|---|---|
| `registryId` | `string \| null` | 12-digit EPA Facility Registry Service (FRS) identifier. |
| `sourceId` | `string \| null` | Program-specific permit or handler ID (NPDES ID, ICIS-Air ID, or RCRA Handler ID). |
| `facilityName` | `string \| null` | Facility name reported to EPA ECHO. |
| `street` | `string \| null` | Facility street address. |
| `city` | `string \| null` | Facility city. |
| `state` | `string \| null` | Two-letter state code. |
| `zip` | `string \| null` | Facility ZIP code. |
| `county` | `string \| null` | County name. |
| `naicsCodes` | `string \| null` | Space- or comma-separated NAICS codes associated with the facility. |
| `sicCodes` | `string \| null` | Space- or comma-separated SIC codes associated with the facility. |
| `program` | `string` | Regulatory program queried (`CWA`, `CAA`, or `RCRA`). |
| `complianceStatus` | `string \| null` | Current EPA ECHO compliance status label. |
| `quartersInViolationLast12` | `integer \| null` | Number of quarters in noncompliance over the last 12 quarters (3 years). |
| `isSNC` | `boolean` | `true` if the facility is flagged for Significant Noncompliance (SNC) or High Priority Violation (HPV). |
| `lastInspectionDate` | `string \| null` | Most recent inspection/evaluation date (`YYYY-MM-DD`). |
| `daysSinceLastInspection` | `integer \| null` | Days elapsed since the most recent inspection/evaluation. |
| `formalActions5yr` | `integer \| null` | Count of formal enforcement actions over the past 5 years. |
| `totalPenalties5yrUsd` | `number \| null` | Total formal enforcement penalties assessed over the past 5 years (USD). |
| `lastPenaltyDate` | `string \| null` | Date of the most recent penalty assessment (`YYYY-MM-DD`). |
| `echoUrl` | `string \| null` | Direct link to the EPA ECHO Detailed Facility Report (`https://echo.epa.gov/detailed-facility-report?fid=<registryId>`). |

#### Sample output row (synthetic example)

```json
{
  "registryId": "110099990001",
  "sourceId": "AZ0099991",
  "facilityName": "Example Desert Surface Processing Plant",
  "street": "100 Industrial Parkway",
  "city": "Phoenix",
  "state": "AZ",
  "zip": "85040",
  "county": "Maricopa",
  "naicsCodes": "327320",
  "sicCodes": "3273",
  "program": "CWA",
  "complianceStatus": "Significant/Category I Noncompliance",
  "quartersInViolationLast12": 4,
  "isSNC": true,
  "lastInspectionDate": "2026-06-15",
  "daysSinceLastInspection": 138,
  "formalActions5yr": 1,
  "totalPenalties5yrUsd": 18500,
  "lastPenaltyDate": "2026-08-01",
  "echoUrl": "https://echo.epa.gov/detailed-facility-report?fid=110099990001"
}
```

### How pricing works

Pay per result. You only pay for rows returned. The Actor checks your run's maximum spending limit before every dataset push (recommended default: `"maxTotalChargeUsd": 50`) and stops cleanly if that limit is reached.

### Data source and terms

- **Official source:** [U.S. EPA Enforcement and Compliance History Online (ECHO) Web Services](https://echo.epa.gov/tools/web-services) and [ECHO About the Data / Terms](https://echo.epa.gov/resources/echo-data/about-the-data).
- **Update frequency:** EPA ECHO refreshes ICIS-NPDES (CWA), ICIS-Air (CAA), and RCRAInfo data weekly from federal and state reporting systems.
- **Disclaimers:**
  - **Facility data only, with a residual privacy risk:** This Actor outputs facility-level regulatory records only. It does not seek personal information about individuals. Residual risk: a facility name in EPA ECHO can be a sole proprietor's personal name, and a facility address can in some cases be associated with an individual operator. Treat `facilityName` and address fields accordingly.
  - **Status labels:** "In violation", "Significant Noncompliance", and "High Priority Violation" are administrative status labels published by EPA ECHO based on federal/state agency data submissions and are **not a legal finding of liability**.
  - This Actor is an independent tool and is **not affiliated with or endorsed by the U.S. Environmental Protection Agency (EPA)**.

### Limits and FAQ

- **Why do some rows show `complianceStatus: "No Violation Identified"` while `quartersInViolationLast12` is greater than 0?**
  EPA ECHO's `p_qiv=GE1` filter selects facilities with at least one quarter of noncompliance in the 12-quarter (3-year) window. A facility that had a violation in an earlier quarter but returned to compliance in the most recent quarter will have `quartersInViolationLast12 >= 1` and a current status of `"No Violation Identified"`. Set `onlySignificantNoncompliance: true` to restrict results to active SNC/HPV facilities.
- **Why are `formalActions5yr` or `lastPenaltyDate` null on many rows?**
  Most environmental compliance violations are resolved through informal notices or corrective actions without a formal penalty. When EPA ECHO reports no formal enforcement action or no penalty date for a facility, those fields are returned as `null`.
- **How many states can I query at once?**
  Up to 10 states per run. Each state is queried sequentially at a maximum of 2 requests/second to respect EPA ECHO server limits.

#### Canary input

A tiny input that returns at least 1 row in under 30 seconds:

```json
{
  "program": "CWA",
  "states": ["AZ"],
  "onlySignificantNoncompliance": false,
  "minPenaltiesUsd": 0,
  "naicsPrefix": "",
  "maxItems": 5
}
```

A healthy canary run ends with a status starting `Complete:` and every row validates against the output schema.

### Troubleshooting

- **Status starts with `PARTIAL:`**: The run finished but something was withheld or a source problem occurred (for example a state query failed, or EPA ECHO returned fewer rows than it reported). The status message states the concrete reason and the scanned / matched / emitted counts. A `PARTIAL:` caused only by your own limits (max charge) is expected behavior; raise the limit to get more rows.
- **Zero matches (`Complete: 0 rows`)**: The source is healthy but no facility matched your filters. Widen them: add states, set `onlySignificantNoncompliance` to `false`, lower `minPenaltiesUsd`, or clear `naicsPrefix`.
- **Run failed with a schema drift message**: EPA ECHO renamed or dropped a column this Actor depends on. The failure message names the missing fields.

Found a problem or need a field? Open an issue on the Issues tab; we respond quickly.

### Changelog

#### v0.1.3 (2026-11-01)

- Retry policy raised to 6 attempts with exponential backoff 2, 4, 8, 16, 32 s plus jitter, capped at 120 s. Retry-After is now parsed as seconds or as an HTTP-date. Added exampleRunInput matching the default input, description now starts with "Scrape", sort semantics and the residual privacy risk for facility names are documented plainly, and the 512 MB / 600 s resource choice is justified in TEST_NOTES.

#### v0.1.2 (2026-10-31)

- Red Team fix round. High: the core-column sentinel now also covers the street, city, state, ZIP, county, and SIC columns for all three programs, so a rename there fails loudly instead of emitting nulls. Medium: the request read timeout is 90 s (the ECHO air service legitimately takes 36 to 110 s on filtered queries; a 30 s timeout failed whole CAA runs on latency alone). Medium: when ECHO returns several permits for one registry and they disagree, the dedupe now keeps the most severe record (current SNC first, then most quarters in violation, then the latest inspection) instead of the first row returned. Retry-After is honored on retried requests.

- **v0.1.1 (2026-10-31):** Honest status contract (`Complete:` / `PARTIAL:` with scanned, matched, and emitted counts; zero matches is now a successful `Complete: 0` run instead of a failure); per-row output validation with a 20% skip limit; core-column sentinel that fails loudly on EPA ECHO schema drift; per-state failure isolation; `isSNC` now reflects current SNC status only (historical SNC quarters alone no longer flag a facility); store category corrected to BUSINESS; default run options set (max charge $50).

- **v0.1 (2026-10-31):** Initial release supporting CWA, CAA, and RCRA facility violation queries, `QueryID` pagination, SNC/HPV filtering, and penalty/inspection sorting.

# Actor input Schema

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

Maximum number of facility violation records to return.

## `minPenaltiesUsd` (type: `integer`):

Minimum total formal enforcement penalties assessed over the last 5 years in USD.

## `naicsPrefix` (type: `string`):

Optional NAICS industry code prefix filter (e.g. "31", "32", "33" for manufacturing, or "2213" for water/sewage systems). Leave empty for all industries.

## `onlySignificantNoncompliance` (type: `boolean`):

When true, keeps only facilities currently in Significant Noncompliance (CWA/RCRA SNC) or High Priority Violation (CAA HPV) status.

## `program` (type: `string`):

EPA regulatory program to query: CWA (Clean Water Act / NPDES), CAA (Clean Air Act), or RCRA (Resource Conservation and Recovery Act hazardous waste).

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

List of 1 to 10 two-letter US state or territory codes to query (e.g. \["AZ"] or \["AZ", "NV"]). Queried one state at a time against EPA ECHO.

## Actor input object example

```json
{
  "maxItems": 200,
  "minPenaltiesUsd": 0,
  "naicsPrefix": "",
  "onlySignificantNoncompliance": false,
  "program": "CWA",
  "states": [
    "AZ"
  ]
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# 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 = {
    "maxItems": 200,
    "minPenaltiesUsd": 0,
    "naicsPrefix": "",
    "onlySignificantNoncompliance": false,
    "program": "CWA",
    "states": [
        "AZ"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("groundplanes/epa-echo-facility-violations").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 = {
    "maxItems": 200,
    "minPenaltiesUsd": 0,
    "naicsPrefix": "",
    "onlySignificantNoncompliance": False,
    "program": "CWA",
    "states": ["AZ"],
}

# Run the Actor and wait for it to finish
run = client.actor("groundplanes/epa-echo-facility-violations").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 '{
  "maxItems": 200,
  "minPenaltiesUsd": 0,
  "naicsPrefix": "",
  "onlySignificantNoncompliance": false,
  "program": "CWA",
  "states": [
    "AZ"
  ]
}' |
apify call groundplanes/epa-echo-facility-violations --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,groundplanes/epa-echo-facility-violations"
        }
    }
}
```

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/21MwGmqgRZR1vEC30/builds/1XWwRNPWO5DKEHmEm/openapi.json
