EPA Facility & Enforcement Data Scraper (US EPA ECHO Records) avatar

EPA Facility & Enforcement Data Scraper (US EPA ECHO Records)

Pricing

from $5.00 / 1,000 facility delivereds

Go to Apify Store
EPA Facility & Enforcement Data Scraper (US EPA ECHO Records)

EPA Facility & Enforcement Data Scraper (US EPA ECHO Records)

US EPA ECHO air, water (CWA/NPDES) and waste (RCRA) facilities plus enforcement cases from official EPA web services. Universe: all ECHO records, queried per state; coverage complete within it - 44x the air-only slice in DE (18,937 vs 427). Monitor emits only changes.

Pricing

from $5.00 / 1,000 facility delivereds

Rating

0.0

(0)

Developer

Steadfetch

Steadfetch

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Categories

Share

Extract US EPA ECHO records from the official ECHO web services: air, water (CWA/NPDES) and hazardous waste (RCRA) facilities plus federal enforcement cases, in one schema - address, coordinates, operating status, compliance status, high-priority-violation/SNC status, NAICS, penalties and enforcement case metadata. Built by Steadfetch ("Web data that doesn't break").

Universe and coverage

  • Universe: every facility and enforcement case in US EPA's Enforcement and Compliance History Online (ECHO), the official public register of EPA and state environmental compliance.
  • Coverage: complete within that universe, queried per state (ECHO rejects unfiltered nationwide queries). Five services in one Actor: air (air_rest_services), all media in one query (echo_rest_services), water (cwa_rest_services), waste (rcra_rest_services) and enforcement cases (case_rest_services.get_cases). The all-media search is the difference that matters: in Delaware, 18,937 facilities across all media vs 427 air-only (44x) (measured 2026-11-01 with plain HTTP calls against the documented services).

What it does

The Actor calls the official, public and key-less EPA ECHO REST services:

  • <service>_rest_services.get_facilities (or case_rest_services.get_cases) runs the search and returns a QueryID.
  • <service>_rest_services.get_qid returns the records for that QueryID (paged).

It does not scrape the ECHO website, does not need a login or API key, and does not touch CAPTCHAs. Only facility and enforcement-case records are read: no officer names, contacts or other personal data.

Search inputs are mapped to the documented ECHO parameters:

InputECHO parameterNotes
serviceservice familyair (default), all (air + water + waste), water, waste or enforcement.
statep_st (p_state for cases)Two-letter US postal abbreviation, for example NJ. Required for enforcement: case records carry no state of their own.
facilityNamep_fn (p_name for cases)Facility or case name filter.
naicsp_ncs (p_naics for cases)NAICS prefix, 2 to 6 digits. p_ncs is the documented parameter (p_naics is not, for facility searches).
sicp_sicFour-digit SIC code.
maxResultsresponseset + pagesDefault 100, maximum 5000.

At least one of state, facilityName, naics or sic is required: ECHO rejects unfiltered nationwide queries because the result set exceeds its query limit.

Snapshot and monitor modes

  • mode: "snapshot" (default): emit every record returned by the query. Use it for one-off research, backfills and exports.
  • mode: "monitor": emit only new records, or records whose compliance_status or hpv_status changed since the previous run of the same service. The dedupe key is registry_id + compliance_status + hpv_status (the case number stands in for the registry ID on enforcement cases), and the state is kept in the Actor's key-value store under one key per service, so status spaces never mix. Unchanged records are not pushed, so they are not charged.

Each item is validated against the published output schema before it is pushed. With strictSchema: true (the default) an invalid item fails the run instead of delivering malformed data. Every item carries a service field (air, all, water, waste, enforcement) so mixed datasets stay self-describing.

Example output

Facility item (air service; all, water and waste items share the same shape with service set accordingly):

{
"url": "https://echo.epa.gov/detailed-facility-report?fid=110014984464",
"title": "JERFS MOBIL SERVICE CENTER",
"scrapedAt": "2026-10-20T12:00:00.000Z",
"service": "air",
"registry_id": "110014984464",
"facility_name": "JERFS MOBIL SERVICE CENTER",
"source_id": "NJ000B0294",
"street": "592 RAMAPO VALLEY RD",
"city": "",
"state": "NJ",
"zip": "07436",
"county": "Bergen",
"latitude": 41.03846,
"longitude": -74.23281,
"status": "Permanently Closed",
"compliance_status": "No Violation Identified",
"hpv_status": "No High Priority Violation",
"naics": "447190: Other Gasoline Stations",
"last_violation_date": null,
"source_url": "https://echodata.epa.gov/echo/air_rest_services.get_qid?output=JSON&qid=876"
}

Enforcement case item:

{
"url": "https://echodata.epa.gov/echo/case_rest_services.get_case_report?output=JSON&p_id=02-2012-0813",
"title": "DuPont Chambers Works",
"scrapedAt": "2026-11-01T09:00:00.000Z",
"service": "enforcement",
"registry_id": "02-2012-0813",
"facility_name": "DuPont Chambers Works",
"source_id": "EPCRA-02-2012-0813",
"street": "",
"city": "",
"state": "DE",
"zip": "",
"county": "",
"compliance_status": "Closed",
"last_violation_date": null,
"case_category": "Administrative - Formal",
"enforcement_agency": "EPA",
"federal_penalty": "$0.00",
"source_url": "https://echodata.epa.gov/echo/case_rest_services.get_qid?output=JSON&qid=812"
}

Field notes:

  • url is the stable public ECHO Detailed Facility Report page, keyed by the FRS registry ID (the ECHO case report service, keyed by case number, for enforcement cases).
  • service says which ECHO search produced the item, so mixed datasets stay self-describing.
  • source_url is the exact ECHO get_qid JSON URL used for the run.
  • city is empty when ECHO returns the street in the city field (a real data quirk, normalized here instead of duplicating the address).
  • naics uses the code plus description when ECHO provides it, for example 447190: Other Gasoline Stations.
  • hpv_status is the air high-priority-violation status, or the equivalent significant-non-compliance signal for water (CWPSNCStatus), waste (RCRASNC) and cross-program (FacSNCFlg) records.
  • last_violation_date is an ISO date, or null when ECHO has no violation date; the water, waste, cross-program and case services expose no last-violation date, so it is null there.
  • last_inspection_date (cross-program, water and waste) and the enforcement fields case_category, enforcement_agency and federal_penalty are additive optional fields.

Input example

{
"state": "NJ",
"maxResults": 10
}

All media in one query (air + water + waste):

{
"service": "all",
"state": "DE",
"maxResults": 100
}

A monitor run over enforcement cases (state required):

{
"service": "enforcement",
"state": "NJ",
"maxResults": 100,
"mode": "monitor"
}

Pricing

Pay-per-event with the two synthetic platform events:

EventCharged whenPrice
apify-actor-starta run starts (charged automatically)0.00005 USD per run (Apify-recommended default; includes 5 seconds of free compute)
apify-default-dataset-itema validated facility or case is saved to the dataset (charged automatically)0.005 USD per record = 5 USD per 1,000 results

There is no custom per-item event and no Actor.charge() call: adding one on top of the synthetic dataset event would bill users twice.

The price sits in the middle of the direct ECHO competitor band on the Apify Store (roughly 2.50 to 12.50 USD per 1,000 facilities) and inside the Steadfetch 1-10 USD band.

Measured platform cost: a typical run is 2 HTTP calls (one search call plus one get_qid page for up to 1,000 results) and a few hundred milliseconds of 256 MB compute, well under 0.15 USD per 1,000 records. The gross margin is above 97%. Large runs above 1,000 results add one get_qid request per additional 1,000 results.

Only validated results are pushed. API errors, empty runs and unchanged records in monitor mode are not charged.

Data source and normalisation

  • Source: US EPA ECHO REST services, documented at https://echo.epa.gov/tools/web-services (air, cross-program, water and waste facility searches plus the enforcement case search).
  • Licence/legal: US federal public data, public web services with no login and no CAPTCHA; facility and case records only.
  • Service parameters used in this version: p_st, p_fn, p_ncs, p_sic, responseset, qid, pageno for facility searches; p_state, p_name, p_naics, p_sic for the case search; qcolumns on the cross-program and water get_qid calls, because their default columns miss fields this Actor requires (RegistryID, source ID, compliance and SNC status). Column IDs were checked against each service's metadata endpoint on 2026-11-01; the air and waste services need no custom columns.
  • Dates are converted from ECHO MM/DD/YYYY to ISO YYYY-MM-DD; impossible dates become null.
  • Duplicate registry_id values inside one response are collapsed before delivery.
  • The air service keeps its original mapping untouched; the expansion adds services and additive optional fields without changing existing required fields.

Output schema validation

Nothing at Steadfetch is published without automated output-schema tests:

  • npm test runs lint, typecheck and Vitest offline. The schema is compiled with Ajv, valid and invalid fixture items behave as expected, and extraction plus monitor dedupe run against committed ECHO JSON fixtures (no credit spent).
  • npm run publish:check runs the Steadfetch publish gate (schema compilation, PPE price band, English listing, CI presence).
  • npm run schemas:validate runs apify-cli validate-schema on the input, dataset and output schemas.
  • CI executes tests, build, entrypoint verification, the publish gate and schema validation on every push.

Development

npm install
npm test
npm run publish:check
npm run schemas:validate
npm run build && npm run verify:build

Fixtures live in test/fixtures/; tests never touch the network or the Apify platform. npm run smoke runs the Actor locally with apify run (no APIFY_TOKEN needed for a local run).

FAQ

Do I need an EPA account or API key? No. The ECHO web services used here are public and key-less.

Does it scrape the ECHO website? No. It calls the documented JSON web services (get_facilities, get_cases and get_qid). The url field points to the public facility report (or case report service) for human verification.

What is the difference between the services? air searches air facilities only (the original service). all runs the cross-program search covering air, water and waste in one query. water and waste run the single-program CWA and RCRA searches. enforcement returns federal enforcement cases; it requires a state because case records carry no state field.

What is the difference between snapshot and monitor mode? Snapshot emits every matching record. Monitor emits only new records or records whose compliance status or high-priority-violation status changed since the previous run of the same service, using registry_id + compliance_status + hpv_status as the dedupe key (the case number stands in for the registry ID on enforcement cases). State is kept per service, so running different services never cross-contaminates.

Why is city empty for some facilities? ECHO sometimes returns the street address in the city field. The Actor detects that duplicate and emits an empty city instead of repeating the street.

How many results can I request? Up to 5,000 per run. ECHO serves at most 1,000 records per page and the Actor pages automatically.

Why can't I query the whole nation? ECHO rejects unfiltered nationwide queries (the result set exceeds its queryset limit); at least one filter is required, and state-level queries cover the full universe.

Does it include personal data? No. It emits facility- and case-level fields only.

When am I charged? Once per run start and once per validated record saved to the dataset. Errors, empty runs and unchanged records in monitor mode deliver no items and incur no per-result charge.

Is the output schema stable? Yes. The 0.2 expansion added the service field and optional enforcement/inspection fields additively; existing required fields never changed, and air items keep their original shape (plus the additive service field).

Can I request another source? Yes. Open an issue in the Steadfetch Actors repository describing the public source you need.

Maintenance

This Actor is tested daily: the official ECHO endpoints are exercised with a minimal public input, the output is validated against the published schema, and silent drift (fewer results, empty required fields) turns into an issue. The ECHO services are versioned upstream; when EPA changes a field or a column ID the Actor is repaired before any new release.

Project structure

.actor/actor.json Actor definition (payPerEvent, dataset storage)
.actor/input_schema.json INPUT schema (service + filters + mode)
.actor/dataset_schema.json Dataset item JSON Schema (fields) + Store table view
.actor/output_schema.json Actor output schema (default dataset results)
.actor/pay_per_event.json PPE pricing (two synthetic events)
src/main.ts Entry point: fetch, validate, monitor dedupe, push
src/fetch.ts ECHO HTTP client, service registry and pagination (mocked in tests)
src/extract.ts Pure mapping, normalization and monitor state (offline tests)
src/validate-output.ts Dataset item schema validator (Ajv)
test/ Offline tests and ECHO JSON fixtures (one per service)