# NYC Restaurant Inspection Data Scraper (DOHMH Grades JSON) (`steadfetch/nyc-restaurant-inspection-scores`) Actor

Scrape official NYC Open Data restaurant inspections (DOHMH): one row per inspection and violation with score, grade, action, violation codes and descriptions, borough, cuisine, address and coordinates. Filter by date, borough, cuisine or grade. Establishment data only, no personal contact fields.

- **URL**: https://apify.com/steadfetch/nyc-restaurant-inspection-scores.md
- **Developed by:** [Steadfetch](https://apify.com/steadfetch) (community)
- **Categories:** Lead generation, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 inspection row delivereds

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

## NYC Restaurant Inspection Data Scraper (DOHMH Grades JSON)

Extract official New York City restaurant inspection results from the [NYC Open Data Socrata API](https://data.cityofnewyork.us/resource/43nn-pn8j.json). Structured JSON with inspection dates, scores, grades, violation codes and establishment location. No browser, no login, no CAPTCHA.

### What it does

The Actor queries the official DOHMH dataset **DOHMH New York City Restaurant Inspection Results** (Socrata resource `43nn-pn8j`) and returns one dataset item per inspection or violation row.

- **Date window:** `since` and `until` filter on `inspection_date`. Default is the last 30 days.
- **Location and type filters:** borough, cuisine and grade (A, B or C), all applied server-side with SoQL.
- **Normalized output:** scores and coordinates become numbers, dates become `YYYY-MM-DD`, and missing values become `null`.
- **Deduplication:** key is `camis` + `inspection_date` + `violation_code`, both inside a run and across monitor runs.
- **Monitor mode:** with `mode: "monitor"` the Actor persists delivered keys in its key-value store and only emits rows that were never delivered before for the same borough, cuisine and grade scope.
- **Schema validation:** every item is validated against `.actor/dataset_schema.json` before it reaches the dataset. With `strictSchema: true` (the default) an invalid item fails the run.

**No charge for noise:** errors, retries, duplicate rows, rows already delivered by a monitor run and the source placeholder date `1900-01-01` are never pushed, so they are never billed.

#### Source, licence and privacy

- **Source:** official NYC Open Data API, dataset published by the NYC Department of Health and Mental Hygiene (DOHMH). It is keyless and updated daily by the City.
- **Terms:** re-read on 2026-10-20. The NYC Open Data home page links to the [NYC.gov Terms of Use](https://www.nyc.gov/main/terms-of-use), which require lawful use and do not prohibit lawful automated access to public data; the Socrata metadata for this dataset has no separate licence field. The Actor attributes DOHMH and links the public source URL on every row. Store publication keeps the CTO sign-off item in `PUBLISHING.md`.
- **Privacy:** establishment data only. The Actor never selects the source `phone` column (`$select` excludes it), and no phone, email, officer or other personal contact field is published. No personal data is collected.

### Example output

```json
{
    "url": "https://data.cityofnewyork.us/resource/43nn-pn8j.json?camis=40393413",
    "title": "V & S PIZZERIA OF SHEAPSHEAD BAY",
    "scrapedAt": "2026-10-20T08:18:21.885Z",
    "camis": "40393413",
    "business_name": "V & S PIZZERIA OF SHEAPSHEAD BAY",
    "borough": "Brooklyn",
    "building": "1723",
    "street": "EMMONS AVENUE",
    "zipcode": "11235",
    "cuisine_description": "Pizza",
    "inspection_date": "2026-10-17",
    "action": "Violations were cited in the following area(s).",
    "violation_code": "10F",
    "violation_description": "Non-food contact surface or equipment made of unacceptable material, not kept clean, or not properly sealed, raised, spaced or movable to allow accessibility for cleaning on all sides, above and underneath the unit.",
    "critical_flag": "Not Critical",
    "score": 11,
    "grade": "A",
    "grade_date": "2026-10-17",
    "latitude": 40.583369830769,
    "longitude": -73.951275749439,
    "source_url": "https://data.cityofnewyork.us/resource/43nn-pn8j.json?camis=40393413"
}
```

### Input

Minimal run:

```json
{
    "since": "2026-09-01",
    "maxResults": 10
}
```

Filtered monitor run:

```json
{
    "since": "2026-09-01",
    "borough": "Manhattan",
    "cuisine": "Pizza",
    "grade": "A",
    "maxResults": 500,
    "mode": "monitor"
}
```

| Field | Type | Description |
| --- | --- | --- |
| `since` | string | Optional ISO date (`YYYY-MM-DD`). Default: last 30 days. |
| `until` | string | Optional ISO date (`YYYY-MM-DD`), inclusive. |
| `borough` | string | Optional case-insensitive borough: Manhattan, Brooklyn, Queens, Bronx or Staten Island. |
| `cuisine` | string | Optional case-insensitive DOHMH cuisine description, for example Pizza or American. |
| `grade` | `A`, `B` or `C` | Optional grade filter. |
| `maxResults` | integer | Default 1000, maximum 5000. |
| `mode` | `full` or `monitor` | Default `full`. `monitor` emits only rows never delivered before in the same borough/cuisine/grade scope. |
| `strictSchema` | boolean | Default `true`; fail the run on schema violations. |

No API key or login is required. An optional `SOCRATA_APP_TOKEN` Actor secret can be configured to raise Socrata rate limits; it is never part of the input and is never logged.

### Output fields

| Field | Type | Notes |
| --- | --- | --- |
| `camis` | string | DOHMH establishment id (required). |
| `business_name` | string | Source `dba` (required). |
| `borough` | string or null | Source `boro`. |
| `building`, `street`, `zipcode` | string or null | Establishment address. |
| `cuisine_description` | string or null | DOHMH cuisine label. |
| `inspection_date` | date | Required; `1900-01-01` is dropped. |
| `action` | string or null | Inspection action text. |
| `violation_code`, `violation_description` | string or null | Violation detail; null when no violation was recorded. |
| `critical_flag` | string or null | Critical / Not Critical / Not Applicable. |
| `score` | number or null | Published inspection score. |
| `grade` | string or null | A, B, C, N, P or null, as published. |
| `grade_date` | date or null | Placeholder dates become null. |
| `latitude`, `longitude` | number or null | WGS84; `0/0` placeholders become null. |
| `url`, `source_url` | string | Public NYC Open Data API URL for the establishment. |
| `scrapedAt` | string | ISO 8601 UTC timestamp of the run. |

### Project structure

```text
.actor/actor.json          Actor definition (payPerEvent, dataset schema, output schema)
.actor/input_schema.json   INPUT options (date window, borough, cuisine, grade, maxResults, mode)
.actor/dataset_schema.json Item JSON Schema (fields) + table view for the Store
.actor/pay_per_event.json  PPE pricing (two synthetic platform events)
src/query.ts               Pure SoQL filter and API URL construction (no phone column)
src/fetch.ts               Socrata HTTP client: pagination, retry/backoff
src/extract.ts             Pure normalization, dedupe and monitor helpers (fixture-tested)
src/main.ts                Entry point: fetch, validate, monitor filter, push
src/validate-output.ts     Ajv validator shared by the run and the tests
test/                      Offline fixtures and Vitest suites (no network, no credit)
```

### Pay-per-event pricing

The Actor uses Apify **pay-per-event** pricing with the two synthetic platform events:

| Event | Charged when | Price |
| --- | --- | --- |
| `apify-actor-start` | a run starts (synthetic, automatic) | 0.00005 USD per run (Apify-recommended default; includes 5 seconds of free compute) |
| `apify-default-dataset-item` | one inspection row is saved to the dataset (synthetic, automatic) | 0.003 USD per row = **3.00 USD per 1,000 results** |

There is no custom per-item event and no `Actor.charge()` call: the platform already charges `apify-default-dataset-item` per pushed item, so adding another event would bill users twice.

The price sits inside the direct-competitor band (0.001-0.01 USD per inspection) and inside Steadfetch's 1-10 USD per 1,000 results band.

**Measured cost:** a 1,000-row run takes about 4.0 seconds at the 256 MB minimum memory, using one free Socrata request. At Apify's 0.20 USD per GB-hour compute reference that is about 0.00006 USD of compute per 1,000 rows, well under the 0.10 USD target and leaving a gross margin above 99%. Larger runs reuse the same page size and do not need a browser or proxy.

### Development

```bash
npm install
npm test                # lint + typecheck + offline Vitest suites (no credit)
npm run publish:check   # Steadfetch publish gate
npm run schemas:validate
npm run smoke           # local Apify run (no platform credit)
```

The default test suite runs against the committed fixture `test/fixtures/socrata-inspections.json` and never touches the network. It covers the SoQL filters, the prohibited `phone` column, pagination, retry/backoff, deduplication, monitor state and output-schema validation.

### FAQ

**Do I need an API key?**
No. The NYC Open Data Socrata endpoint is public and keyless.

**Does it use a browser?**
No. The source is a JSON API, so plain HTTP is roughly 10x cheaper than rendering the web front end.

**Does it return personal data?**
No. Only establishment fields are returned. The `phone` column is not even selected from the API, and no other contact field is published.

**When am I charged?**
Once per run start (`apify-actor-start`) and once per inspection row saved to the dataset (`apify-default-dataset-item`). Errors, retries, duplicates and the `1900-01-01` placeholder are not charged.

**How does monitor mode avoid charging twice?**
It stores the dedupe keys it delivered in an Actor key-value store. On the next run with the same borough, cuisine and grade scope it skips those keys before pushing, so they are not billed again. Use `mode: "full"` when you want the complete matching window on every run.

**Why can one inspection produce several rows?**
The source publishes one row per violation citation. The Actor keeps that grain, which is what compliance and analytics teams need, and deduplicates identical `camis` + `inspection_date` + `violation_code` rows.

**Is the output schema stable?**
Yes. New fields are additive and the required fields in `.actor/dataset_schema.json` do not change.

### Maintenance

This Actor is **tested daily**: automated runs check that the NYC Open Data API is reachable and that the output still matches the published schema. Failures are reported as issues in the Steadfetch Actors repository.

# Actor input Schema

## `since` (type: `string`):

Optional ISO date (YYYY-MM-DD). Only rows with inspection_date on or after this date are returned. Default: the last 30 days.

## `until` (type: `string`):

Optional ISO date (YYYY-MM-DD). Only rows with inspection_date on or before this date are returned.

## `borough` (type: `string`):

Optional NYC borough, case-insensitive: Manhattan, Brooklyn, Queens, Bronx or Staten Island.

## `cuisine` (type: `string`):

Optional cuisine description as published by DOHMH, case-insensitive (for example American, Pizza, Chinese).

## `grade` (type: `string`):

Optional inspection grade. Rows without a grade are not returned when this filter is set.

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

Maximum number of inspection rows to save to the dataset.

## `mode` (type: `string`):

full: return every matching row (deduplicated inside the run). monitor: return only rows that were never emitted by a previous monitor run with the same borough, cuisine and grade scope. The monitor state is stored in the Actor key-value store.

## `strictSchema` (type: `boolean`):

Fail the run when an item does not match the OUTPUT schema. Keep enabled: Steadfetch never publishes Actors without output-schema validation.

## Actor input object example

```json
{
  "since": "2026-09-01",
  "until": "2026-10-20",
  "borough": "Manhattan",
  "cuisine": "Pizza",
  "maxResults": 10,
  "mode": "full",
  "strictSchema": true
}
```

# Actor output Schema

## `results` (type: `string`):

Dataset items validated against the item schema in dataset_schema.json (fields).

# 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 = {
    "since": "2026-10-01",
    "until": "2026-10-05",
    "maxResults": 10,
    "mode": "full",
    "strictSchema": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadfetch/nyc-restaurant-inspection-scores").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 = {
    "since": "2026-10-01",
    "until": "2026-10-05",
    "maxResults": 10,
    "mode": "full",
    "strictSchema": True,
}

# Run the Actor and wait for it to finish
run = client.actor("steadfetch/nyc-restaurant-inspection-scores").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 '{
  "since": "2026-10-01",
  "until": "2026-10-05",
  "maxResults": 10,
  "mode": "full",
  "strictSchema": true
}' |
apify call steadfetch/nyc-restaurant-inspection-scores --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steadfetch/nyc-restaurant-inspection-scores"
        }
    }
}
```

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/3DhATkGSANs8bksbx/builds/C1j5kPCcFaPZvUJ1V/openapi.json
