CA San Francisco Building Violations (NOV)
Pricing
from $0.03 / 1,000 violation records
CA San Francisco Building Violations (NOV)
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.
Pricing
from $0.03 / 1,000 violation records
Rating
0.0
(0)
Developer
Wenhao Yang
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
8 hours ago
Last modified
Categories
Share
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 50) |
Default run = the 50 most recently filed notice items - fast for the daily auto-test. For a targeted query add a filter; for a broad view use aggregate.
Example inputs
The items on one complaint - A notice is one complaint number that fans out into several line items.
{"complaintNumber": "202662503", "maxResults": 25}
Every violation ever filed at one parcel - block + lot is the assessor parcel key - the same key the SF permit register uses.
{"block": "3637", "lot": "019B", "maxResults": 25}
Active notices on one street - status=active is matched as an exact value, never as a substring.
{"status": "active", "streetName": "Mission", "maxResults": 25}
Notice items by code section - mode=aggregate returns one summary row per group with a count.
{"mode": "aggregate", "groupBy": "category"}
Low cost
From $0.00005 per record, down to $0.00003 at Gold - billed only for the rows you use, at the low end of the store. Cost scales with what you pull, not with the size of the register, and each record is metered individually.
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.
Example output
One notice item - complaintNumber=202662503 returns the items on that notice; one of them:
{"platform": "ca-sf-violations","source": "san-francisco-notices-of-violation","mode": "rows","groupKey": "","groupCount": "","groupBy": "","complaintNumber": "202662503","itemSequence": "1429309","recordId": "202662503-1429309","dateFiled": "2026-09-14","status": "active","block": "3703","lot": "004","streetNumber": "34","streetName": "06th","streetSuffix": "St","unit": "","category": "fire section","item": "provide fire sprinklers (906hc)","itemDescription": "remove the tape from over the sprikler heads in the boiler room.","codeViolation": "","workWithoutPermit": "","additionalWorkBeyondPermit": "","expiredPermit": "","cancelledPermit": "","unsafeBuilding": "","receivingDivision": "Housing Inspection Services","assignedDivision": "Housing Inspection Services","neighborhood": "South of Market","supervisorDistrict": "6","zipcode": "94103","dataAsOf": "2026-09-15","latitude": "37.781458584","longitude": "-122.409856729"}
mode=aggregate, groupBy=category - notice items by code section:
Building section 139,526Interior surfaces section 69,851Fire section 61,821Plumbing and electrical section 33,276
Source
- SF Dept. of Building Inspection: Notices of Violation - 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.
