# Toronto DineSafe Scraper (`crawlerbros/toronto-dinesafe-scraper`) Actor

Scrape City of Toronto DineSafe food premises inspection records - restaurant/food vendor inspection status, infractions, severity, fines, and location, sourced from Toronto Public Health's official open data.

- **URL**: https://apify.com/crawlerbros/toronto-dinesafe-scraper.md
- **Developed by:** [Crawler Bros](https://apify.com/crawlerbros) (community)
- **Categories:** Automation, Travel, 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 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/platform/actors/running/actors-in-store#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

## Toronto DineSafe Scraper

Scrape **DineSafe** — the City of Toronto's official food premises inspection
disclosure system, run by Toronto Public Health. Search inspection records by
restaurant name or address, or pull the complete inspection history for a
specific establishment: pass/fail status, infractions found, severity level,
court outcomes, and fines. Sourced directly from Toronto's official open data
portal ([open.toronto.ca](https://open.toronto.ca/dataset/dinesafe/)). No
login, no API key, no cookies.

### What this actor does

- **Two modes:** `search` (by name/address/infraction keyword + filters) and
  `byEstablishmentIds` (full inspection history for known establishments)
- **Filters:** inspection status, infraction severity, inspection date range,
  fine amount range, infraction keyword (e.g. "pest", "temperature", "sanitized")
- **Sorting:** newest/oldest inspection date, establishment name, fine amount
- **Clean data:** upstream address/text formatting artifacts are cleaned up
  (see FAQ), phone numbers formatted, and every record includes a ready-to-use
  Google Maps link
- **Empty fields are omitted** — only populated data appears in the output

### Output per inspection record

- `inspectionId` — unique ID for this inspection row
- `establishmentId`, `establishmentIdLegacy` — current + legacy establishment IDs
- `establishmentName`, `address`, `phone`
- `inspectionDate`, `inspectionStatus` (`Pass` / `Conditional Pass` / `Closed`)
- `observation` — inspector's summary statement
- `severity` — `NA` / `M - Minor` / `S - Significant` / `C - Crucial` (only present when an infraction was recorded)
- `infractionType`, `infractionDetails` — only present when an infraction was recorded
- `outcomeDate`, `outcomeDescription` — only present when the case went to prosecution
- `amountFinedCad` — only present when a court fine was recorded
- `latitude`, `longitude`, `mapUrl`
- `sourceUrl` — link to the official DineSafe open-data page
- `recordType: "inspection"`, `scrapedAt`

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `mode` | select | `search` | `search` / `byEstablishmentIds` |
| `establishmentName` | string | `pizza` | Name contains (mode=search) |
| `streetOrPostalCode` | string | – | Address/postal code contains (mode=search) |
| `infractionKeyword` | string | – | Infraction/violation description contains (mode=search), e.g. `pest`, `temperature` |
| `establishmentIds` | array | – | Establishment IDs (mode=byEstablishmentIds) |
| `inspectionStatus` | select | any | `Pass` / `Conditional Pass` / `Closed` |
| `severity` | select | any | `NA` / `M - Minor` / `S - Significant` / `C - Crucial` |
| `dateFrom` | date | – | Inspections on/after this date (`YYYY-MM-DD`) |
| `dateTo` | date | – | Inspections on/before this date (`YYYY-MM-DD`) |
| `minAmountFined` | int | – | Minimum court fine (CAD) |
| `maxAmountFined` | int | – | Maximum court fine (CAD) |
| `sortBy` | select | `inspectionDateDesc` | Result ordering |
| `maxItems` | int | `50` | Hard cap (1–5000) |

#### Example: recent closures

```json
{
  "mode": "search",
  "inspectionStatus": "Closed",
  "dateFrom": "2026-01-01",
  "sortBy": "inspectionDateDesc",
  "maxItems": 100
}
```

#### Example: inspection history for a specific restaurant

```json
{
  "mode": "byEstablishmentIds",
  "establishmentIds": ["001Vo000013QjdPIAS"]
}
```

#### Example: crucial infractions on a street

```json
{
  "mode": "search",
  "streetOrPostalCode": "Queen St",
  "severity": "C - Crucial",
  "maxItems": 50
}
```

#### Example: pest-control infractions city-wide

```json
{
  "mode": "search",
  "infractionKeyword": "pest",
  "maxItems": 100
}
```

### Use cases

- **Consumer safety apps** — surface pass/fail status and recent infractions for restaurants near you
- **Real-estate & retail-siting research** — check the inspection history of a commercial address before leasing
- **Journalism & data analysis** — track closures, repeat offenders, and fine outcomes across Toronto
- **Compliance monitoring** — franchise or restaurant-group owners tracking inspection results across multiple locations
- **Academic research** — bulk export food-safety enforcement data for public-health studies

### FAQ

**What's DineSafe?** Toronto Public Health's food safety disclosure program.
Every food premises in Toronto (restaurants, cafés, food trucks, grocers,
etc.) is inspected regularly; each inspection results in a `Pass`,
`Conditional Pass`, or `Closed` notice. See the [official DineSafe
page](https://www.toronto.ca/community-people/health-wellness-care/health-programs-advice/food-safety/dinesafe/).

**How far back does the data go?** The live dataset covers roughly the last
3 years and is refreshed daily by the City of Toronto. Older records are
retired from the live feed by the source.

**Why do some records have no `severity`/`infractionType`/`infractionDetails`
fields?** Those fields only apply when an infraction was actually found
during the inspection. A clean `Pass` inspection has no infraction — since
`no output field is null`, these fields are simply omitted rather than
sent as empty placeholders. (Internally, the source publishes the literal
text `"None"` for these sub-fields on clean passes; the actor treats that as
"no infraction" and drops the field.)

**Why do some addresses look different from the source's raw address
string?** The upstream feed inserts a placeholder token where an establishment
has no unit number (e.g. the raw string is `1871 O'Connor Dr None M4A 1X1`
when there's no unit). The actor removes that empty-unit placeholder so
`address` reads as a normal street address (`1871 O'Connor Dr M4A 1X1`).

**Is a fine amount recorded for most inspections?** No — `amountFinedCad`
only appears for the small number of cases that went to prosecution and
resulted in a conviction. Setting `minAmountFined`/`maxAmountFined` only
returns rows with a confirmed fine in that range (rows with no recorded
fine are excluded, not passed through) — so most searches will correctly
return very few (or zero) results; this reflects the real data, not a bug.

**Can I look up an establishment by its legacy numeric ID instead of the
new ID format?** Yes — `establishmentIds` accepts both the current ID format
(e.g. `001Vo000013QjdPIAS`) and the legacy numeric ID (e.g. `10752656`)
interchangeably.

**Does this actor cover ward/neighbourhood filters?** The source dataset
does not publish a ward or neighbourhood field, only `address`,
`latitude`/`longitude`. Use `streetOrPostalCode` to narrow by area, or filter
the output by coordinates downstream.

**How fresh is the data?** Daily — Toronto Public Health refreshes the
DineSafe dataset every day with newly completed inspections.

# Actor input Schema

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

What to fetch.

## `establishmentName` (type: `string`):

Free-text match against the restaurant/food premises name, e.g. `pizza`, `subway`.

## `streetOrPostalCode` (type: `string`):

Free-text match against the establishment address, e.g. `Queen St`, `M5V`, `M5S 2E4`.

## `infractionKeyword` (type: `string`):

Free-text match against the infraction/violation description, e.g. `pest`, `temperature`, `sanitized`, `handwashing`. Combine with the other search fields to narrow further.

## `establishmentIds` (type: `array`):

DineSafe establishment IDs — accepts either the current ID (e.g. `001Vo000013QjdPIAS`) or the legacy numeric ID (e.g. `10752656`). Returns every inspection on record for each establishment.

## `inspectionStatus` (type: `string`):

Only return inspections with this outcome.

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

Only return inspection rows with this infraction severity level. Rows with no infraction have no severity and are excluded by any severity filter.

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

Only return inspections on or after this date. DineSafe's live dataset covers roughly the last 3 years.

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

Only return inspections on or before this date.

## `minAmountFined` (type: `integer`):

Only return rows with a recorded court fine of at least this amount. Fines are recorded on a small minority of rows (only cases that went to prosecution) — this filter will exclude most inspection rows, which have no associated fine.

## `maxAmountFined` (type: `integer`):

Only return rows with a recorded court fine of at most this amount.

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

Result ordering.

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

Hard cap on emitted records.

## Actor input object example

```json
{
  "mode": "search",
  "establishmentName": "pizza",
  "establishmentIds": [],
  "inspectionStatus": "",
  "severity": "",
  "sortBy": "inspectionDateDesc",
  "maxItems": 50
}
```

# Actor output Schema

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

Dataset containing all scraped DineSafe 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",
    "establishmentName": "pizza",
    "establishmentIds": [],
    "inspectionStatus": "",
    "severity": "",
    "sortBy": "inspectionDateDesc",
    "maxItems": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("crawlerbros/toronto-dinesafe-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",
    "establishmentName": "pizza",
    "establishmentIds": [],
    "inspectionStatus": "",
    "severity": "",
    "sortBy": "inspectionDateDesc",
    "maxItems": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("crawlerbros/toronto-dinesafe-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",
  "establishmentName": "pizza",
  "establishmentIds": [],
  "inspectionStatus": "",
  "severity": "",
  "sortBy": "inspectionDateDesc",
  "maxItems": 50
}' |
apify call crawlerbros/toronto-dinesafe-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,crawlerbros/toronto-dinesafe-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/PtX8HnZmxILmK0PdN/builds/6XhlsUDsfgM1QiDxI/openapi.json
