# EPA ECHO — US Facility Environmental Compliance and Enforcement (`dalbian/epa-echo-facility-compliance`) Actor

Search every US facility the EPA regulates and return its compliance record: current violation status under the Clean Air Act, Clean Water Act, RCRA and the Safe Drinking Water Act, inspections, formal enforcement actions and penalties in dollars. Free EPA open data, no key, no scraping.

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

## Pricing

from $30.00 / 1,000 search runs

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

## EPA ECHO — US Facility Environmental Compliance and Enforcement

Search every industrial and commercial facility the US Environmental Protection Agency regulates, and get back its compliance record: who is currently in violation, under which law, for how many quarters, how often they have been inspected, what enforcement followed and how much they were fined.

ECHO — Enforcement and Compliance History Online — is the EPA's combined view of its four big permitting programmes. No API key, no scraping, no licence fee.

### What a search returns

`states: ["NJ"]`, `majorFacilitiesOnly: true` — the 419 major sources in New Jersey:

| Summary field | Value |
| --- | --- |
| `facilitiesMatched` | 419 |
| `facilitiesWithSignificantViolations` | 30 |
| `facilitiesWithCurrentViolations` | 210 |
| `facilitiesWithViolationsInLast3Years` | 283 |
| `facilitiesInspected` | 365 |
| `facilitiesWithFormalEnforcement` | 201 |
| `totalPenalties`, `totalPenaltiesUsd` | `"$11,747,197"` · `11747197` |
| `facilitiesUnderAir` / `UnderWater` / `UnderHazardousWaste` | 319 · 279 · 289 |

Then one row per facility. ALERIS ROLLED PRODUCTS, INC in Clayton, NJ:

| Field | Value |
| --- | --- |
| `complianceStatus` | Violation Identified |
| `airComplianceStatus` | No Violation Identified |
| `waterComplianceStatus` | Violation Identified |
| `hazardousWasteComplianceStatus` | No Violation Identified |
| `drinkingWaterComplianceStatus` | *absent* — not a public water system |
| `quartersInNonCompliance` | 12 |
| `waterQuarterlyHistory` | `VVVVVVVVVVVVV` |
| `hazardousWasteQuarterlyHistory` | `_____VV_____` |
| `inspectionCount`, `lastInspectionDate` | 4 · 2026-02-19 |
| `formalActionCount`, `lastFormalActionDate` | 5 · 2026-06-15 |
| `totalPenalties`, `totalPenaltiesUsd` | `"$68,790"` · `68790` |
| `airPenaltiesUsd`, `hazardousWastePenaltiesUsd` | 61 665 · 7 125 |
| `airIds`, `npdesIds`, `rcraIds` | NJ0000003401500019 · NJG143723 · NJD051415909 |
| `echoFacilityUrl` | https://echo.epa.gov/detailed-facility-report?fid=110000321535 |

Every dollar figure arrives from ECHO pre-formatted as a string like `"$68,790"`. Both forms are kept: the string under `totalPenalties`, the number under `totalPenaltiesUsd`, so you can sort and sum without parsing.

### Nulls are not zeros

This is the point of the dataset and the easiest thing to get wrong.

`complianceStatus` takes the values **No Violation Identified**, **Violation Identified**, **Significant Violation**, **Violation**, **Unknown**, or nothing at all. The per-programme fields have longer vocabularies of their own — the Clean Air Act status alone runs to *Violation Addressed; Local Has Lead Enforcement*, *Violation Unaddressed; State Has Lead Enforcement*, *Violation w/in 1 Year*, and more.

A **missing** `drinkingWaterComplianceStatus` does not mean the facility complies with the Safe Drinking Water Act. It means the facility is not a public water system and the Act does not apply to it. Of 2 419 major facilities sampled in New Jersey, Texas and California, 567 had no Clean Air Act status at all and 1 203 had no Clean Water Act status. Counting those as compliant would overstate compliance by half.

The companion `regulatedUnderAir`, `regulatedUnderWater`, `regulatedUnderHazardousWaste` and `regulatedUnderDrinkingWater` booleans say which programmes actually apply, so you can tell "not regulated" from "regulated and clean".

The quarterly history strings hold one character per quarter, oldest first: `_` no violation on record, `V` violation, `S` significant violation, `U` unknown.

### Filters that actually filter

Every filter in this Actor was checked against the live API by comparing the filtered row count with the unfiltered one, because **ECHO silently ignores query parameters it does not recognise**. A misspelled filter does not raise an error; it returns the whole unfiltered set, which looks like a perfectly good result. Some plausible spellings are among the ignored ones.

State, city, county, ZIP, NAICS, SIC, facility name, registry and permit IDs, programme, major sources, active sites, federal sites, current violations, significant violations, quarters in violation, facilities with penalties, and a latitude/longitude radius in statute miles.

Some worked examples from real runs:

- `states: ["CA"]`, `mediaProgram: hazardousWaste`, `majorFacilitiesOnly`, `inViolationOnly`, `minQuartersInViolation: GT4` — 170 facilities, $62 688 299 in penalties.
- `states: ["TX"]`, `significantViolationsOnly`, `withPenaltiesOnly`, `activeOnly` — 311 facilities, $237 274 119.
- `latitude: 41.8240`, `longitude: -71.4128`, `radiusMiles: 3`, `majorFacilitiesOnly` — 9 major sources within three miles of downtown Providence.
- `states: ["NJ"]`, `facilityName: "MERCK"` — 44 facilities, including CALGON and DUPONT MERCK PHARMACEUTICAL CO, because ECHO matches the aliases it holds for a site as well as its current name.

### Enforcement detail

Turn on `includeEnforcementDetail` and each facility also gets its Detailed Facility Report unpacked into two extra row types.

`programSummary`, one row per statute — inspections, last inspection, quarters in non-compliance, quarters in significant non-compliance, formal actions, penalties. For ALERIS: CAA clean with $61 665 in penalties across four actions, CWA in violation for all twelve quarters with none, RCRA clean with $7 125.

`enforcementAction`, one row per formal action — the date, the action type, whether the state or the EPA led it, and the penalty. The four Clean Air Act administrative orders against ALERIS run 2022-07-27 · $2 400, 2023-08-07 · $30 800, 2024-05-02 · $24 640, 2026-06-15 · $3 825.

This costs one extra request per facility, so it is off by default and capped by `maxDetailReports`, highest penalties first.

### Who uses this

Environmental and ESG due-diligence teams screening a target's sites before an acquisition. Lenders and insurers pricing environmental liability on an industrial property. Journalists and advocacy groups tracking which plants in a county have been in violation for years without enforcement. Site-selection and real-estate analysts checking what sits within a few miles of a parcel. Supply-chain and procurement teams auditing suppliers against their own environmental standards. Law firms building a facility's enforcement history.

### Scope and limits

**ECHO refuses any query matching more than 100 000 facilities.** Verified: 98 167 rows succeed, 103 402 are rejected. This bites sooner than you expect — New Jersey alone holds 334 914 regulated records. The Actor reports the refusal in the summary row with the row count ECHO computed, so you know how much to narrow by. Adding `majorFacilitiesOnly` takes New Jersey from 334 914 to 419.

**Most records are inactive.** Rhode Island has 22 329 facilities on file and 3 838 currently active. `activeOnly` is off by default because EPA's own active flag is missing on some live sites, but it is usually what you want.

**Compliance data lags.** Status is computed from what state and EPA systems have reported, quarter by quarter. A violation that occurred last month may not appear for two quarters, and a resolved one may still show until the next refresh. The summary row carries the `dataVersion` string ECHO returns.

**State-lead enforcement dominates.** Most actions in the enforcement rows are taken by a state agency, not the EPA. The `leadAgency` field says which.

### Source and licence

**US Environmental Protection Agency**, Enforcement and Compliance History Online, through `echodata.epa.gov/echo` — the all-programme facility search and the Detailed Facility Report service. US federal government works, public domain. Queried live on every run; nothing is cached.

### Frequently asked

**Why does the summary say 365 inspections for 419 facilities when the facilities list adds up to 1 801?**
It does not say inspections. `facilitiesInspected` is the number of facilities with at least one inspection. All of ECHO's aggregate counts are facility counts, not event counts — this is the most common misreading of the data, so the fields here are named to prevent it.

**Can I get every facility in a state?**
Only for smaller states. The 100 000-row ceiling is ECHO's, not this Actor's, and it is not raisable by a parameter. Split a large state by county, by ZIP or by NAICS code.

**Why is `totalPenalties` $0 when `lastPenalty` is $13,000?**
`totalPenalties` covers the recent enforcement window ECHO reports on; `lastPenalty` is the most recent penalty on record whenever it happened. A facility fined in 2008 and quiet since will show exactly that pattern.

**Is `echoFacilityUrl` the same as ECHO's own link?**
It points at the same report. ECHO's own `DfrUrl` column currently ships a malformed address with spaces around the `=`, so the link here is rebuilt from the registry ID.

**What happens on a search that matches nothing?**
You get one `summary` row with `facilitiesMatched: 0` and a `note` saying so. Zero is a real answer from ECHO, not a failure, and it is charged as one search.

**How large a run is practical?**
Results are paged a thousand at a time. A run collecting 8 000 Rhode Island facilities took 44 seconds and returned 8 000 distinct registry IDs with no duplicates.

**Does it handle ECHO's expiring query IDs?**
Yes, and it has to. ECHO stores each query server-side under a numeric ID drawn from a small pool shared by every caller of every ECHO service. Measured on 2026-09-18, that counter advanced about six IDs per second and wrapped around a thousand, and an ID created for a 683-row query was handed to a different, 25-row query 189 seconds later — with no error, just different rows. This Actor re-runs its query before the ID can age out, checks every page against the row count the query reported, and re-runs and resumes if the ID has been taken.

# Actor input Schema

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

Two-letter state or territory codes, one per line — NJ, TX, PR. ECHO refuses any search that would match more than 100 000 facilities, so a whole state on its own is usually too broad; add a programme, a county or the major-facilities filter.

## `facilityName` (type: `string`):

Part of a facility name, for example MERCK. ECHO matches the name and the aliases it holds for the same site, so a search for MERCK also returns DUPONT MERCK PHARMACEUTICAL CO.

## `registryIds` (type: `array`):

Look up known facilities directly by their 12-digit EPA Registry ID, or by a programme ID such as an NPDES permit or an RCRA handler ID, one per line. Identifiers ECHO does not recognise are listed in the summary row.

## `city` (type: `string`):

City name, for example NEWARK.

## `county` (type: `string`):

County name without the word County, for example ESSEX.

## `zipCodes` (type: `array`):

Five-digit ZIP codes, one per line.

## `naicsCodes` (type: `array`):

Two, four or six-digit NAICS codes, one per line. A short code is a prefix match: 325 covers all chemical manufacturing.

## `sicCodes` (type: `array`):

Two to four-digit SIC codes, one per line. 2911 is petroleum refining; the two-digit 29 covers the whole petroleum and coal products group.

## `mediaProgram` (type: `string`):

Restrict the search to facilities regulated under one programme. A facility outside that programme is excluded entirely, rather than returned with a blank status.

## `majorFacilitiesOnly` (type: `boolean`):

Only major sources — large air emitters and major NPDES dischargers. On by default: ECHO refuses any search matching more than 100 000 facilities, and a whole state exceeds that on its own (New Jersey holds 334 914 regulated facilities but 419 majors). Turn it off only together with a narrower filter — a county, a ZIP code, a NAICS code or a facility name.

## `inViolationOnly` (type: `boolean`):

Only facilities with a current violation under any programme.

## `significantViolationsOnly` (type: `boolean`):

Only facilities in significant non-compliance — the EPA's own high-priority tier. Overrides the in-violation filter when both are on.

## `minQuartersInViolation` (type: `string`):

Persistence rather than a single bad quarter. ECHO tracks the last twelve quarters, so more than 8 means a facility that has been out of compliance for most of three years.

## `withPenaltiesOnly` (type: `boolean`):

Only facilities that have ever been assessed a monetary penalty.

## `activeOnly` (type: `boolean`):

Exclude facilities EPA records as closed or no longer operating. Around eight in ten records in a state-wide search are inactive.

## `federalFacilitiesOnly` (type: `boolean`):

Only sites operated by a federal agency — military bases, laboratories, depots.

## `latitude` (type: `string`):

Centre of a radius search, in decimal degrees (NAD83), for example 41.8240.

## `longitude` (type: `string`):

Centre of a radius search, in decimal degrees, negative in the US, for example -71.4128.

## `radiusMiles` (type: `integer`):

Search radius in statute miles around the latitude and longitude. Required when either coordinate is given.

## `includeEnforcementDetail` (type: `boolean`):

Fetch each facility's Detailed Facility Report and add a row per statute (inspections, quarters in non-compliance, penalties) and a row per formal enforcement action (date, action type, lead agency, penalty amount). One extra request per facility, so it is capped and off by default.

## `maxDetailReports` (type: `integer`):

Cap on how many facilities get a Detailed Facility Report, highest penalties first.

## `maxResults` (type: `integer`):

Cap on facilities returned. ECHO itself refuses any query matching more than 100 000.

## Actor input object example

```json
{
  "states": [
    "NJ"
  ],
  "mediaProgram": "any",
  "majorFacilitiesOnly": true,
  "inViolationOnly": false,
  "significantViolationsOnly": false,
  "minQuartersInViolation": "any",
  "withPenaltiesOnly": false,
  "activeOnly": false,
  "federalFacilitiesOnly": false,
  "includeEnforcementDetail": false,
  "maxDetailReports": 100,
  "maxResults": 500
}
```

# Actor output Schema

# 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": [
        "NJ"
    ],
    "majorFacilitiesOnly": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("dalbian/epa-echo-facility-compliance").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": ["NJ"],
    "majorFacilitiesOnly": True,
}

# Run the Actor and wait for it to finish
run = client.actor("dalbian/epa-echo-facility-compliance").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": [
    "NJ"
  ],
  "majorFacilitiesOnly": true
}' |
apify call dalbian/epa-echo-facility-compliance --silent --output-dataset

```

## MCP server setup

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

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/nO1eWRsBBqz74pjvr/builds/36GmiaxWb9xGMbebQ/openapi.json
