# OSHA Inspections & Citations Scraper - Penalties by Company (`groundplanes/osha-inspection-citations`) Actor

OSHA inspections joined with citations from the official DOL API: company, site address, NAICS, inspection type, open and close dates, serious, willful and repeat counts, initial and current penalties and cited standards, newest first. Filter by state, industry and penalty.

- **URL**: https://apify.com/groundplanes/osha-inspection-citations.md
- **Developed by:** [John](https://apify.com/groundplanes) (community)
- **Categories:** Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.10 / 1,000 inspections

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

## OSHA Inspections & Citations Scraper - Penalties by Company

Fetch recent federal and state-plan OSHA workplace safety inspections joined with their cited violations, penalty amounts (`initialPenaltyUsd` and `currentPenaltyUsd`), gravity counts (`Serious`, `Willful`, `Repeat`), and human-readable CFR safety standards via the official U.S. Department of Labor (DOL) Open Data API v4.

### What it does

The Occupational Safety and Health Administration (OSHA) conducts tens of thousands of workplace inspections each year across construction, manufacturing, warehousing, and industrial facilities. In the raw DOL database, inspection metadata (`OSHA/inspection`) and individual violation citations (`OSHA/violation`) live in separate endpoints keyed by `activity_nr`, and safety standards are stored as unformatted 8-character strings (e.g. `19260501`).

This Actor:

- Queries `https://apiprod.dol.gov/v4/get/OSHA/inspection/json` filtered by worksite state (`states`), NAICS industry prefix (`naicsPrefix`), and opening date (`openDateFrom`).
- Batch-fetches matching violation records from `https://apiprod.dol.gov/v4/get/OSHA/violation/json` by `activity_nr`.
- Aggregates violation counts (`violationsCount`, `seriousCount`, `willfulCount`, `repeatCount`), sums `initialPenaltyUsd` and `currentPenaltyUsd`, and maps the 30 most-cited OSHA standards to readable labels (e.g. `19260501` → `"1926.501 Fall Protection"`, `19100147` → `"1910.147 Control of Hazardous Energy (Lockout/Tagout)"`).

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

### Who it's for

- **Environmental Health & Safety (EHS) consulting and training firms** identifying establishments that recently underwent planned, complaint, or referral inspections.
- **Industrial safety equipment and PPE distributors** matching cited CFR standards (fall protection, respiratory protection, scaffolding, machine guarding, lockout/tagout) to safety hardware needs.
- **Commercial insurance underwriters and workers' compensation brokers** evaluating workplace safety history by NAICS and geography.
- **Workplace safety and labor researchers** analyzing penalty trends and enforcement activity.

### Input

- **`states`** *(array of strings)* — Two-letter US state codes for the inspected worksite. Default: `["AZ", "TX"]`. Leave empty (`[]`) for all states.
- **`naicsPrefix`** *(string)* — NAICS industry code prefix (e.g. `"23"` for Construction, `"31"`–`"33"` for Manufacturing). Default: `"23"`. Leave empty (`""`) for all industries.
- **`openDateFrom`** *(string, YYYY-MM-DD)* — Earliest inspection `open_date` to include. Defaults to 365 days.
- **`minCurrentPenaltyUsd`** *(integer)* — Minimum summed `currentPenaltyUsd` per inspection (`0` includes recent inspections even before citations are issued). Default: `0`.
- **`violationTypes`** *(array of strings)* — Optional filter for violation gravity (`"Serious"`, `"Willful"`, `"Repeat"`, `"Other-than-serious"`). Default: `[]`.
- **`onlyWithCitations`** *(boolean)* — Default `true`. Skips inspections with no citation items issued yet (citations usually follow the inspection by weeks or months).
- **Partial results:** if the time budget runs out, the DOL API keeps answering 429, the 4,000-inspection safety cap is reached, the source feed looks stale or drifted, or the max charge limit stops the push, the run still returns what it fully checked and the status message starts with **PARTIAL** and gives the reason. When the reason is on the source side (rate limit, schema drift, stale feed, excluded invalid rows), the message also ends with: If this keeps happening, please open an issue on this actor's Issues tab and we'll fix it fast. A PARTIAL caused only by your own limits (max charge, time budget, safety caps) omits that line — raising the limit is the fix there, not a bug report.
- **`maxScanSeconds`** *(integer)* — Time budget, default `75`. The DOL API is rate-limited; raise this and the run timeout for large pulls.
- **`useApifyProxy`** *(boolean)* — Default `false` (opt-in). The DOL API throttles some shared cloud IPs, including Apify's, and can answer HTTP 429 from the first request. If that happens, turn this on. It uses Apify's shared datacenter proxy on your account: every Apify plan includes it (the Free plan has 5 shared IPs), and its data transfer counts toward your platform usage.
- **`onlyOpenCases`** *(boolean)* — Keep only inspections where `closeCaseDate` is `null`. Default: `false`.
- **`dolApiKey`** *(string, secret, optional)* — Your own free DOL Open Data API v4 key from [dataportal.dol.gov](https://dataportal.dol.gov/registration). If left blank, the Actor reads the `DOL_API_KEY` environment secret configured on the Actor; if neither is set, the Actor fails with a clear message saying so.
- **`maxItems`** *(integer)* — Maximum inspection rows to return. Default: `100`.

#### Example input JSON

```json
{
  "states": ["AZ", "TX"],
  "naicsPrefix": "23",
  "openDateFrom": "2026-07-25",
  "minCurrentPenaltyUsd": 0,
  "violationTypes": [],
  "onlyOpenCases": false,
  "maxItems": 200
}
```

#### Canary input (quick health check)

```json
{
  "states": ["AZ"],
  "naicsPrefix": "23",
  "maxItems": 5
}
```

A healthy canary returns 5 rows and ends `Complete: 5 rows`. It ends **PARTIAL** only with a stated source-side reason — for example, if DOL has not posted any inspection for the query in over 60 days, or the API rate-limited the scan. Citation lag alone never makes a run PARTIAL: freshness is measured on the newest inspection DOL has posted for your query, before citation filters are applied (see the FAQ below). A canary should never end in an unexplained failure.

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

Set a default maximum charge of **$50** (`"maxTotalChargeUsd": 50`) in your run options:

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

### Output

Results are deduplicated by `activityNumber` and sorted newest first by `openDate` descending.

| Field | Type | Description |
|---|---|---|
| `activityNumber` | `string` | Unique OSHA inspection activity number (`activity_nr`). |
| `establishmentName` | `string \| null` | Name of the inspected business establishment. |
| `siteStreet` | `string \| null` | Inspected worksite street address. |
| `siteCity` | `string \| null` | Worksite city. |
| `siteState` | `string \| null` | Worksite 2-letter state code. |
| `siteZip` | `string \| null` | Worksite postal ZIP code. |
| `naicsCode` | `string \| null` | 6-digit NAICS industry code. |
| `inspectionType` | `string \| null` | Decoded inspection trigger (`Planned`, `Complaint`, `Referral`, `Accident`, `Follow-Up`, etc.). |
| `scope` | `string \| null` | Decoded inspection scope (`Complete`, `Partial`, `Records Only`, `No Inspection`). |
| `openDate` | `string \| null` | Date the inspection opened (`YYYY-MM-DD`). |
| `closeCaseDate` | `string \| null` | Date the case closed (`YYYY-MM-DD`) or `null` if still open. |
| `unionStatus` | `string \| null` | Worksite union representation status (`Union` or `Non-Union`). |
| `ownerType` | `string \| null` | Sector classification (`Private`, `Local Government`, `State Government`, `Federal Government`). |
| `violationsCount` | `integer` | Total cited violations joined to this inspection. |
| `seriousCount` | `integer` | Count of Serious (`S`) violations. |
| `willfulCount` | `integer` | Count of Willful (`W`) violations. |
| `repeatCount` | `integer` | Count of Repeat (`R`) violations. |
| `initialPenaltyUsd` | `number` | Summed initial proposed penalty in USD. |
| `currentPenaltyUsd` | `number` | Summed current penalty in USD (reflects latest post-settlement/contest adjustments). |
| `citedStandards` | `array<string>` | Human-readable CFR safety standards cited on the inspection. |
| `inspectionUrl` | `string \| null` | OSHA IMIS inspection detail URL for interactive browser lookup. |

#### Sample output row (synthetic example)

```json
{
  "activityNumber": "1999999",
  "establishmentName": "EXAMPLE STRUCTURAL BUILDERS LLC",
  "siteStreet": "1200 W INDUSTRIAL PKWY",
  "siteCity": "PHOENIX",
  "siteState": "AZ",
  "siteZip": "85009",
  "naicsCode": "236220",
  "inspectionType": "Planned",
  "scope": "Partial",
  "openDate": "2026-10-12",
  "closeCaseDate": null,
  "unionStatus": "Non-Union",
  "ownerType": "Private",
  "violationsCount": 2,
  "seriousCount": 2,
  "willfulCount": 0,
  "repeatCount": 0,
  "initialPenaltyUsd": 24800.0,
  "currentPenaltyUsd": 17360.0,
  "citedStandards": [
    "1926.501 Fall Protection",
    "1926.1053 Ladders"
  ],
  "inspectionUrl": "https://www.osha.gov/ords/imis/establishment.inspection_detail?id=1999999"
}
```

### How pricing works

Pay per result. You only pay for rows returned. Set `"maxTotalChargeUsd": 50` in your run options to cap spend automatically.

### Data source and terms

- **Official source:** [U.S. Department of Labor Open Data Portal (dataportal.dol.gov)](https://dataportal.dol.gov/).
- **Important context on penalties and citations:**
  - OSHA citations and proposed penalties are often reduced or modified after an informal settlement conference with the OSHA Area Director, or may still be under formal contest before the Occupational Safety and Health Review Commission (OSHRC). `currentPenaltyUsd` reflects the latest value recorded in the DOL enforcement feed.
  - Because citations can take several weeks after `openDate` to be issued and posted, very recent inspections may show `violationsCount: 0` and `currentPenaltyUsd: 0`.
- **Affiliation disclaimer:** This Actor is an independent tool and is **not affiliated with or endorsed by the Occupational Safety and Health Administration (OSHA) or the U.S. Department of Labor (DOL)**.

### Compliance

This Actor outputs business establishment names and physical worksite addresses published by the U.S. Department of Labor (no personal contact emails or phone numbers are included). Buyers are responsible for complying with the Telephone Consumer Protection Act (TCPA), the National Do-Not-Call Registry, the CAN-SPAM Act, and all applicable federal and state privacy laws before any business outreach. Do not use this Actor for consumer or individual targeting.

### Troubleshooting

- **"A DOL API key is required"** — Neither the `DOL_API_KEY` environment secret nor the `dolApiKey` input was set. Add one of them (see FAQ below) and re-run.
- **"DOL API rejected the API key (HTTP 401/403)"** — The configured key is invalid or revoked. Register or regenerate a key at dataportal.dol.gov and update the secret/input.
- **Status starts with PARTIAL** — The run returned everything it could fully verify and stopped for the stated reason (rate limit, time budget, safety cap, stale or drifted feed, or the max charge limit). Re-run with a narrower window or a higher budget/limit.
- **"returned no matching inspections"** — The filters matched nothing in the DOL feed. Widen `openDateFrom`, `states`, or `naicsPrefix`.

### Limits and FAQ

- **How do I get a free DOL API key?**
  Register for a free account at [dataportal.dol.gov](https://dataportal.dol.gov/registration) and paste your API key into the `dolApiKey` input, or configure the `DOL_API_KEY` environment secret on the Actor. If neither is set, the run fails with instructions; there is no other built-in key.
- **Why do some recent inspections have 0 violations?**
  Under the OSH Act, OSHA has up to 6 months from an inspection opening date to issue citations. Keeping `minCurrentPenaltyUsd: 0` returns newly opened inspections immediately; raising `minCurrentPenaltyUsd` (e.g. `5000`) filters for inspections where penalty citations have already posted.
- **How is "stale source" judged?**
  Freshness is measured on the newest inspection DOL has posted for your query — before the `onlyWithCitations`, penalty, and violation-type filters are applied. Because citations trail inspections by weeks to months, the newest *cited* inspection is routinely old even when the feed is perfectly current, so it is never used for the freshness check. Only if DOL has posted nothing for the query in over 60 days does the run end PARTIAL with a "source data may be stale" note.

### Changelog

- **v0.3 (2026-10-26):** Freshness is now measured on the newest inspection DOL has posted for the query (before citation filters), so normal citation lag no longer makes every run PARTIAL-stale; the stale threshold is 60 days. The Issues call-to-action is only appended to source-caused PARTIALs, not to PARTIALs caused by your own limits (max charge, time budget, safety caps). Runs now fail when more than 20% of collected rows fail output validation. The schema-drift sentinel also catches value-level drift (a mapped field present but null/empty across a page, e.g. every `estab_name` null), with a stricter threshold on identity fields. More robust source parsing: `delete_flag` is read as deleted only for explicit affirmative values, violation gravity accepts full-word encodings ("Serious", "Willful", "Repeat") as well as the S/W/R codes, non-numeric penalty values are counted and reported instead of silently summed as $0, duplicate citation rows are deduplicated, the citation pagination cap notes itself, and the time budget is enforced during citation fetching too. Non-string inputs now fail with a clear message instead of crashing.
- **v0.2 (2026-10-26):** The Actor now reads the `DOL_API_KEY` environment secret (the per-run `dolApiKey` input remains as an optional fallback); v0.1.6 as shipped required the input key and ignored the secret. Added per-row output schema validation with counted exclusions, a schema-drift sentinel on both DOL endpoints, a freshness check, the verbatim Issues call-to-action on PARTIAL statuses (including the charge-limit exit, now a proper PARTIAL), and an honest fail (instead of "no matches") when collected inspections must be dropped.
- **v0.1 (2026-10-23):** Initial release with DOL Open Data API v4 inspection/violation join and Top-30 CFR standard label mapping.

# Actor input Schema

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

Two-letter US state codes for the inspected worksite (e.g. \['AZ', 'TX']). Leave empty for all states.

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

NAICS industry code prefix (e.g. '23' for Construction, '31'–'33' for Manufacturing). Leave empty for all industries.

## `openDateFrom` (type: `string`):

Start of inspection open_date window in YYYY-MM-DD format. Leave empty for today minus 365 days.

## `minCurrentPenaltyUsd` (type: `integer`):

Minimum total current penalty in USD across all violations on the inspection. Set to 0 to include all inspections (even before citations post).

## `violationTypes` (type: `array`):

Optional filter to keep only inspections containing at least one violation of these gravity types.

## `onlyOpenCases` (type: `boolean`):

When true, keeps only inspections where close_case_date is null.

## `onlyWithCitations` (type: `boolean`):

Skip inspections where OSHA has not (yet) issued any citation items. Citations are often issued weeks or months after the inspection opens.

## `dolApiKey` (type: `string`):

Optional. If blank, the Actor uses the DOL_API_KEY environment secret configured on the Actor. Otherwise paste your own free key from https://dataportal.dol.gov/registration. If neither is set, the run fails with instructions.

## `maxScanSeconds` (type: `integer`):

Stop reading new pages of inspections after this many seconds and return what was collected. The DOL API is rate-limited, so raise this (and the run timeout) for large pulls.

## `useApifyProxy` (type: `boolean`):

Off by default. Turn on if the DOL API answers HTTP 429 from the first request; it throttles some shared cloud IPs, including Apify's. Uses Apify's shared datacenter proxy, which runs on the run's account: included on every Apify plan (the Free plan has 5 shared IPs), but its data transfer counts toward your platform usage.

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

Maximum number of inspection records to return.

## Actor input object example

```json
{
  "states": [
    "AZ",
    "TX"
  ],
  "naicsPrefix": "23",
  "minCurrentPenaltyUsd": 0,
  "violationTypes": [],
  "onlyOpenCases": false,
  "onlyWithCitations": true,
  "maxScanSeconds": 75,
  "useApifyProxy": false,
  "maxItems": 100
}
```

# 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 = {
    "states": [
        "AZ",
        "TX"
    ],
    "naicsPrefix": "23",
    "minCurrentPenaltyUsd": 0,
    "violationTypes": [],
    "onlyOpenCases": false,
    "maxItems": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("groundplanes/osha-inspection-citations").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": [
        "AZ",
        "TX",
    ],
    "naicsPrefix": "23",
    "minCurrentPenaltyUsd": 0,
    "violationTypes": [],
    "onlyOpenCases": False,
    "maxItems": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("groundplanes/osha-inspection-citations").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": [
    "AZ",
    "TX"
  ],
  "naicsPrefix": "23",
  "minCurrentPenaltyUsd": 0,
  "violationTypes": [],
  "onlyOpenCases": false,
  "maxItems": 100
}' |
apify call groundplanes/osha-inspection-citations --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,groundplanes/osha-inspection-citations"
        }
    }
}
```

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/BQLfkQyaDdXXPDZ1V/builds/HjmUd7cwjoxNRRNkz/openapi.json
