# Boston Restaurant Inspections Scraper (`crawlerbros/boston-restaurant-inspections-scraper`) Actor

Scrape City of Boston food establishment health inspection records from the public data.boston.gov CKAN datastore. Search by business name, address, neighborhood, ZIP, type, result, or date range. Lookup by license number, property ID, or violation code.

- **URL**: https://apify.com/crawlerbros/boston-restaurant-inspections-scraper.md
- **Developed by:** [Crawler Bros](https://apify.com/crawlerbros) (community)
- **Categories:** Automation, Lead generation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Boston Restaurant Inspections Scraper

Scrape **City of Boston food establishment health inspection records** from the public data.boston.gov open-data CKAN datastore. Search by business name, address, neighborhood, ZIP code, establishment type, license status, inspection result, violation status/severity, or a date range. Look up full inspection history by license number or building (property ID), or find every establishment cited for a specific health code violation citywide. HTTP-only via the public `data.boston.gov` API — no auth, no proxy, no login.

### What this actor does

- **Four modes:** `search`, `byLicense`, `byPropertyId`, `byViolationCode`
- **Full-text keyword search** — search across violation descriptions and inspector comments (e.g. `rodent`, `hand washing`, `hot holding`)
- **Owner name search** — search by the establishment's legal owner / license-holder name, independent of the business's trade name
- **Near-me radius search** — find inspections within a chosen distance of a latitude/longitude
- **License expiry date range** — find establishments whose food license is expiring/expired within a window (compliance & renewal monitoring)
- **Sort by date, business name, or violation severity** — ascending or descending, in every mode
- **Violation severity mapping** — Boston's raw `*`/`**`/`***` codes are mapped to `Minor`/`Significant`/`Critical`
- **Building history lookup** — Boston uniquely tracks a `propertyId` per building, so you can pull every establishment ever inspected at one address across tenant changes
- **Empty fields are omitted**

### Output per inspection

- `recordId` — Boston's internal datastore row ID
- `businessName`; `dbaName` (when it differs from `businessName`)
- `legalOwner`, `ownerFirstName`, `ownerLastName` — as filed by the establishment (source data occasionally lists a company name in the "first name" column and vice versa)
- `licenseNumber`, `licenseIssuedDate`, `licenseExpiresDate`, `licenseStatus` (`Active`/`Inactive`/`Deleted`), `licenseCategory` (`FS`/`FT`/`MFW`/`RF`)
- `establishmentType` — `Eating & Drinking`, `Eating & Drinking w/ Take Out`, `Mobile Food Walk On`, or `Retail Food`
- `result` — inspection outcome/status code (see FAQ for the `HE_*` code meanings)
- `resultDate` — `YYYY-MM-DD`
- `violationCode`, `violationLevel` (`Minor`/`Significant`/`Critical`), `violationDescription`, `violationDate`, `violationStatus` (`Pass`/`Fail`) — only present on rows that cite a specific violation
- `comments` — inspector's free-text notes
- `address`, `city` (neighborhood), `state`, `zipCode`
- `propertyId` — Boston Assessing Department building identifier
- `latitude`, `longitude` — when the city has geocoded the address
- `sourceUrl` — direct link to this record in the source API
- `recordType: "foodInspection"`, `scrapedAt`

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `mode` | string | `search` | `search` / `byLicense` / `byPropertyId` / `byViolationCode` |
| `businessName` | string | – | Partial match on business/DBA name (mode=search) |
| `ownerName` | string | – | Partial match on legal owner / license-holder name (mode=search) |
| `address` | string | – | Partial match on street address |
| `city` | string | – | Boston neighborhood, exact match (mode=search) |
| `zipCode` | string | – | ZIP code (prefix match, so `02108` also matches `02108-1234`) |
| `establishmentType` | string | – | `Eating & Drinking` / `Eating & Drinking w/ Take Out` / `Mobile Food Walk On` / `Retail Food` |
| `licenseStatus` | string | – | `Active` / `Inactive` / `Deleted` |
| `result` | string | – | Inspection result/status code |
| `violationStatus` | string | – | `Pass` / `Fail` |
| `violationLevel` | string | – | `Minor` / `Significant` / `Critical` |
| `dateFrom` / `dateTo` | string | – | Inspection result date range, `YYYY-MM-DD` |
| `licenseExpiresFrom` / `licenseExpiresTo` | string | – | Food license expiration date range, `YYYY-MM-DD` (mode=search) |
| `keyword` | string | – | Full-text search across violation description + comments |
| `latitude` / `longitude` | number | – | Center point for a "near me" radius search (mode=search) |
| `radiusKm` | number | `1` | Radius in kilometers around `latitude`/`longitude` (0.1-50) |
| `licenseNumbers` | array | – | License numbers to fetch full history for (mode=byLicense) |
| `propertyIds` | array | – | Property IDs to fetch a building's full inspection history (mode=byPropertyId) |
| `violationCodes` | array | – | Exact violation code(s) to look up citywide (mode=byViolationCode) |
| `sortBy` | string | `resultDate` | Sort field: `resultDate` / `businessName` / `violationLevel` (all modes) |
| `sortAscending` | bool | `false` | Oldest/lowest-first instead of newest/highest-first (all modes) |
| `maxItems` | int | `50` | Hard cap (1-10000) |

#### Example: recent restaurants that failed inspection

```json
{
  "mode": "search",
  "establishmentType": "Eating & Drinking",
  "result": "HE_Fail",
  "maxItems": 50
}
```

#### Example: critical violations mentioning rodents in Dorchester

```json
{
  "mode": "search",
  "city": "DORCHESTER",
  "violationLevel": "Critical",
  "keyword": "rodent",
  "maxItems": 50
}
```

#### Example: inspections in a ZIP code within a date range

```json
{
  "mode": "search",
  "zipCode": "02118",
  "dateFrom": "2024-01-01",
  "dateTo": "2024-12-31",
  "maxItems": 100
}
```

#### Example: inspections within 500 meters of a location

```json
{
  "mode": "search",
  "latitude": 42.3601,
  "longitude": -71.0589,
  "radiusKm": 0.5,
  "maxItems": 50
}
```

#### Example: full inspection history for a license number

```json
{
  "mode": "byLicense",
  "licenseNumbers": ["313440"]
}
```

#### Example: full inspection history for a building

```json
{
  "mode": "byPropertyId",
  "propertyIds": ["156226"]
}
```

#### Example: every establishment cited for a specific health code

```json
{
  "mode": "byViolationCode",
  "violationCodes": ["22-4-601/602.11"]
}
```

#### Example: retail food licenses expiring around the annual renewal window

```json
{
  "mode": "search",
  "establishmentType": "Retail Food",
  "licenseExpiresFrom": "2026-12-01",
  "licenseExpiresTo": "2027-01-31",
  "maxItems": 100
}
```

> Note: Boston's food licenses overwhelmingly expire in a single annual batch on
> January 1st (the license year), with only a handful of stragglers on other dates.
> A `licenseExpiresFrom`/`licenseExpiresTo` window that doesn't straddle a January 1st
> will typically return 0 records — this is a real characteristic of the source data,
> not a bug. Pick a window spanning December–January (or the January of the year you
> care about) to see the bulk of expirations.

#### Example: every establishment licensed to a given owner, most severe violations first

```json
{
  "mode": "search",
  "ownerName": "Khosla",
  "sortBy": "violationLevel",
  "maxItems": 50
}
```

### Use cases

- **Food safety research** — analyze violation patterns across neighborhoods or establishment types
- **Restaurant/franchise due diligence** — pull a business's full inspection history by license number
- **Commercial real estate / leasing** — check a building's food-safety history across every tenant that has operated there via `propertyId`
- **Journalism & public health reporting** — surface repeat failures or specific violation codes citywide
- **Insurance & risk underwriting** — screen a food business's compliance record before writing a policy
- **Academic research** — bulk-export inspection data for epidemiological or urban studies

### FAQ

**What's the data source?**  The City of Boston's official open-data portal (`data.boston.gov`), Food Establishment Inspections dataset, maintained by the Inspectional Services Department's Health Division. This actor is an independent third-party tool and is not affiliated with the City of Boston.

**How far back does the data go?**  April 2006 to the present, updated by the city on a rolling (typically daily) basis.

**Are there rate limits?**  The public CKAN datastore endpoint has generous limits for read-only queries; the actor paginates with small delays to stay well within them.

**What do the `HE_*` result codes mean?**  They're Boston's Health Division inspection-workflow status codes, e.g. `HE_Pass`/`HE_Fail` (pass/fail a routine inspection), `HE_FailExt` (fail, extension granted to correct), `HE_NotReq` (no inspection required this cycle), `HE_OutBus` (establishment closed/out of business), `HE_Hearing`/`HE_Filed`/`HE_Hold` (administrative hearing workflow states), `HE_VolClos`/`HE_Closure` (voluntary or emergency closure). Plain `Pass`/`Fail`/`Closed`/`NoViol`/`PassViol` are older, pre-`HE_` era result codes still present on historical rows.

**Why do `ownerFirstName`/`ownerLastName` sometimes look swapped or contain a company name?**  That's how the source data is filed — Boston's underlying license record occasionally has a company name in the person-name columns. This actor passes through exactly what the city publishes rather than guessing at a correction.

**What's the difference between `businessName` and `dbaName`?**  `businessName` is the name on file for the inspection; `dbaName` ("doing business as") is only included when Boston lists a different trade name for the same establishment.

**Why is a violation's severity `Minor`/`Significant`/`Critical` instead of the raw `*`/`**`/`***`?**  This actor maps Boston's raw asterisk-count severity codes to their standard food-code meaning for readability. A small number of legacy rows (<0.01%) have a malformed severity value in the source data and are simply omitted from the mapped field rather than guessed at.

**Why does a record sometimes have no `violationCode`/`violationDescription`?**  Boston's dataset has one row per inspection *event* — some rows represent a clean pass or an administrative status (e.g. `HE_NotReq`) with no violation cited, so those violation-specific fields are simply omitted for that row.

**What is `propertyId`?**  A Boston Assessing Department building identifier. Multiple different restaurants that have operated at the same street address over the years typically share the same `propertyId`, letting you pull a location's complete food-safety history independent of ownership changes.

**How does the "near me" radius search work?**  Provide `latitude`, `longitude`, and optionally `radiusKm` (default 1 km) — the actor returns inspections whose geocoded address falls within that great-circle distance of the point you gave. Only inspections with a geocoded address can match.

**Why is `zipCode` a prefix match instead of exact?**  A small number of source rows store an extended ZIP+4 code (e.g. `02119-3212`); a prefix match on the 5-digit code still matches those rows correctly.

**How does `sortBy` work?**  It applies to every mode. `resultDate` (default) sorts by inspection result date; `businessName` sorts alphabetically; `violationLevel` sorts by severity (`Critical` first when descending). Combine with `sortAscending` to reverse any of the three. Note: ~0.8% of source rows (rows with no violation cited, or a small number of malformed source values) carry a raw severity marker outside the `*`/`**`/`***` set; those rows' `violationLevel` output field is correctly omitted, but their position in a `sortBy=violationLevel` ordering is determined by the raw source value rather than a true severity rank — this is an upstream data quality artifact, not a bug in the sort itself.

### Data Source & Limitations

This actor exposes the **entire public, unauthenticated query surface** of the City of Boston's Food Establishment Inspections CKAN datastore (`data.boston.gov`, resource id confirmed live 2026-08-17): every column the API returns is either an output field or a matching search filter, and every field with a small, stable set of known values (establishment type, license status, inspection result, violation status/severity, neighborhood, sort field) is a dropdown enum rather than free text.

What is intentionally **not** exposed, and why:

- **No separate "license category" filter.** The raw `licensecat` column (`FS`/`FT`/`MFW`/`RF`) is a 1:1 duplicate of `establishmentType` on every one of the ~896K rows in the dataset (confirmed via a live `GROUP BY` query) — filtering on it would be a redundant second control for the same axis, so it's kept only as the human-readable `establishmentType` filter.
- **`violationCodes` stays free-text array, not a dropdown.** There are 462 distinct violation codes in the live data — above the "<100 stable values → enum" threshold this project uses — so a curated dropdown would either be incomplete or unmaintainable; exact-code lookup (mode=`byViolationCode`) is the intended usage pattern here, typically copied from an inspection report.
- **`state` is not a filter.** Over 98% of rows are `MA` (with a handful of `Ma`/`ma` casing variants and a small number of nulls); it is not a meaningful search axis for a single-city dataset.
- **`licenseIssuedFrom`/`licenseIssuedTo` (`issdttm`) and `violationDate` (`violdttm`)/`statusDate` (`status_date`) are output-only, not separate date filters.** `licenseExpiresFrom`/`licenseExpiresTo` (a genuinely distinct, well-populated axis used for compliance/renewal monitoring) is exposed as a filter; adding four more date-range filters for closely-related/lower-signal timestamp columns on top of the existing `dateFrom`/`dateTo` (result date) and `licenseExpiresFrom`/`licenseExpiresTo` would add input-schema surface area without a correspondingly distinct real-world query need — all four values are still returned on every record for client-side filtering if needed.
- **No authentication-gated data exists for this dataset.** The CKAN `datastore_search_sql` and `package_show` endpoints used by this actor are fully public with no API key, cookies, or login — nothing is held back for auth reasons.

# Actor input Schema

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

What to fetch.

## `businessName` (type: `string`):

Partial, case-insensitive match against the establishment's business name or DBA name.

## `ownerName` (type: `string`):

Partial, case-insensitive match against the establishment's legal owner / license holder name (also checks the first/last name columns on file).

## `address` (type: `string`):

Partial, case-insensitive match against the street address.

## `city` (type: `string`):

Boston neighborhood (nearly all rows list the city as `BOSTON`; the remainder use the specific neighborhood name).

## `zipCode` (type: `string`):

Exact 5-digit ZIP code.

## `establishmentType` (type: `string`):

The license category of the food establishment.

## `licenseStatus` (type: `string`):

Current status of the establishment's food service license.

## `result` (type: `string`):

Outcome/status code of the inspection or violation record. `HE_*` codes are Boston Health Division inspection-workflow statuses.

## `violationStatus` (type: `string`):

Whether the specific violation line item passed or failed re-inspection.

## `violationLevel` (type: `string`):

Severity of the cited violation: Minor (*), Significant (**), or Critical (***).

## `dateFrom` (type: `string`):

Only include inspections on/after this date (YYYY-MM-DD), matched against the result date.

## `dateTo` (type: `string`):

Only include inspections on/before this date (YYYY-MM-DD), matched against the result date.

## `licenseExpiresFrom` (type: `string`):

Only include establishments whose food license expiration date is on/after this date (YYYY-MM-DD). Useful for compliance/renewal monitoring (e.g. "expiring this quarter").

## `licenseExpiresTo` (type: `string`):

Only include establishments whose food license expiration date is on/before this date (YYYY-MM-DD).

## `keyword` (type: `string`):

Full-text, case-insensitive search across the violation description and inspector comments (e.g. `rodent`, `hand washing`, `hot holding`).

## `latitude` (type: `number`):

Center latitude for a "near me" radius search. Combine with `longitude` and `radiusKm`.

## `longitude` (type: `number`):

Center longitude for a "near me" radius search. Combine with `latitude` and `radiusKm`.

## `radiusKm` (type: `number`):

Search radius in kilometers around `latitude`/`longitude`. Only applied when both are set. Default 1 km.

## `licenseNumbers` (type: `array`):

City of Boston food establishment license numbers to fetch the full inspection/violation history for.

## `propertyIds` (type: `array`):

Boston Assessing Department property IDs to fetch the full inspection history of every establishment ever registered at that building.

## `violationCodes` (type: `array`):

Exact Massachusetts food code violation code(s) to look up citywide, e.g. `22-4-601/602.11`.

## `sortBy` (type: `string`):

Field to sort results by (all modes). `Violation severity` sorts Critical-first when descending.

## `sortAscending` (type: `boolean`):

Sort oldest/lowest-first instead of newest/highest-first (applies to all modes).

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

Hard cap on emitted records.

## Actor input object example

```json
{
  "mode": "search",
  "businessName": "pizza",
  "ownerName": "",
  "address": "",
  "city": "",
  "zipCode": "",
  "establishmentType": "",
  "licenseStatus": "",
  "result": "",
  "violationStatus": "",
  "violationLevel": "",
  "dateFrom": "",
  "dateTo": "",
  "licenseExpiresFrom": "",
  "licenseExpiresTo": "",
  "keyword": "",
  "radiusKm": 1,
  "licenseNumbers": [],
  "propertyIds": [],
  "violationCodes": [],
  "sortBy": "resultDate",
  "sortAscending": false,
  "maxItems": 50
}
```

# Actor output Schema

## `inspections` (type: `string`):

Dataset containing all scraped Boston food establishment inspection records.

# 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 = {
    "mode": "search",
    "businessName": "pizza",
    "ownerName": "",
    "address": "",
    "city": "",
    "zipCode": "",
    "establishmentType": "",
    "licenseStatus": "",
    "result": "",
    "violationStatus": "",
    "violationLevel": "",
    "dateFrom": "",
    "dateTo": "",
    "licenseExpiresFrom": "",
    "licenseExpiresTo": "",
    "keyword": "",
    "radiusKm": 1,
    "licenseNumbers": [],
    "propertyIds": [],
    "violationCodes": [],
    "sortBy": "resultDate",
    "sortAscending": false,
    "maxItems": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("crawlerbros/boston-restaurant-inspections-scraper").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 = {
    "mode": "search",
    "businessName": "pizza",
    "ownerName": "",
    "address": "",
    "city": "",
    "zipCode": "",
    "establishmentType": "",
    "licenseStatus": "",
    "result": "",
    "violationStatus": "",
    "violationLevel": "",
    "dateFrom": "",
    "dateTo": "",
    "licenseExpiresFrom": "",
    "licenseExpiresTo": "",
    "keyword": "",
    "radiusKm": 1,
    "licenseNumbers": [],
    "propertyIds": [],
    "violationCodes": [],
    "sortBy": "resultDate",
    "sortAscending": False,
    "maxItems": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("crawlerbros/boston-restaurant-inspections-scraper").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 '{
  "mode": "search",
  "businessName": "pizza",
  "ownerName": "",
  "address": "",
  "city": "",
  "zipCode": "",
  "establishmentType": "",
  "licenseStatus": "",
  "result": "",
  "violationStatus": "",
  "violationLevel": "",
  "dateFrom": "",
  "dateTo": "",
  "licenseExpiresFrom": "",
  "licenseExpiresTo": "",
  "keyword": "",
  "radiusKm": 1,
  "licenseNumbers": [],
  "propertyIds": [],
  "violationCodes": [],
  "sortBy": "resultDate",
  "sortAscending": false,
  "maxItems": 50
}' |
apify call crawlerbros/boston-restaurant-inspections-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,crawlerbros/boston-restaurant-inspections-scraper"
        }
    }
}

```

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/G7vukJTgXmMrUy5O1/builds/GeReeJL6TbJMDUaTX/openapi.json
