# CA San Francisco Building Violations (NOV) (`j0401/ca-sf-violations`) Actor

San Francisco building Notices of Violation (public open data, 517k records): the code violation line items behind each notice - category, inspector finding, flagged conditions, parcel block+lot, address, district. Filter by status/category/block+lot/date or aggregate the register.

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

## Pricing

$0.10 / 1,000 ca san francisco violation 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

## San Francisco Building Violations - Notices of Violation (NOV)

San Francisco's Department of Building Inspection issues a **Notice of Violation** when a property breaks the building code - and publishes every notice, every line item, as open data. This actor turns that register into a **charged-per-record lookup, filter and aggregate tool**: pull the code violations on a parcel, sweep a street or a district, isolate the active or work-without-permit cases, or read the register by category.

**Built for:** property and title diligence, real-estate and construction underwriting, code-compliance screening, and anyone who needs the **violation history behind an address** - not a keyword match against a street name.

### What it covers

**517,136 notice items** across **133,944 complaints** - every violation line the city has filed, spanning the code's own sections:

| Category | Items |
|---|---|
| Building section | 139,526 |
| Other section | 97,841 |
| Interior surfaces section | 69,851 |
| Fire section | 61,821 |
| Plumbing and electrical section | 33,276 |
| Security requirements section | 18,916 |
| Smoke detection section | 14,833 |
| Sanitation section | 12,684 |
| Lead section | 1,391 |
| HCO | 145 |
| (blank category) | 66,852 |

Each item carries the **inspector's finding in the source's own words** (*"remove egress obstruction"*, *"repair damaged walls"*, *"provide self-closing devices at exterior doors"*) - present on **420,368 items (81%)** - plus the drafted code citation where the city provides one, and the flagged conditions the record marks directly:

- **work without permit** - **25,507 items**
- **unsafe building** - **15,213 items**
- additional work beyond permit - 3,527
- expired permit - 3,463 / cancelled permit - 86

and the location spine: **street address, block + lot (the assessor parcel key, present on 100% of rows), neighborhood, supervisor district, ZIP, latitude/longitude**.

### The fine print that matters

The **block + lot key is the point.** It is the *same* key San Francisco's building-permit register uses, so a parcel's violation history and its permit history line up **exactly** - no fuzzy address matching. Pair this actor with the SF building-permits actor on block+lot and the two registers join cleanly (verified: block 3637 / lot 019B returns 13 notices and 13 permits; block 0280 / lot 008 returns 84 notices).

A notice is **one event with many line items**: the complaint number repeats across its items (517,136 items over 133,944 complaints), so `maxResults` counts items, and a single address can return a dozen rows for one notice. Status is exactly two source values - **`active` (29,437, still open)** and **`not active` (487,699, resolved/closed)** - matched as exact values, never as a substring. There is **no per-row change marker** on this dataset, so the actor offers rows + aggregate; the recency axis is the filing date (9,676 items filed in 2026 so far, 762 in the last 30 days).

### Typical questions

- "Every violation ever filed at **block 3637 / lot 019B**."
- "**Active** notices on **Mission Street**."
- "Notices flagged **work without permit** in district 8."
- "Notices filed **this year** in the **Twin Peaks** neighborhood."
- "Aggregate the register by **category**, **neighborhood** or **district**."

### Inputs

| Input | What it does |
|---|---|
| `mode` | `rows` (default) / `aggregate` |
| `status` / `category` | notice status (active / not active) and code section |
| `complaintNumber` | exact DBI complaint number |
| `block` / `lot` | the assessor parcel key (join to permits) |
| `address` / `streetName` / `neighborhood` / `supervisorDistrict` / `zipcode` | where |
| `filedFrom` / `filedTo` | filing-date range |
| `workWithoutPermit` / `unsafeBuilding` | flagged-condition filters |
| `groupBy` | aggregate over status / category / neighborhood / district / ZIP / division |
| `maxResults` | cap records (default 200) |

**Default run = the 200 most recently filed notice items** - fast for the daily auto-test. For a targeted query add a filter; for a broad view use `aggregate`.

### Low cost

**From $0.0001 per record** - billed only for the rows you use, at the platform floor. Cost scales with what you pull, not with the size of the register, and because each record is metered individually there's no per-run charge cap to hit on a big pull.

The hard part of this register is that a notice is **not one row**. It is a complaint number that fans out into a sequence of line items, each holding a drafted code citation, a free-text inspector finding, and its own condition flags - and the flags are **three-valued**: `Y`, `N`, and *blank*, where blank means the condition was not assessed at all, not that it was cleared. A thin scraper that filters `work_without_permit` with a text match pulls in the `N` rows too and quietly inflates the count. The status column is a matching trap of its own - `active` is a **substring** of `not active`, so the obvious `like '%active%'` returns *everything and its opposite*; it has to be matched as two exact values. And the violation categories arrive both named and **blank** (66,852 items carry no section at all), so a category filter has to treat blank as a real bucket rather than dropping it. Normalizing the multi-row notice, the three-valued flags and the substring-overlapping status into a schema where a `block+lot` / `status=active` / `workWithoutPermit` query returns exactly what you mean is the actual product. Every pull is integrity-checked against the register's known shape, so a degraded source fails loudly instead of returning bad rows.

### Source

- [SF Dept. of Building Inspection: Notices of Violation](https://data.sfgov.org/Housing-and-Buildings/Notices-of-Violation-issued-by-the-Department-of-Bui/nbtm-fbw5) - the city's full NOV register, refreshed daily. Public open data. Records are the city's own published fields; not an endorsement of any property or finding.

# Actor input Schema

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

rows = violation items matching your filters (default). aggregate = one count row per group (see groupBy).

## `status` (type: `string`):

Notice status. active = still open; not active = resolved/closed (~94% of the register). Blank = any.

## `category` (type: `string`):

Code category (fuzzy): building section, fire section, interior surfaces section, plumbing and electrical section, smoke detection section, sanitation section, security requirements section, lead section. Blank = any.

## `complaintNumber` (type: `string`):

Exact DBI complaint number (a notice carries several item rows).

## `block` (type: `string`):

Assessor block (parcel key). Combine with lot to pull one parcel's notices, or to join against the SF building-permit register.

## `lot` (type: `string`):

Assessor lot (parcel key).

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

Street number or street name, e.g. 'Mission' or '1200'.

## `streetName` (type: `string`):

Street name only, e.g. 'Burnett'.

## `neighborhood` (type: `string`):

Analysis neighborhood substring, e.g. 'Mission', 'Sunset', 'Twin Peaks'.

## `supervisorDistrict` (type: `string`):

SF supervisor district number (1-11).

## `zipcode` (type: `string`):

ZIP code.

## `filedFrom` (type: `string`):

Only notices filed on/after this date (YYYY-MM-DD). Use with filedTo for a window.

## `filedTo` (type: `string`):

Only notices filed before this date (YYYY-MM-DD).

## `workWithoutPermit` (type: `boolean`):

When on, returns only items flagged as work performed without a permit.

## `unsafeBuilding` (type: `boolean`):

When on, returns only items flagged as an unsafe building condition.

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

Which dimension to aggregate over. category -> what kind of violation; neighborhood / district -> where; receivingDivision -> which DBI unit handled it.

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

Cap the number of records pushed (0 = up to ~10k per run; each record is metered individually, so there is no per-run charge cap). An aggregate has at most a few hundred groups.

## Actor input object example

```json
{
  "mode": "rows",
  "status": "",
  "category": "",
  "complaintNumber": "",
  "block": "",
  "lot": "",
  "address": "",
  "streetName": "",
  "neighborhood": "",
  "supervisorDistrict": "",
  "zipcode": "",
  "filedFrom": "",
  "filedTo": "",
  "workWithoutPermit": false,
  "unsafeBuilding": false,
  "groupBy": "status",
  "maxResults": 50
}
```

# Actor output Schema

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

San Francisco building violation records or aggregates - 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-sf-violations").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-sf-violations").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-sf-violations --silent --output-dataset

```

## MCP server setup

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

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/eCPsu5QP2pN6Fx7lf/builds/zTecpUgiUnL3QUFjt/openapi.json
