# CA Food Facility Inspections - San Francisco Restaurant History (`j0401/ca-food-safety`) Actor

SF restaurant / food-facility inspection history (public data, 2016-present, ~126k records): pass / conditional / closure, violation codes + text, scores, risk. Compliance & closure diligence for franchise / restaurant buyers.

- **URL**: https://apify.com/j0401/ca-food-safety.md
- **Developed by:** [Wenhao Yang](https://apify.com/j0401) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.00 / 1,000 california food facility inspection records

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?

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

## CA Food Facility Inspections - San Francisco Restaurant History

San Francisco's Department of Public Health publishes its food-facility inspection history as **open data** (city Socrata, free public data). The catch: the city has published it in **three successive datasets with different schemas**, so a naive lookup only ever sees one slice. This actor merges all three into one queryable, charged-per-record API - **the full SF restaurant inspection history from 2016 to today.**

**Built for:** restaurant / ghost-kitchen / franchise compliance & closure diligence, supply-chain vetting, insurance, and food-safety research.

### What it covers

**San Francisco food-facility inspections, 2016 -> present (~126k records across three eras):**

| Era | Rows | Granularity | What it records |
|---|---|---|---|
| `current` (2024-present) | ~22k | per inspection | **Pass / Conditional Pass / Closure**, violation **codes + code text** + violation count, exact **permit number**, DBA, neighborhood, inspector |
| `y2020_2023` | ~50k | per violation | PASS / CONDITIONAL PASS / CLOSURE, violation severity (Minor / Major / OUT...), description |
| `y2016_2019` | ~54k | per violation | inspection **score (0-100)**, **risk category** (High / Moderate / Low), violation description |

**Why three eras?** SF changed systems in 2016, 2020 and 2024 - the old tables froze and a new one started, with a ~2023-08 -> 2024-01 gap between eras. There is no exact key linking a business across all three, so **cross-era history is matched on business name** (`generation: all`). Within the current era, `permit_number` is the stable per-business key.

> Note on era-specific filters: only the eras that actually record a field honor a filter on it. A `rating` / `closure` filter runs against the current and 2020-2023 eras only (the 2016-2019 era has no rating column - it records a score instead), so "show every closure across SF locations" returns closures from 2020 onward. Use `risk` / `minScore` for the 2016-2019 era.

### Typical questions

- "Show every **Closure** a restaurant chain has had across its SF locations."
- "What violations does this food facility (by permit number) have on its latest inspections?"
- "Which violations mention **rodents / vermin / improper cooling** - current and historical?"
- "Find high-risk (or low-score) inspections from the 2016-2019 era for this business."
- "How many **Conditional Pass** results did restaurants in the Mission get last year?"

### Results vs. violations - row semantics

Each era has its own row granularity, and the `generation` field tells you which:

- **current** = one row per **inspection** (violation text is the full `violation_codes` blob; `violationCount` = how many).
- **y2020\_2023 / y2016\_2019** = one row per **violation** (a clean inspection still emits one row with empty violation text).

### Inputs

| Input | What it does |
|---|---|
| `generation` | `current` (default) / `y2020_2023` / `y2016_2019` / `all` (full history by name) |
| `business` | business-name substring |
| `permit` | exact permit number (current era only - the stable store key) |
| `dateFrom` / `dateTo` | inspection date window |
| `rating` | `pass` / `conditional` / `closure` |
| `keyword` | violation-text substring |
| `risk` | High / Moderate / Low (2016-2019 era) |
| `severity` | violation severity text (2020-2023 era) |
| `minScore` | only inspections scoring at least this (2016-2019 era) |
| `aggregate` / `groupBy` | one row per group (rating / neighborhood / inspectionType / severity / risk) with a count |
| `maxResults` | cap records (default 200) |

**Default run = the ~200 most recent SF inspections** (2024-present era) - fast enough for the daily auto-test, and a live look at what SF inspectors are finding right now.

### Low cost

**From $0.001 per record** - billed only for the rows you use, with a per-run charge cap so a broad query can't surprise-bill.

Behind this is an **awkward source, unified**. SF has published its food-inspection history as **three successive datasets with different schemas** (2016-2019 carries a 0-100 score, 2020-2023 carries a Pass/Conditional/Closure rating, 2024+ is per-inspection with violation codes) - and there is **no key linking a restaurant across them**; even the rating enum was once mistyped by the city (`CONDITIIONAL`). We normalize all three onto one schema, one rating vocabulary, and one cross-era store history matched on name - with the future-dated garbage rows SF lets into its tables clamped out so a date filter can never surface a 2031 inspection. You query one clean history; the schema archaeology stays on our side.

### Source

- [SF Health Inspections 2024-Present](https://data.sfgov.org/Health-and-Social-Services/Health-Inspections-2024-Present/tvy3-wexg) - official.
- [SF Restaurant Scores 2020-2023](https://data.sfgov.org/Health-and-Social-Services/Restaurant-Scores-2020-2023/5tti-66ds) - official.
- [SF Restaurant Scores 2016-2019](https://data.sfgov.org/Health-and-Social-Services/Restaurant-Scores-LIVES-Standard/pyih-qa8i) - official.

> Note: inspection results are SF's own public health ratings. A "Closure" result means the facility was closed at the time of that inspection; reasons are recorded in free-text violation notes rather than a structured field.

# Actor input Schema

## `generation` (type: `string`):

current = 2024-present (~22k, per-inspection, rating + violation codes, permit number). y2020\_2023 = 2020-2023 (~50k per-violation rows). y2016\_2019 = 2016-2019 (~54k per-violation rows, score + risk). all = full SF history matched on business name.

## `business` (type: `string`):

Business name filter (substring). Use together with generation=all to trace a name across eras; the current era adds permit\_number for an exact match.

## `permit` (type: `string`):

Exact SF permit number - the stable per-business key in the 2024-present generation (e.g. 06762361). Not available in the older two eras.

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

Only inspections on/after this date.

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

Only inspections on/before this date.

## `rating` (type: `string`):

Filter to a result. current era reads facility\_rating\_status; 2020-2023 reads facility\_status. closure = facility closed after this inspection.

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

Substring match against the violation text (codes + code text in current era, the single description in 2020-2023, the violation description in 2016-2019).

## `risk` (type: `string`):

2016-2019 era only: filter to a risk category (High / Moderate / Low).

## `severity` (type: `string`):

2020-2023 era only: violation\_observed text filter (e.g. 'Minor', 'Major', 'OUT (Not in Compliance)').

## `minScore` (type: `integer`):

2016-2019 era only: only inspections with a score >= this value.

## `aggregate` (type: `boolean`):

When on, returns one summary row per group with a count. Requires a single generation (not 'all').

## `groupBy` (type: `string`):

rating = result (current/2020-2023). neighborhood = SF analysis neighborhood (current). inspectionType = current / 2020-2023 / 2016-2019. severity = 2020-2023. risk = 2016-2019. Choosing a dimension the generation lacks errors.

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

Cap records pushed (0 = a reasonable ceiling). Default = the ~50 most recent SF inspections.

## Actor input object example

```json
{
  "generation": "current",
  "aggregate": false,
  "groupBy": "rating",
  "maxResults": 50
}
```

# Actor output Schema

## `recordsUrl` (type: `string`):

California food-facility inspection records - as JSON

## `datasetUrl` (type: `string`):

No description

## `runUrl` (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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("j0401/ca-food-safety").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("j0401/ca-food-safety").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 '{}' |
apify call j0401/ca-food-safety --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,j0401/ca-food-safety"
        }
    }
}
```

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/gmJIKfLfvKVrWjXa0/builds/mzbVykYagpSqlWmNB/openapi.json
