EPA Nonattainment Air Permit Screener — NSR/PSD Siting Risk
Pricing
from $4.40 / 1,000 results
EPA Nonattainment Air Permit Screener — NSR/PSD Siting Risk
EPA nonattainment air-permit screener. Batch point-in-polygon check of coordinates against 11 EPA Clean Air Act NAAQS pollutant standards: returns nonattainment/maintenance status, area name, classification (Marginal-Extreme), Federal Register citation, and NSR/PSD permitting-risk flag per site.
Pricing
from $4.40 / 1,000 results
Rating
0.0
(0)
Developer
Kyle Maloney
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
10 days ago
Last modified
Categories
Share
EPA Clean Air Act nonattainment screener. Give it a list of coordinates and it runs a server-side point-in-polygon check against 11 EPA National Ambient Air Quality Standards (NAAQS) pollutant layers — Ozone (1997/2008/2015 standards), Lead, SO₂, PM2.5 (three standards), PM10, CO and NO₂ — and returns, for each point: whether it sits inside an active Nonattainment area, an EPA-tracked Maintenance area, or an area designated under a revoked NAAQS; the matched area name(s); the classification severity (Marginal → Moderate → Serious → Severe-15 → Severe-17 → Extreme); the Federal Register designation citation and URL; the statutory attainment date; the published 10-year design-value series; and a structured NNSR-vs-PSD permitting pathway with the CAA §182 major-source threshold and offset ratio.
One clean, billable row per asset — including for a clear site outside every designated area, which is a useful and valid result.
This is a screening tool, not a permitting determination. It tells you which Clean Air Act preconstruction-review pathway a major stationary source would fall under at a coordinate. Actual applicability depends on source category, potential-to-emit and the approved State Implementation Plan. Confirm with the state permitting agency or EPA Regional Office.
What changed in v1.1 (and why it matters)
v1.0 shipped three defects that produced confident wrong answers on billable rows while the run reported SUCCESS. All are fixed; no field was removed or renamed.
- Severe areas ranked below Marginal.
worst_classificationrecognised only five exact strings. EPA never writes the bare word "Severe" — it writesSevere 15,Severe-15,Severe-17,Subpart 2/Severe 15. Every one of those scored 0. Houston and Dallas both reportedSeriouswhen they areSevere 15for the 2008 ozone NAAQS. Under CAA §182 that is the difference between a 50 tpy and a 25 tpy major-source threshold, and 1.2:1 vs 1.3:1 offsets. - A third of records fell through both status buckets.
current_statushas four values, not two: EPA also publishesNonattainment (NAAQS revoked)andMaintenance (NAAQS revoked), and 192 of the 614 screened records (31.3%) carry one. v1.0 matched on strict string equality, so those records setclear: falsewhile flagging nothing and emitting an emptynote. They now report throughin_revoked_standard_area,pollutants_revoked_standardandverdict: "revoked_standard_only". - A dead layer produced a billable "clear". A per-layer fetch failure was swallowed into "no match found". Every layer now reports
status_<layer>on the row, and any unanswered layer nullsclear,in_nonattainment,nsr_psd_permitting_flag,verdictandpermitting_pathwayrather than rendering them negative. If every asset fails, the run fails and nothing is billed.
The actor also now runs 26 live drift probes against the EPA service before producing a single row — layer numbering, the revoked-layer identity, per-layer record counts, the status and classification vocabularies, positive canaries (Houston, Los Angeles), a negative control (Wichita), coordinate-system magnitude and envelope, and a truncation guard read from the service's own maxRecordCount. If any probe fails, the run fails and nothing is billed.
Behaviourally breaking for existing pipelines (read this if you automate on v1.0 output)
All three fixes above correct wrong answers, and no field was removed or renamed. Two of them nevertheless change values you may be keying on, so a downstream step that does string equality or a boolean filter will behave differently on the same coordinate:
| Field | v1.0 | v1.1 | Example |
|---|---|---|---|
worst_classification | Serious | Severe 15 | Dallas TX (32.7767, -96.7970) and Houston TX (29.7604, -95.3698), 2008 ozone NAAQS. v1.0 scored every Severe* string as 0, i.e. below Marginal. |
in_nonattainment | true | false | Clarksville TN (36.5298, -87.3595). The area is designated only under a revoked NAAQS, so it is not an active nonattainment area for new-source review. It now reports in_revoked_standard_area: true and verdict: "revoked_standard_only". |
Both new values are the correct ones. If you match on classification strings,
switch to worst_classification_tier (a normalised, sortable vocabulary) or
worst_classification_rank (numeric) instead of the raw EPA string, which EPA
writes several ways for the same tier. If you filter on in_nonattainment, add
in_revoked_standard_area to keep the anti-backsliding cases you used to catch
by accident.
Also note the doctrine contract now in force on every screen-integrity field:
null means not checked, false means checked and negative. A filter
written as !row.in_nonattainment will now also pass rows where the screen was
incomplete. Use row.in_nonattainment === false.
Who it's for
- Air-permit / NSR-PSD consultants — screen a shortlist of candidate sites for nonattainment exposure before scoping a permit application, with the classification tier and offset ratio already resolved.
- Industrial, power-plant and data-center siting teams — flag the single biggest hidden air-permitting cost gate before committing to a location, including sites just outside a boundary.
- EHS and compliance teams — batch-check an existing facility portfolio against every current and revoked NAAQS designation, with the Federal Register citation for each.
- EPCs and project developers — screen a multi-site portfolio for classification severity, offset requirements and redesignation risk in one call.
- Phase-I ESA / environmental due-diligence — add an authoritative nonattainment data point to a site assessment.
- GIS & data teams / AI agents — enrich any coordinate list with EPA nonattainment status without hosting the polygon layers yourself.
Example input
{"assets": [{ "lat": 29.7604, "lon": -95.3698, "label": "Houston, TX" },{ "lat": 34.0522, "lon": -118.2437, "label": "Los Angeles, CA" },{ "lat": 36.4971, "lon": -87.3829, "label": "Clarksville, TN" },{ "lat": 33.8659, "lon": -84.4032, "label": "Atlanta, GA" },{ "lat": 29.7000, "lon": -96.3500, "label": "Colorado County, TX" },{ "lat": 37.6872, "lon": -97.3301, "label": "Wichita, KS" }],"includeMaintenance": true,"nearbyRadiusKm": 40,"maxAssets": 500}
| Site | verdict | worst_classification | nsr_major_source_threshold_tpy | permitting_pathway |
|---|---|---|---|---|
| Houston, TX | nonattainment | Severe 15 | 25 | NNSR |
| Los Angeles, CA | nonattainment | Extreme | 10 | NNSR |
| Clarksville, TN | revoked_standard_only | null | null | PSD |
| Atlanta, GA | maintenance_only | null | null | PSD |
| Colorado County, TX | clear | null | null | PSD |
| Wichita, KS | clear | null | null | PSD |
Colorado County is the interesting row: it is genuinely clear, but with nearbyRadiusKm: 40 it reports near_boundary_flag: true and nearest_area_distance_km: 26.73 to the Houston-Galveston-Brazoria boundary -- a redesignation-risk signal a pure in/out check would miss.
Input options
| Field | Type | Default | Meaning |
|---|---|---|---|
assets | array | required | [{ lat, lon, label }] in WGS84 decimal degrees. One billable row per asset. |
pollutants | array | [] (all 11) | Restrict to specific standard keys, e.g. ["ozone_8hr_2015","pm25_annual_2012"]. Excluded standards report status_<key>: "not_queried". |
includeMaintenance | boolean | true | Set false to report only nonattainment designations and drop every maintenance match, including revoked-NAAQS maintenance areas. |
nearbyRadiusKm | integer | 0 | Set above 0 to also report designated areas within N km that the point is outside of. Populates the nearby_* fields; leave at 0 and they read null (= not checked). |
maxAssets | integer | 500 | Safety cap, max 2000. |
Valid pollutants keys: ozone_8hr_1997, ozone_8hr_2008, ozone_8hr_2015, lead_2008, so2_1hr_2010, pm25_24hr_2006, pm25_annual_1997, pm25_annual_2012, pm10_1987, co_1971, no2_1971.
Output fields — one row per asset
Identity
| Field | Type | Description |
|---|---|---|
asset_label | string | Your label, or asset_N if omitted. |
asset_lat | number | Screened latitude (WGS84). |
asset_lon | number | Screened longitude (WGS84). |
checked_at | string | UTC timestamp of the run. |
source_url | string | The EPA OAR FeatureServer used. |
disclaimer | string | Informational-use disclaimer. |
Headline verdict
| Field | Type | Description |
|---|---|---|
verdict | string | nonattainment · maintenance_only · revoked_standard_only · clear. null whenever any contributing layer did not answer — it can never read clear on an incomplete screen. |
in_nonattainment | boolean | Inside an active nonattainment area under a currently enforceable NAAQS. false = checked and negative; null = not checked. |
in_maintenance | boolean | Inside an EPA maintenance-plan area under a currently enforceable NAAQS. |
in_revoked_standard_area | boolean | Inside an area designated under a revoked NAAQS. Not enforced for new-source review, but SIP anti-backsliding obligations may still apply. |
clear | boolean | Outside every designated area for every screened standard. |
nsr_psd_permitting_flag | boolean | True when in_nonattainment is true. |
note | string | Plain-English one-line summary. Never empty on a billable row. |
Classification severity
| Field | Type | Description |
|---|---|---|
worst_classification | string | The raw EPA string of the most severe active nonattainment match, e.g. Severe 15. |
worst_classification_tier | string | Normalised tier — Transitional, Sub-marginal, Marginal, Moderate, Serious, Severe, Severe-15, Severe-17, Extreme. Sort and filter on this. |
worst_classification_rank | number | Severity 0.25–5 (Marginal 1, Moderate 2, Serious 3, Severe-15 4, Severe-17 4.5, Extreme 5). |
worst_classification_pollutant | string | Which standard produced the worst classification. |
worst_classification_area | string | Which EPA area produced it. |
worst_classification_basis | string | One-sentence audit trail for the headline severity. |
Permitting implication (informational)
| Field | Type | Description |
|---|---|---|
permitting_pathway | string | NNSR (Nonattainment New Source Review) or PSD (Prevention of Significant Deterioration). |
permitting_pathway_basis | string | Why that pathway was reported. |
nsr_major_source_threshold_tpy | number | CAA §182 major stationary source threshold in tpy of VOC/NOx for the worst ozone classification (Marginal/Moderate 100, Serious 50, Severe 25, Extreme 10). null for non-ozone pollutants, which have no statutory tier table. |
nsr_offset_ratio | string | CAA §182 minimum offset ratio (1.1:1 → 1.5:1). The SIP may require more. |
nsr_threshold_basis | string | Which ozone classification and area the threshold came from. §182 tiers are ozone-only while worst_classification covers every pollutant, so when they disagree — Phoenix is Serious for PM-10 but Moderate for ozone, giving 100 tpy — this field says so instead of leaving two numbers to be reconciled. |
Designations found
| Field | Type | Description |
|---|---|---|
pollutants_nonattainment | array | EPA pollutant_name of every active nonattainment standard hit. |
pollutants_maintenance | array | Every active maintenance standard hit. |
pollutants_revoked_standard | array | Every revoked-NAAQS standard hit. |
matched_area_count | integer | Distinct designated polygons containing the point. |
active_designation_count | integer | Of those, how many are under a currently enforceable NAAQS. |
revoked_designation_count | integer | Of those, how many are under a revoked NAAQS. |
matched_areas | array | Full 52-field detail per match — see below. |
Screen integrity (doctrine: null = not checked, false = checked and negative)
| Field | Type | Description |
|---|---|---|
screen_complete | boolean | True only when every requested layer answered. |
layers_queried_count | integer | Layers screened for this asset (0 for an invalid coordinate). |
layers_ok_count | integer | Layers that returned a usable response. |
layers_failed_count | integer | Layers that did not answer. Any value above 0 nulls every verdict field. |
layers_failed | array | Keys of the failed layers. |
layers_failed_detail | array | key:outcome strings. |
error | string | Message for an invalid coordinate or an incomplete screen. |
error_kind | string | invalid_input or upstream_unavailable. |
status_ozone_8hr_1997 | string | Per-layer outcome: ok · unavailable · not_queried. |
status_ozone_8hr_2008 | string | Per-layer outcome. |
status_ozone_8hr_2015 | string | Per-layer outcome. |
status_lead_2008 | string | Per-layer outcome. |
status_so2_1hr_2010 | string | Per-layer outcome. |
status_pm25_24hr_2006 | string | Per-layer outcome. |
status_pm25_annual_1997 | string | Per-layer outcome. |
status_pm25_annual_2012 | string | Per-layer outcome. |
status_pm10_1987 | string | Per-layer outcome. |
status_co_1971 | string | Per-layer outcome. |
status_no2_1971 | string | Per-layer outcome. |
Near-boundary (populated only when nearbyRadiusKm > 0)
| Field | Type | Description |
|---|---|---|
nearby_radius_km | number | The radius used. null = the proximity pass was not requested. |
nearby_area_count | integer | Designated areas within the radius that the point is outside of. |
nearest_area_distance_km | number | Distance to the nearest such boundary, from geometry generalized to ~0.002° (≈200 m). |
nearest_area_name | string | EPA name of that nearest area. |
nearest_area_pollutant | string | Standard under which it is designated. |
near_boundary_flag | boolean | True when at least one area is within the radius. |
nearby_areas | array | Same 52-field detail as matched_areas, sorted nearest first, each with contains_site: false and distance_km. |
Inside matched_areas / nearby_areas — 52 fields per match
pollutant_key, pollutant, area_name, state_name, status (raw EPA string), status_normalized (Nonattainment/Maintenance), status_recognized, naaqs_revoked, classification (raw), classification_tier, classification_rank, classification_recognized, naaqs_standard, layer_id, objectid, composite_id, epa_region, epa_region_office, designation_citation, designation_url, designation_pub_date, designation_effective_date, statutory_attainment_date, classification_citation, classification_effective_date, classification_url, design_value, design_value_units, design_value_series (the published 10-year series), design_value_prior_year, design_value_trend (improving/worsening/flat), original_design_value, original_design_value_24hr, original_design_value_years, clean_data_determination_effective_date, clean_data_determination_citation, clean_data_determination_url, attainment_determination_effective_date, attainment_determination_citation, attainment_determination_url, redesignation_proposed_pub_date, redesignation_effective_date, redesignation_citation, redesignation_url, projected_redesignation_date, population_2010, population_2020, population_current, nsr_major_source_threshold_tpy, nsr_offset_ratio, contains_site, distance_km.
A single point can match more than one standard — Los Angeles hits four. The flat row rolls up the worst classification and every pollutant name; matched_areas lists each match individually.
Which inputs populate the sparser fields
Some EPA fields are genuinely sparse. These coordinates populate them, and each is pinned by an offline fixture so it cannot silently rot:
| Field | Populating input | What you get |
|---|---|---|
attainment_determination_* | Atlanta, GA 33.8659, -84.4032 | DAA 87 FR 60897, effective 2022-11-07 (live non-null rate 30.6%) |
projected_redesignation_date | Chicago, IL 41.8781, -87.6298 | 2022-09-30 under the 2008 ozone NAAQS. EPA writes 2001-01-01 as a null placeholder on 746 of 792 records (94.2%); this actor maps that sentinel to null rather than emitting a fabricated date |
clean_data_determination_* | Los Angeles, CA 34.0522, -118.2437 | CDD records on the PM2.5 designations (live non-null rate 25.4%) |
nearby_* | Colorado County, TX 29.7000, -96.3500 with nearbyRadiusKm: 40 | 3 nearby areas, nearest 26.73 km |
error, error_kind, layers_failed, layers_failed_detail | (none — by design) | These are null on every healthy run. That is good news, not a dead column: they populate only when a coordinate is unusable or an EPA layer does not answer |
Pricing
$8 per 1,000 result rows (Pay-per-event, one automatic result event per asset row). A 250-site portfolio screen across all 11 NAAQS standards costs $2.00. Graduated paid-plan discounts apply automatically (Bronze −20% → Diamond −70%). Failed runs bill nothing: the drift gate and the billing gate both fire before any row is written.
Use as an MCP tool
This actor is callable by AI agents (Claude, Cursor, etc.) via mcp.apify.com. Its typed input (assets[]{lat,lon,label}) and field-level output descriptions make it a clean, chainable "is this coordinate in an EPA nonattainment area, and does it trigger NNSR instead of PSD?" tool for siting, permitting and environmental-diligence agents. Agents should read verdict and screen_complete together — a null verdict means the screen was incomplete, not that the site is clear.
FAQ
Is my site in an EPA nonattainment area? Pass its coordinate and read verdict, in_nonattainment, pollutants_nonattainment and matched_areas.
Does my project need Nonattainment New Source Review? permitting_pathway reports NNSR whenever the point is inside an active nonattainment area for any screened pollutant, and nsr_major_source_threshold_tpy / nsr_offset_ratio give the CAA §182 numbers for that classification. This is a screen: NNSR applies only to major new or modified stationary sources, so applicability also depends on source category and potential-to-emit, and the approved SIP may be more stringent. Confirm with the state permitting agency.
What does the classification mean? For pollutants with tiered classifications (mainly ozone, plus some PM standards), it drives the major-source threshold and the offset ratio. EPA publishes compound values such as Severe 15 and Subpart 2/Moderate; worst_classification returns the string verbatim and worst_classification_tier / worst_classification_rank give a stable sortable form. Lead, SO₂, NO₂ and CO have no tiered classification — worst_classification is null there even when in_nonattainment is true, and worst_classification_basis says so.
What is a "revoked NAAQS" designation? EPA revoked the 1-hour ozone (1979), 8-hour ozone (1997) and, for most areas, the annual PM2.5 (1997) standards. Their designations remain published with a (NAAQS revoked) status — 31.3% of the records this actor screens. They do not drive new-source review, but Clean Air Act anti-backsliding obligations in the SIP can survive revocation. They are reported separately in in_revoked_standard_area and pollutants_revoked_standard and never counted as enforceable nonattainment.
Nonattainment vs maintenance? Nonattainment means the area currently fails the NAAQS. Maintenance means it has attained the standard but remains under an EPA-approved maintenance plan, so a redesignation-to-nonattainment risk still exists. Set includeMaintenance: false to screen only active nonattainment.
Why is clear sometimes null instead of false? Because the screen was incomplete. null means "not checked" and false means "checked and negative" — a layer that never answered cannot rule anything out, so the actor refuses to render a negative it did not verify. Check screen_complete, layers_failed and error_kind.
How current is this data? The EPA OAR source layer is refreshed weekly from an internal OAQPS database and is the same authoritative source behind EPA's Green Book. Every run re-reads the service metadata and re-verifies the layer set, record counts and vocabularies before screening.
Related actors
- EPA GHGRP Emitter Screener — reported greenhouse-gas emissions by facility and sector, for the same siting and permitting workflow.
- EPA Contaminated Site Screener — Superfund, RCRA, USTs and ECHO enforcement around a coordinate, for Phase I ESA.
- Site Due Diligence Bundle — 20-layer federated screen for one coordinate, including this air-nonattainment layer, in a single call.
Source
EPA Office of Air and Radiation (OAR) Nonattainment Areas and Designations ArcGIS FeatureServer — 12 polygon layers, of which 11 are screened. Layer 10 (1-hour ozone, 1979) is excluded because its layer name declares the standard revoked; note that layers 0 and 6 also carry revoked-status records, which are reported per record rather than dropped.
https://services.arcgis.com/cJ9YHowT8TU7DUyn/arcgis/rest/services/Nonattainment_Areas_and_Designations/FeatureServer