EPA ECHO Facility Violation History & Risk Score
Look up a U.S. facility's environmental compliance history — EPA's alleged violations and formal enforcement — and get one risk score with a plain-English summary.
The problem
Environmental due diligence, ESG review, lending and insurance underwriting, and supply-chain vendor screening all eventually need the same thing: has this facility had alleged environmental violations, and how serious are they? EPA publishes that data through ECHO (Enforcement and Compliance History Online), but ECHO's own interface is built for reading one facility's detailed report at a time, not for pulling a risk signal programmatically at scale.
Who it's for
Environmental and ESG compliance teams, lenders and insurers doing facility-level due diligence, and supply-chain or vendor-risk teams screening a manufacturing or industrial supplier before signing.
Use cases
- Facility-level environmental due diligence before a lending or insurance decision
- Vendor onboarding screen for a manufacturing or industrial supplier
- Re-checking an existing supplier or portfolio company on your own review schedule
- ESG/compliance review ahead of an acquisition or audit
What it does
Query a facility by name, street address, or EPA FRS registry ID and get back a single unified risk profile: a 0–100 risk score, a risk tier (low/medium/high/critical), active alleged violations, the facility's formal enforcement history with penalty totals for Clean Air Act, Clean Water Act and RCRA programs, which programs (including the Safe Drinking Water Act) the facility is regulated under versus currently flagged on, and a plain-English AI summary. The score and tier are always computed by the same deterministic point-based rubric — the AI writes only the narrative, from the same ECHO record.
Example
Request
GET /company-risk-screen?company_name=Acme%20Manufacturing
Response
Captured live 2026-09-18 from the request above, abridged: 2 of the 25 OSHA inspection records. Every other field and value is verbatim. This product reads five agencies live, so counts and dates move between calls.
{
"input": {
"company_name": "Acme Manufacturing",
"address": null,
"city": null,
"state": null,
"zip": null,
"epa_id": null
},
"epa": {
"resolved": true,
"facility": {
"name": "ACME BATTERY MANUFACTURING CO INC",
"address": "3340 MORGANFORD RD",
"city": "SAINT LOUIS",
"state": "MO",
"zip": "63116",
"epa_registry_id": "110001455684",
"epa_region": "07",
"is_major_facility": null,
"detailed_report_url": "https://echo.epa.gov/detailed-facility-report?fid=110001455684"
},
"risk_score": 0,
"risk_tier": "low",
"match_count": 17,
"unreadable_inputs": [],
"note": "EPA ECHO matched 17 facilities for this query; only the first result was considered, and the other 16 were not scored -- narrow the query (state/city/epa_id) for a more precise match. Its facility name (\"ACME BATTERY MANUFACTURING CO INC\") does not match company_name closely enough to be treated as this company (name similarity 0.692, below the 0.92 required of every agency), so this facility's risk_score is shown but contributes no points to cross_agency_risk_score. Supply epa_id to name the facility."
},
"osha": {
"status": "match",
"candidates": [
{
"matched_name": "ACME MANUFACTURING CO.",
"match_type": "exact",
"score": 1,
"record_count": 25,
"records": [
{
"inspection_nr": "305897357",
"establishment_name": "ACME MANUFACTURING CO.",
"date_opened": "2005-01-04T00:00:00",
"office_id": "0524500",
"state": "IL",
"inspection_type": null,
"scope": null,
"sic": "3444",
"naics": "333415",
"violations_count": 0,
"detail_url": "https://www.osha.gov/ords/imis/establishment.inspection_detail?id=305897357"
},
{
"inspection_nr": "305894198",
"establishment_name": "ACME MANUFACTURING CO.",
"date_opened": "2004-06-09T00:00:00",
"office_id": "0524500",
"state": "IL",
"inspection_type": null,
"scope": null,
"sic": "3444",
"naics": "333415",
"violations_count": 0,
"detail_url": "https://www.osha.gov/ords/imis/establishment.inspection_detail?id=305894198"
}
]
}
],
"source": {
"name": "OSHA Inspection + Violation, via the DOL Open Data API",
"url": "https://apiprod.dol.gov/v4/get",
"basis": "live",
"as_of": null,
"note": "OSHA's own inspection and citation datasets, retrieved through DOL's Open Data API rather than scraped from osha.gov, which blocks automated traffic. inspection_type and scope are reported as null: this dataset supplies coded values where those fields are documented as OSHA's free text, and we publish neither a raw code in a prose field nor an unverified decoding of one. violations_count is a count of citation rows joined from OSHA/Violation: 0 means the join ran and that dataset held no citations for the inspection, which is not the same as OSHA stating that none were issued. Do not read 0 as a clean bill of health. When truncated lists OSHA/Violation, the citation join was cut short, so violations_count is a lower bound and a 0 may be citations that were not read. The source publishes no data date in its response, so as_of is null rather than the date of the request."
},
"truncated": [
{
"dataset": "OSHA/Inspection",
"limit": 25
},
{
"dataset": "OSHA/Violation",
"limit": 100
}
],
"records_below_match_threshold": 0,
"withheld_possible_individual": false,
"coverage_note": null
},
"dol": {
"status": "no_match",
"candidates": [],
"source": {
"name": "DOL Open Data API (WHD Enforcement, MSHA Assessed Violations)",
"url": "https://apiprod.dol.gov/v4/get",
"basis": "live",
"as_of": null,
"note": "Fetched live from apiprod.dol.gov for this request. The source publishes no data date in its response, so as_of is null rather than the date of the request. EBSA is not covered -- its only enforcement-adjacent dataset is a case-tracking system, not a comparable violation list."
},
"truncated": [],
"records_below_match_threshold": 0,
"withheld_possible_individual": false,
"coverage_note": null
},
"fda": {
"status": "no_match",
"candidates": [],
"source": {
"name": "openFDA Enforcement (Food / Drug / Device)",
"url": "https://api.fda.gov",
"basis": "live",
"as_of": null,
"note": "Read live from api.fda.gov; the same company's result is served from our cache for up to 6 hours. api.fda.gov reports its own last-updated date, which this section does not yet carry through, so as_of is null rather than the date of the request."
},
"truncated": [],
"records_below_match_threshold": 0
},
"cpsc": {
"status": "no_match",
"candidates": [],
"source": {
"name": "CPSC Recall API (saferproducts.gov)",
"url": "https://www.saferproducts.gov/RestWebServices/Recall",
"basis": "live",
"as_of": null,
"note": "Read live from saferproducts.gov; the same company's result is served from our cache for up to 6 hours. The source publishes no data date in its response, so as_of is null rather than the date of the request."
},
"truncated": [],
"records_below_match_threshold": 0
},
"cross_agency_risk_score": 23,
"cross_agency_risk_tier": "medium",
"cross_agency_summary": "Acme Manufacturing has a cross-agency risk score of 23/100 (medium risk) combining EPA, OSHA, DOL, FDA, and CPSC federal enforcement/recall history. The first facility EPA ECHO returned does not match this company's name closely enough to be treated as this company, so it contributes no EPA points. OSHA records at least 25 inspection(s) (match confidence: match) -- a partial list: the source holds more records than this screen read. No DOL Wage and Hour (WHD) or MSHA enforcement case matched. No openFDA recall history matched. No CPSC recall history matched. Coverage is incomplete: the OSHA record list is partial -- the source holds more records than this screen reads, so those counts are lower bounds and the score was computed from a partial list. See each section's truncated field. Coverage is incomplete: the EPA ECHO facility found does not match this company's name closely enough to be scored, so the score includes no EPA compliance check -- supply epa_id to name the facility. Coverage is incomplete: EPA ECHO matched 17 facilities and only the first was considered, so the other 16 are not in the score. This is an automated, advisory-only assessment -- see the disclaimer field before relying on it.",
"incomplete_coverage": true,
"disclaimer": "This output is advisory only. It is not a regulatory, legal, or credit determination, and it is not a substitute for due diligence performed directly with each agency. Every section below carries its own `source` manifest (basis: live, mock, or mock-pending-key, plus as_of: the data's date as the source dates it, or null when the source publishes no date) -- when any section is not \"live\", the cross-agency risk picture is incomplete, flagged via `incomplete_coverage`. Company-name matching across agencies is deterministic fuzzy text matching, not verified corporate-identity resolution (e.g. shared EIN/DUNS) -- a high-confidence name match is not proof of legal entity identity, and a \"no_match\" does not guarantee a company has no history with an agency, particularly for a name variant not present in that agency's public data. Verify directly with the named agency before relying on this output for lending, insurance, procurement, or legal decisions.",
"generated_at": "2026-09-19T02:06:21.670Z"
}
Calling it directly — real, working requests you can paste and run:
cURL
curl "https://epa-echo-compliance-history-ai-risk-score-api.p.rapidapi.com/company-risk-screen?company_name=Acme%20Manufacturing" \ -H "X-RapidAPI-Key: $RAPIDAPI_KEY" \ -H "X-RapidAPI-Host: epa-echo-compliance-history-ai-risk-score-api.p.rapidapi.com"
Python
import os
import requests
RAPIDAPI_HOST = "epa-echo-compliance-history-ai-risk-score-api.p.rapidapi.com"
resp = requests.get(
f"https://{RAPIDAPI_HOST}/company-risk-screen",
params={"company_name": "Acme Manufacturing"},
headers={
"X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"],
"X-RapidAPI-Host": RAPIDAPI_HOST,
},
)
print(resp.json())
Node.js
const RAPIDAPI_HOST = 'epa-echo-compliance-history-ai-risk-score-api.p.rapidapi.com';
const url = new URL(`https://${RAPIDAPI_HOST}/company-risk-screen`);
url.searchParams.set('company_name', 'Acme Manufacturing');
const res = await fetch(url, {
headers: {
'X-RapidAPI-Key': process.env.RAPIDAPI_KEY,
'X-RapidAPI-Host': RAPIDAPI_HOST,
},
});
console.log(await res.json());
Where this API's data comes from, how we read it, and each source's own update schedule: Data Sources & Freshness.
Machine-readable: the OpenAPI 3.1 document for this API, generated from its code — import it into Postman or a client generator. More on the developer page.
A ready-made Postman collection is also available: import it and the shared environment in Postman (Import, then select the file), then set RAPIDAPI_KEY to your RapidAPI key.
Key features
- One score across four federal environmental programs (CAA, CWA, RCRA, SDWA)
- Deterministic, point-based scoring rubric — never an AI judgment on the number itself
- Live per-request query against EPA ECHO, never a stale bundled snapshot
- match_count on every name/address search, so you know when to narrow an ambiguous query
- GET /company-risk-screen expands the same company-name lookup across EPA, OSHA, DOL, openFDA, and CPSC into one cross-agency risk score
- A coverage_note on the agency sections whose sources name people as well as organizations, stating in words what that section could and could not cover — so a thin result reads as a bounded read rather than as a clean record
Pricing
A free tier of 20 requests a month covers light testing, no card required. Paid plans start at $99/month for 500 lookups and are hard-capped — calls stop at the limit rather than billing you. Only the top plan (5,000 lookups) runs on a soft limit, with overage at $0.08 per additional lookup beyond it.
Data is live, not a stale snapshot. This API queries EPA’s ECHO database on every request, so you see what ECHO shows at the moment you ask. ECHO itself says its data are “typically refreshed on a weekly schedule”, with drinking-water data extracted less often — see ECHO’s About the Data page. The same page says “a lag time exists (anywhere from a week up to three months) between when data are entered into the source database and when they appear in ECHO.”
This API is advisory only. The risk score and summary are a heuristic applied to EPA’s public ECHO data, and ECHO itself aggregates state and federal reporting, so a facility’s true current status can differ from what ECHO shows. Verify directly with ECHO or the facility before relying on this for lending, insurance, legal, or procurement decisions. If a query matches more than one facility, narrow it with state, city, or an exact EPA registry ID rather than assuming the first result is the one you meant.
Multi-agency company risk screen
GET /company-risk-screen takes a company name — plus optional address, city, state, zip,
or an exact EPA registry ID — and returns candidate matches and findings across EPA, OSHA,
DOL, openFDA, and CPSC in one call, with a single deterministic cross-agency risk score.
This is additive: it doesn’t replace GET /risk-score, the EPA-only facility lookup
described at the top of this page.
GET /company-risk-screen?company_name=Acme+Manufacturing+Inc&state=OH
company_name is required. state narrows the EPA and OSHA sections and city narrows EPA;
address and zip are accepted and echoed back but do not narrow anything on this endpoint
(on GET /risk-score they do). An exact EPA registry ID resolves the EPA section directly. That
ID is the FRS ID, which EPA’s Facility Registry Service assigns to uniquely identify a facility and
link its program records (ECHO’s All Media Programs Search Results Help);
ECHO’s facility search results show it in the FRS ID column, and a matched facility carries it as
facility.epa_registry_id.
The response has one section per agency. The osha, dol, fda and cpsc sections each carry
status, candidates, source (a manifest naming the source and how it was read), truncated
records_below_match_threshold and, when the check failed, error; osha and dol also carry
withheld_possible_individual and coverage_note. The epa section is shaped differently, because it resolves
one facility rather than listing candidates: resolved, facility, risk_score, risk_tier,
match_count, unreadable_inputs and note. Beside them: cross_agency_risk_score,
cross_agency_risk_tier, cross_agency_summary, incomplete_coverage, disclaimer and
generated_at.
A real response, captured 2026-09-17 19:01Z for a company that operates no EPA-regulated facility
(abridged: input also echoed address, city, zip and epa_id as null, each source also
carries name, url and a note, and cross_agency_summary and disclaimer are omitted here for
length). Values verbatim.
{
"input": {
"company_name": "Zzqxv Nonexistent Holdings LLC",
"state": "OH"
},
"epa": {
"resolved": false,
"facility": null,
"risk_score": null,
"risk_tier": null,
"match_count": null,
"unreadable_inputs": null,
"note": "No EPA-regulated facility found in EPA ECHO for a facility-name search on this company_name (narrowed by state and city when supplied) -- not every company operates an EPA-regulated facility."
},
"osha": {
"status": "no_match",
"candidates": [],
"source": {
"basis": "live",
"as_of": null
},
"truncated": [],
"records_below_match_threshold": 0,
"withheld_possible_individual": false,
"coverage_note": null
},
"dol": {
"status": "no_match",
"candidates": [],
"source": {
"basis": "live",
"as_of": null
},
"truncated": [],
"records_below_match_threshold": 0,
"withheld_possible_individual": false,
"coverage_note": null
},
"fda": {
"status": "no_match",
"candidates": [],
"source": {
"basis": "live",
"as_of": null
},
"truncated": [],
"records_below_match_threshold": 0
},
"cpsc": {
"status": "no_match",
"candidates": [],
"source": {
"basis": "live",
"as_of": null
},
"truncated": [],
"records_below_match_threshold": 0
},
"cross_agency_risk_score": 0,
"cross_agency_risk_tier": "low",
"incomplete_coverage": true,
"generated_at": "2026-09-17T19:01:43.023Z"
}
incomplete_coverage is true here because EPA did not resolve a facility — a zero score with
no resolved facility is not a clean bill of health, and the flag says so.
Coverage: five agency sections — EPA (ECHO), OSHA (OSHA’s own inspection and violation datasets, served through the Department of Labor’s open-data API since 2026-09-07), DOL (Wage & Hour and MSHA enforcement), openFDA (food, drug and device enforcement), and CPSC (saferproducts.gov recalls). openFDA and CPSC are keyless public sources behind a shared response cache, so a free government source is not re-queried on every call; EPA, OSHA and DOL are queried on each request. DOL does not cover EBSA, so nothing employee-benefit-related is in scope. This product uses the US Department of Labor API but is not endorsed or certified by the US Department of Labor.
openFDA’s enforcement pages say the data “is updated
weekly” and that recalls are included in FDA’s weekly Enforcement Report “once they are classified
according to the level of hazard involved”, so a recall FDA has not yet classified is not in the fda
section. FDA also “does not update the status of a recall after the recall has been classified”, so a
recall record’s status stays as published. In the osha section, an inspection with
violations_count 0 is not a clean result: it means OSHA’s violation dataset held no citation rows for
that inspection, not that OSHA stated none were issued.
Don’t take our word for which sections are live — the response tells you. Each agency
section carries its own source manifest with a basis field. If this page and a response ever
disagree, the response is the truth and we want to hear about it.
Each agency is queried independently: if one source is unavailable, that section comes back with
status: "error" and its own reason while the others still return real results — a failing agency
never takes down the whole screen, and is never quietly reported as a clean no_match. When a
source holds more records for the name than the screen reads, that section’s truncated list
names the dataset and the limit, and its counts are lower bounds.
A near miss is counted, not swallowed. A record the source returned whose name scored below this
API’s match floor is not shown — a short trading name can score below the floor against a longer
registered legal name — so each section reports how many it dropped in
records_below_match_threshold, and the summary says so. A no_match beside a non-zero count is
not a statement that nothing there resembled the name; re-screen with the full registered legal name.
Organizations only. A record is treated as an organization when its name carries a legal form or
a trade word. OSHA establishment names and DOL Wage and Hour and MSHA names include people — sole proprietors and labor contractors filed under their own names. A record whose listed
name may identify an individual is never returned, counted or scored. When one would have matched,
that section sets withheld_possible_individual to true and says so in coverage_note, which
names no one, and incomplete_coverage is true: a section that withheld a record is not a clean
result. The rule reads business forms and trade words in the name, so an organization listed only
by a brand name is withheld the same way.
incomplete_coverage is true whenever any section could not be checked, any section is
truncated, the OSHA or DOL section withheld a record that would have matched, or the EPA section did not resolve to exactly one facility whose name matches
company_name. When the name matches several facilities, only the first is scored and epa.note
says how many were not; when the one found is named differently, its score is shown but adds
nothing to cross_agency_risk_score. Send epa_id to name the facility yourself. Check
incomplete_coverage before treating a clean-looking result as a clean bill of health.
Entity resolution across agencies is deterministic name-similarity matching, the same as the
rest of this API, never an LLM judgment call.
What each agency section searches. DOL and OSHA are searched for the longest part of the company name left after removing legal-form suffixes and punctuation, in the name’s own case and in upper case (and, for a name with accents, also without them), and every row found is then compared with the full name you sent. A name typed all in lower case can miss Wage and Hour records, which DOL stores as filed. openFDA is searched by recalling firm, for the longest part of the name left after removing legal-form suffixes, without accents, and with and without an apostrophe; a recall where the company is the manufacturer but not the recalling firm is not found. CPSC is searched in its Manufacturer, Importer and Distributor fields for the longest part of the name left after removing legal-form suffixes and punctuation, so a recall that names the company only as a retailer is not found. When CPSC’s answer is larger than this screen reads, the name as you sent it is searched instead and the section is marked incomplete. CPSC’s search keeps accents, so a recall filed without them can be missed for a name typed with them.
How the EPA risk score is computed
The score is a fixed point rubric over ECHO’s own facility flags and counts. The model never sets it; it writes only the summary. Every request is scored by the same rubric.
| component | points |
|---|---|
| Current violation severity: a current Significant Violator 35, else currently in violation 20, else a violation in the last 3 years 8 | 0–35 |
| High Priority Violator or Significant Noncomplier flags: 5 per program flagged (CAA, CWA, RCRA, SDWA) | 0–15 |
| Formal enforcement actions: 4 each | 0–20 |
| Total penalties: more than $0 4, $10,000 or more 8, $100,000 or more 12, $500,000 or more 15 | 0–15 |
| Quarters in noncompliance, out of the 12 ECHO counts: proportional | 0–10 |
| Last formal action: within 2 years 5, within 5 years 2 | 0–5 |
In EPA’s terms, Significant Noncompliance (Clean Water Act and RCRA), High Priority Violation (Clean Air
Act) and Serious Violator (Safe Drinking Water Act) are “the most serious level of violation noted in EPA
databases” (ECHO FAQ). Of enforcement actions,
only formal ones score — EPA’s examples are “administrative orders, civil/judicial litigation” — while informal
ones, such as a “verbal notification”, are returned by GET /risk-score as total_informal_actions and
add no points. ECHO defines the windows: formal actions (FacFormalActionCount), total penalties
(FacTotalPenalties) and the last formal action date cover the last five years, and quarters in
noncompliance (FacQtrsWithNC) the last twelve quarters; its formal action and penalty counts include
only records entered in ICIS-Air, ICIS-NPDES and RCRAInfo
(All Media Programs Search Results Help,
last updated 2026-06-08).
Tiers: 0–19 low, 20–44 medium, 45–69 high, 70–100 critical. A flag or count ECHO left
blank adds no points and is named in unreadable_inputs; it is never read as “no”. A low score is
not a finding of compliance. In EPA’s words, “EPA cannot positively state that facilities without
violations shown in ECHO are necessarily fully compliant with environmental laws”
(ECHO FAQ, last updated 2023-10-10). ECHO
also “does not contain any corporate linkage data”, so a parent company’s other facilities are not
part of any score. EPA lists state-by-state reporting gaps on its
Known Data Problems page (last
updated 2026-08-18), and About the Data says: “Data for smaller facilities can vary widely (i.e.,
nonexistent, a partial representation, voluntarily entered by some states and not by others).” Nothing
from ECHO’s Toxics Release Inventory data or its Summary of Criminal Prosecutions is read.
If you believe an ECHO facility record is wrong, EPA takes corrections through the “Report Error” button
on the facility’s Detailed Facility Report, linked in responses as facility.detailed_report_url
(How to Report an Error).
Querying ECHO yourself? Nine traps in its services — errors that arrive as HTTP 200, filters ECHO silently ignores, a failed download that parses as rows — are written up with a runnable script: EPA ECHO API: Nine Traps in the Facility Search Services. The Department of Labor’s enforcement API has its own, from a key that only works in the URL to a full page that looks complete: The Department of Labor’s Open Data API: Eight Traps. Searching the recall sources by company has traps of its own: CPSC’s Recall API: Nine Traps and Searching openFDA Recalls by Company: Nine Traps.
Further reading: The guide What a screening API does not show you sets out the seven questions to ask of any screening response — including this one — and shows the same status in three different situations: a match with further entries disclosed below the floor, a no-match with the same disclosure, and a no-match where nothing close was read.