# SF Building Permit Scraper (`crawlerbros/sf-building-permit-scraper`) Actor

Scrape the official San Francisco Department of Building Inspection permit dataset. Search/filter by permit type, status, neighborhood, district, zip, use, construction type, cost, or date; lookup by permit number, property address, or assessor block/lot. Free Socrata API, no login.

- **URL**: https://apify.com/crawlerbros/sf-building-permit-scraper.md
- **Developed by:** [Crawler Bros](https://apify.com/crawlerbros) (community)
- **Categories:** Real estate, Automation, 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/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

## SF Building Permit Scraper

Scrape the official **San Francisco Department of Building Inspection (DBI)** building permit dataset. Search or filter San Francisco building permits by permit type, status, neighborhood, supervisor district, zip code, use, construction type, estimated cost, or filed/issued date — or look up permits by permit number, property address, or assessor block & lot. Powered by the public SF Open Data (Socrata) API. No login, no API key, no proxy required.

### What this actor does

- **Four modes:** `search` (filter/browse), `byPermitNumber` (exact permit lookup), `byAddress` (lookup by street number + street name), `byBlockLot` (lookup by assessor's block & lot)
- **Rich filters:** permit type, status, neighborhood, supervisor district, zip code, existing/proposed use, existing/proposed construction type, estimated-cost range, filed-date range, issued-date range, description keyword, geographic radius around a lat/lon point
- **Special-project toggles:** ADU (Accessory Dwelling Unit), fire-only permit, site permit, structural notification, voluntary soft-story seismic retrofit, reroofing
- **Full-text search** across the permit's work description and other text fields
- **Every permit application** — one record per permit, spanning new construction, alterations/repairs, demolitions, signs, grading, and more
- **Geo data** — latitude/longitude, neighborhood, supervisor district, zip code, block/lot
- **Empty fields are omitted** — a permit missing a completion date or a construction-type code simply has no such field, never a placeholder

### Output per permit record

- `permitNumber` — DBI permit number (usually one row per permit, but DBI's own dataset occasionally publishes more than one row for the same permit number — e.g. a permit renewal alongside its original record; `rowId` is the actual unique key)
- `permitType`, `permitTypeDefinition` — DBI permit classification code and definition
- `status`, `statusDate` — current status and when it was last set
- `filedDate`, `issuedDate`, `completedDate`, `approvedDate`, `firstConstructionDocumentDate`, `permitCreationDate`, `lastPermitActivityDate`
- `description` — free-text work description
- `streetNumber`, `streetNumberSuffix`, `streetName`, `streetSuffix`, `unit`, `unitSuffix`, `fullAddress`
- `city`, `state` — always San Francisco, CA
- `block`, `lot`, `blockLot` — assessor parcel identifiers
- `neighborhood` — SF planning analysis neighborhood
- `supervisorDistrict` — Board of Supervisors district (1-11)
- `zipcode`
- `latitude`, `longitude`
- `numberOfExistingStories`, `numberOfProposedStories`
- `estimatedCost`, `revisedCost` (USD)
- `existingUse`, `existingUnits`, `proposedUse`, `proposedUnits`
- `plansets`
- `existingOccupancy`, `proposedOccupancy` — building-code occupancy classification
- `existingConstructionType`, `existingConstructionTypeDescription`, `proposedConstructionType`, `proposedConstructionTypeDescription`
- `structuralNotification`, `voluntarySoftStoryRetrofit`, `fireOnlyPermit`, `sitePermit`, `adu`, `primaryAddressFlag`, `reroof` — booleans, only present when the flag applies
- `tidfCompliance` — Transportation Impact Development Fee compliance code (Y/R/P/E)
- `applicationSubmissionMethod`
- `pointSource` — how the property's geolocation point was derived
- `recordId` — DBI's internal record identifier (links related permit applications)
- `dataAsOf` — when SF Open Data last refreshed this row
- `rowId` — stable Socrata row identifier
- `sourceUrl` — direct link to this exact record on SF's open-data API
- `recordType: "buildingPermit"`, `scrapedAt`

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `mode` | string | `search` | `search` (filter/browse), `byPermitNumber`, `byAddress`, or `byBlockLot` |
| `searchQuery` | string | – | Full-text search across description/address text fields (mode=search) |
| `descriptionKeyword` | string | – | Case-insensitive substring match on the work description |
| `permitNumbers` | array | – | Exact permit numbers to fetch (mode=byPermitNumber) |
| `streetNumber` / `streetName` | string | – | Street number + name for an address lookup (mode=byAddress) |
| `streetSuffix` / `unit` | string | – | Optional narrowing for an address lookup (mode=byAddress) |
| `block` / `lot` | string | – | Assessor block & lot for a parcel lookup (mode=byBlockLot) |
| `permitType` | string | any | One of 9 DBI permit types (new construction, alterations, demolitions, signs, etc.) |
| `status` | string | any | One of 23 current permit statuses (issued, complete, expired, filed, etc.) |
| `neighborhood` | string | any | One of 41 SF analysis neighborhoods |
| `supervisorDistrict` | string | any | Board of Supervisors district 1-11 |
| `zipcode` | string | any | One of 30 SF zip codes |
| `existingUse` / `proposedUse` | string | any | One of 99 standardized property-use categories |
| `existingConstructionType` / `proposedConstructionType` | string | any | Building-code construction-type classification |
| `filedDateFrom` / `filedDateTo` | string | – | ISO date range (YYYY-MM-DD) on the filed date |
| `issuedDateFrom` / `issuedDateTo` | string | – | ISO date range (YYYY-MM-DD) on the issued date |
| `minEstimatedCost` / `maxEstimatedCost` | number | – | Estimated construction cost bounds (USD, 0–1,000,000,000) |
| `nearLatitude` / `nearLongitude` | number | – | Only include permits within `nearRadiusMeters` of this point (mode=search) |
| `nearRadiusMeters` | number | `500` | Radius in meters for the geographic filter (1–5000); only applies when both lat/lon are set |
| `aduOnly` | boolean | `false` | Only include Accessory Dwelling Unit permits |
| `fireOnlyPermitOnly` | boolean | `false` | Only include fire-department-only permits |
| `sitePermitOnly` | boolean | `false` | Only include site permits (large multi-phase projects) |
| `structuralNotificationOnly` | boolean | `false` | Only include permits with a structural notification |
| `voluntarySoftStoryRetrofitOnly` | boolean | `false` | Only include voluntary soft-story seismic retrofit permits |
| `reroofOnly` | boolean | `false` | Only include reroofing permits |
| `sortBy` | string | `filedDateDesc` | Sort order — filed date, issued date, status date, or estimated cost |
| `appToken` | string | – | Optional free Socrata app token for higher rate limits |
| `maxItems` | int | `50` | Hard cap on emitted records (1–10000) |

#### Example: browse the latest filed permits

```json
{
  "mode": "search",
  "sortBy": "filedDateDesc",
  "maxItems": 20
}
```

#### Example: lookup by permit number

```json
{
  "mode": "byPermitNumber",
  "permitNumbers": ["201903226060"]
}
```

#### Example: all permits at a specific address

```json
{
  "mode": "byAddress",
  "streetNumber": "760",
  "streetName": "14th",
  "streetSuffix": "St"
}
```

#### Example: all permits on a specific assessor parcel

```json
{
  "mode": "byBlockLot",
  "block": "3537",
  "lot": "027"
}
```

#### Example: high-value new-construction permits in a neighborhood

```json
{
  "mode": "search",
  "permitType": "1",
  "neighborhood": "South of Market",
  "minEstimatedCost": 500000,
  "maxItems": 100
}
```

#### Example: voluntary soft-story seismic retrofit permits

```json
{
  "mode": "search",
  "voluntarySoftStoryRetrofitOnly": true,
  "maxItems": 200
}
```

#### Example: ADU permits filed in 2024

```json
{
  "mode": "search",
  "aduOnly": true,
  "filedDateFrom": "2024-01-01",
  "filedDateTo": "2024-12-31",
  "maxItems": 200
}
```

#### Example: permits within 500m of a specific point (e.g. Civic Center)

```json
{
  "mode": "search",
  "nearLatitude": 37.7793,
  "nearLongitude": -122.4193,
  "nearRadiusMeters": 500,
  "maxItems": 100
}
```

### Use cases

- **Real estate research** — check permit history, open work, and estimated construction cost for a property before buying or renting
- **Contractor / architect prospecting** — find recent new-construction or alteration permits by neighborhood, use type, or cost range
- **Seismic-safety tracking** — monitor voluntary soft-story retrofit uptake across the city
- **ADU market research** — track Accessory Dwelling Unit permit volume and location trends
- **Journalism & policy research** — investigate construction trends, demolition activity, or permit-processing timelines
- **Academic research** — bulk-export permit data for urban-planning or housing studies

### FAQ

**What is the data source?**
The San Francisco Department of Building Inspection's "Building Permits" dataset, published on SF Open Data (Socrata, dataset ID `i98e-djp9`) and refreshed regularly by the city.

**Is this affiliated with the City and County of San Francisco or DBI?**
No. This is an independent, third-party actor built on SF's public open-data API.

**Why does the actor use `data.sfgov.org` instead of `sfdbi.org`?**
`sfdbi.org` is a legacy domain that now redirects to `www.sf.gov`, which sits behind a WAF challenge that blocks direct automated access. The DBI's actual permit records are published in full by the city on its official open-data platform, `data.sfgov.org` — the same underlying dataset that powers SF DBI's public permit lookups — so this actor reads directly from that authoritative source instead.

**What does `estimatedCost` vs `revisedCost` mean?**
`estimatedCost` is the cost declared at filing time; `revisedCost` reflects any cost revision made during plan review or construction. When no revision was recorded, only `estimatedCost` appears.

**What does `tidfCompliance` mean?**
The Transportation Sustainability/Impact Development Fee compliance code: `Y` (paid/compliant), `R` (reduced), `P` (pending), or `E` (exempt). Only present when DBI recorded a value.

**Why do some construction-type fields show unusual values like `V`, `B`, or `III` instead of `1`–`5`?**
This mirrors data-entry inconsistencies present in the source dataset itself — DBI sometimes records construction type using building-code Roman-numeral/letter designations instead of the standard numeric 1–5 codes. The actor passes through whatever value DBI's system recorded rather than guessing a normalization.

**Why do some permits have no `structuralNotification`/`voluntarySoftStoryRetrofit`/`fireOnlyPermit`/`sitePermit`/`reroof` field?**
These are opt-in flags DBI only records as `Y` when they apply; when a permit doesn't carry the flag, the field is omitted rather than shown as `false`.

**How does the address lookup (`byAddress`) work?**
Supply `streetNumber` and `streetName` (required together); optionally add `streetSuffix` and/or `unit` to narrow the match further. Matching on `streetName` is case-insensitive and exact (not fuzzy), matching DBI's own address indexing.

**How fresh is the data?**
SF Open Data refreshes this dataset regularly (the city notes that as of December 2024 it publishes new/changed rows incrementally). Each record includes a `dataAsOf` field showing when it was last synced upstream from DBI's permit-tracking system.

**Are there rate limits?**
The Socrata API allows unauthenticated access with reasonable limits. Supplying a free Socrata app token (optional) raises those limits, but the actor works without one.

# Actor input Schema

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

What to fetch.

## `searchQuery` (type: `string`):

Free-text search across permit description, address, and other text fields (mode=search). Leave blank to browse without a text query.

## `descriptionKeyword` (type: `string`):

Case-insensitive substring match on the permit's work description. Example: `kitchen remodel`.

## `permitNumbers` (type: `array`):

Exact SF DBI permit numbers to fetch. Example: `201903226060`.

## `streetNumber` (type: `string`):

Building street number. Example: `760`.

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

Street name, without the suffix (St/Ave/Blvd). Example: `Market`.

## `streetSuffix` (type: `string`):

Optional street suffix to narrow the address match. Example: `St`.

## `unit` (type: `string`):

Optional unit/suite number to narrow the address match.

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

Assessor's block number. Example: `3537`.

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

Assessor's lot number. Example: `027`.

## `permitType` (type: `string`):

Filter to a single DBI permit type.

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

Filter to a single current permit status.

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

Filter to a single SF analysis neighborhood.

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

Filter to a single SF Board of Supervisors district (1-11).

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

Filter to a single SF zip code.

## `existingUse` (type: `string`):

Filter by the property's existing (pre-permit) use category.

## `proposedUse` (type: `string`):

Filter by the property's proposed (post-permit) use category.

## `existingConstructionType` (type: `string`):

Filter by the building's existing construction type code.

## `proposedConstructionType` (type: `string`):

Filter by the building's proposed construction type code.

## `filedDateFrom` (type: `string`):

Drop permits filed before this date.

## `filedDateTo` (type: `string`):

Drop permits filed after this date.

## `issuedDateFrom` (type: `string`):

Drop permits issued before this date.

## `issuedDateTo` (type: `string`):

Drop permits issued after this date.

## `minEstimatedCost` (type: `number`):

Drop permits with a lower estimated construction cost than this.

## `maxEstimatedCost` (type: `number`):

Drop permits with a higher estimated construction cost than this.

## `nearLatitude` (type: `number`):

Only include permits within `nearRadiusMeters` of this latitude/longitude point. Must be combined with Near longitude. Example: `37.7749` (SF Civic Center area).

## `nearLongitude` (type: `number`):

Longitude for the geographic radius filter. Must be combined with Near latitude. Example: `-122.4194`.

## `nearRadiusMeters` (type: `number`):

Radius in meters around the Near latitude/longitude point. Only applies when both are set.

## `aduOnly` (type: `boolean`):

Only include permits for Accessory Dwelling Units.

## `fireOnlyPermitOnly` (type: `boolean`):

Only include permits flagged as fire-department-only permits.

## `sitePermitOnly` (type: `boolean`):

Only include permits flagged as site permits (large multi-phase projects).

## `structuralNotificationOnly` (type: `boolean`):

Only include permits that carry a structural notification flag.

## `voluntarySoftStoryRetrofitOnly` (type: `boolean`):

Only include permits flagged as voluntary soft-story seismic retrofits.

## `reroofOnly` (type: `boolean`):

Only include permits flagged as reroofing work.

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

Sort order for results.

## `appToken` (type: `string`):

Optional free Socrata app token to raise API rate limits. Get one at https://data.sfgov.org/profile/app\_tokens. Not required — the actor works without it.

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

Hard cap on emitted records.

## Actor input object example

```json
{
  "mode": "search",
  "permitNumbers": [],
  "permitType": "",
  "status": "",
  "neighborhood": "",
  "supervisorDistrict": "",
  "zipcode": "",
  "existingUse": "",
  "proposedUse": "",
  "existingConstructionType": "",
  "proposedConstructionType": "",
  "nearRadiusMeters": 500,
  "aduOnly": false,
  "fireOnlyPermitOnly": false,
  "sitePermitOnly": false,
  "structuralNotificationOnly": false,
  "voluntarySoftStoryRetrofitOnly": false,
  "reroofOnly": false,
  "sortBy": "filedDateDesc",
  "maxItems": 50
}
```

# Actor output Schema

## `permits` (type: `string`):

Dataset containing all scraped SF building permit 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",
    "permitNumbers": [],
    "permitType": "",
    "status": "",
    "neighborhood": "",
    "supervisorDistrict": "",
    "zipcode": "",
    "existingUse": "",
    "proposedUse": "",
    "existingConstructionType": "",
    "proposedConstructionType": "",
    "nearRadiusMeters": 500,
    "aduOnly": false,
    "fireOnlyPermitOnly": false,
    "sitePermitOnly": false,
    "structuralNotificationOnly": false,
    "voluntarySoftStoryRetrofitOnly": false,
    "reroofOnly": false,
    "sortBy": "filedDateDesc",
    "maxItems": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("crawlerbros/sf-building-permit-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",
    "permitNumbers": [],
    "permitType": "",
    "status": "",
    "neighborhood": "",
    "supervisorDistrict": "",
    "zipcode": "",
    "existingUse": "",
    "proposedUse": "",
    "existingConstructionType": "",
    "proposedConstructionType": "",
    "nearRadiusMeters": 500,
    "aduOnly": False,
    "fireOnlyPermitOnly": False,
    "sitePermitOnly": False,
    "structuralNotificationOnly": False,
    "voluntarySoftStoryRetrofitOnly": False,
    "reroofOnly": False,
    "sortBy": "filedDateDesc",
    "maxItems": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("crawlerbros/sf-building-permit-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",
  "permitNumbers": [],
  "permitType": "",
  "status": "",
  "neighborhood": "",
  "supervisorDistrict": "",
  "zipcode": "",
  "existingUse": "",
  "proposedUse": "",
  "existingConstructionType": "",
  "proposedConstructionType": "",
  "nearRadiusMeters": 500,
  "aduOnly": false,
  "fireOnlyPermitOnly": false,
  "sitePermitOnly": false,
  "structuralNotificationOnly": false,
  "voluntarySoftStoryRetrofitOnly": false,
  "reroofOnly": false,
  "sortBy": "filedDateDesc",
  "maxItems": 50
}' |
apify call crawlerbros/sf-building-permit-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,crawlerbros/sf-building-permit-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/y2bbPPzyQcI2YdBVd/builds/uEsiTcau6by9qvmKT/openapi.json
