# San Francisco Building Permits Feed: DataSF Open Data (PDDL) (`fecklesspleb/city-building-permits-feed`) Actor

New building permits from San Francisco's DataSF open data API (public domain, PDDL 1.0): address, parcel, work description, permit type, status, dates, estimated cost, uses, units and map point. Keyword, cost and ZIP filters, dedupe across runs. Unofficial.

- **URL**: https://apify.com/fecklesspleb/city-building-permits-feed.md
- **Developed by:** [Scott Williams](https://apify.com/fecklesspleb) (community)
- **Categories:** Real estate, Lead generation, Business
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 building permits

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## San Francisco Building Permits Feed: Daily Permit Data from DataSF

A daily feed of new **San Francisco building permits**, read from the City's official open data portal (DataSF) through its keyless Socrata SODA API.

- **Source:** the DataSF "Building Permits" dataset (`i98e-djp9`), published by the City and County of San Francisco under the **Open Data Commons Public Domain Dedication and License (PDDL) 1.0**.
- **What each record holds:** every permit becomes one flat record with the address, parcel, work description, permit type, status, filed, issued and completed dates, estimated cost, existing and proposed use, units, stories, neighborhood, supervisor district and map point.
- **Filters:** keyword, minimum estimated cost or ZIP code.
- **Scheduled runs** only return permits you haven't already received. This is a new-permits feed: when a permit you already received changes status later, it is not sent again.

> **Unofficial.** This Actor is not affiliated with or endorsed by the City and County of San Francisco. City data can change or be corrected after it is published, so check the source record (`sourceUrl`) before you rely on it.

### Who uses building permit data

- **Contractors, solar installers, roofers and suppliers** looking for active projects. Filter by `keyword` (for example `solar`, `ADU` or `demolition`) or `minEstimatedCost`.
- **Real estate and proptech teams** watching renovation, unit changes and new construction by ZIP code or neighborhood.
- **Analysts and journalists** tracking permit volume and project value over time with `date_range` mode.

### Privacy and responsible use

The DataSF building permits dataset has **no owner, tenant, applicant or contact fields**, and the Actor adds no data from other sources. The work-site street address is part of every record, and that may be a private home.

**Responsible use:** business or contractor names can contain individuals' names, for example a sole trader's business name, or a name written into the free-text description or use fields. You must follow the FCRA, TCPA and CAN-SPAM, and you must not use this data for consumer eligibility decisions (credit, employment, insurance or housing).

### Input

| Input | Default | Notes |
|---|---|---|
| `mode` | `daily_feed` | `daily_feed` reads the last `lookbackDays`. `date_range` reads `startDate` to `endDate` (up to 10 years, from 1980-01-01 onward). |
| `lookbackDays` | 7 | 0 to 90 days, counted in Pacific time. Used in `daily_feed` mode. |
| `startDate`, `endDate` | none | `YYYY-MM-DD`, for `date_range` mode. |
| `dateBasis` | `issued` | `issued` (issue date) or `filed` (application filed date). |
| `keyword` | none | Searches the work description, ignoring case. |
| `minEstimatedCost` | none | In USD. The Console form is prefilled with 50000. |
| `zipCodes` | none | 5-digit San Francisco ZIP codes, for example `94110`. |
| `maxItems` | 4 | Also 4 when left empty. |
| `onlyNewSinceLastRun`, `stateKey` | `auto`, `default` | `auto` skips permits already delivered, in `daily_feed` mode only. `yes` and `no` force it on or off. Use one `stateKey` per schedule. |

Example: large projects, checked daily.

```json
{"mode": "daily_feed", "lookbackDays": 3, "minEstimatedCost": 500000, "maxItems": 500}
```

### Output: San Francisco permit records

This is a real record from a test run on 30 October 2026, with `{"maxItems": 3}`:

```json
{
  "city": "san-francisco",
  "cityName": "San Francisco, CA",
  "permitNumber": "202610290111",
  "permitType": "otc alterations permit",
  "status": "issued",
  "statusDate": "2026-10-29",
  "filedDate": "2026-10-29",
  "issuedDate": "2026-10-29",
  "completedDate": null,
  "description": "like for like replacement east facing wall in garage area. remove damaged mudsill studs, framing and sheathing. install all new like for like new framing, mudsill sheath, waterproof per plan. re-establish connections. no change to use or foot print",
  "estimatedCost": 25000,
  "address": "22 Tioga Av",
  "unit": null,
  "zipCode": "94134",
  "neighborhood": "Visitacion Valley",
  "district": "Supervisor district 10",
  "parcel": "6192/010",
  "existingUse": "1 family dwelling",
  "proposedUse": "1 family dwelling",
  "existingUnits": 1,
  "proposedUnits": 1,
  "proposedStories": 2,
  "latitude": 37.715988733,
  "longitude": -122.403159677,
  "sourceUrl": "https://data.sf.gov/resource/i98e-djp9.json?permit_number=202610290111",
  "datasetUrl": "https://data.sf.gov/d/i98e-djp9",
  "licence": "Open Data Commons Public Domain Dedication and License (PDDL) 1.0",
  "licenceUrl": "https://opendatacommons.org/licenses/pddl/1-0/",
  "attribution": "Source: DataSF, City and County of San Francisco (data.sf.gov). Unofficial; not endorsed by the City.",
  "generatedInternalId": "san-francisco:202610290111",
  "scrapedAt": "2026-10-31T01:33:24+00:00"
}
```

Text fields such as `description` and `permitType` are passed through exactly as the City publishes them.

The run summary (key-value store record `RUN_SUMMARY`) records:

- how many permits were returned;
- how many were skipped as already seen or duplicated;
- how many requests were made.

### Pricing

Pricing is pay per event: **$0.002 per permit** returned, which is $2 per 1,000 permits.

- There is no start fee and there are no other events.
- Permits skipped as already delivered are free.
- Invalid input (not a JSON object, a field of a type that cannot be read such as `zipCodes` given as a number, or a value that is not allowed such as an unknown mode, a bad date or a ZIP code that is not 5 digits) fetches nothing and ends with one free status row: `Invalid input, nothing was fetched or charged: <reason>`.
- An empty input `{}` or the prefilled Console input returns at most 4 permits, which costs at most $0.008.

San Francisco issued between 53 and 68 permits each weekday from 15 to 28 October 2026 (counted from DataSF). So a daily feed with no filters cost roughly $0.11 to $0.14 a weekday over that period. Set a maximum cost per run to stop early.

### Use it from n8n, Make or Zapier

Schedule the Actor in Apify, or trigger it from Make, Zapier or n8n through the Apify integrations or the Apify API. Then read the dataset items, for example to send new permits above a cost threshold to a CRM, a spreadsheet or a team chat. Using `onlyNewSinceLastRun` (on by default in `daily_feed` mode) means each scheduled run only delivers permits you haven't received yet. The Apify Console's API tab for this Actor shows the exact endpoint URLs.

### FAQ

**Which cities are covered?** San Francisco only. Its dataset has a named open licence (PDDL 1.0) that was checked. Other cities will be added only after their licences have been read.

**Will I get a permit again when its status changes?** No. This is a new-permits feed. Dedupe is by permit number, so a permit you have already received is not sent again in later scheduled runs, even when its status or dates change. To see current statuses, run `date_range` mode with `onlyNewSinceLastRun: "no"`.

**Is `estimatedCost` the real project cost?** No. It's the applicant's estimate as published, not the final value.

**Do I need an API key or proxy?** No. DataSF's SODA API is keyless. The Actor checks robots.txt on every run, honours the portal's `Crawl-delay: 1`, makes at most 1 request per second and sends a descriptive User-Agent. If the portal blocks it, or returns anything other than JSON, the run stops cleanly.

**Why did I get 0 permits?** The likely reasons:

- your filters are too narrow;
- the lookback window has no new issued permits, for example a weekend;
- every permit in the window was already delivered to this `stateKey`.

Check `RUN_SUMMARY`.

### Data source and licence

San Francisco data comes from DataSF, City and County of San Francisco, under the [PDDL 1.0](https://opendatacommons.org/licenses/pddl/1-0/).

# Changelog

This Actor's version history is a separate document: https://apify.com/fecklesspleb/city-building-permits-feed/changelog.md

# Actor input Schema

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

daily_feed = the last N days (schedule it daily; only new permits are returned on each run). date_range = a fixed window.

## `dateBasis` (type: `string`):

issued = permit issue date (issued_date). filed = application filed date (filed_date).

## `lookbackDays` (type: `integer`):

Daily feed only: how many days back from today (Pacific time) to include.

## `startDate` (type: `string`):

Date range only, YYYY-MM-DD.

## `endDate` (type: `string`):

Date range only, YYYY-MM-DD (default today).

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

Optional, case-insensitive, e.g. solar, ADU, roof, demolition, EV.

## `minEstimatedCost` (type: `integer`):

Optional. Skip permits whose estimated job cost is lower.

## `zipCodes` (type: `array`):

Optional 5-digit San Francisco ZIP codes, e.g. 94110.

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

Hard cap on permits saved (and charged) per run. Default 4 (at most $0.008).

## `onlyNewSinceLastRun` (type: `string`):

auto = yes in daily feed mode, no in date range mode.

## `stateKey` (type: `string`):

Use a different key per saved task so their dedupe memories do not mix.

## Actor input object example

```json
{
  "mode": "daily_feed",
  "dateBasis": "issued",
  "lookbackDays": 7,
  "minEstimatedCost": 50000,
  "maxItems": 4,
  "onlyNewSinceLastRun": "auto",
  "stateKey": "default"
}
```

# Actor output Schema

## `documents` (type: `string`):

No description

## `summary` (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 = {
    "minEstimatedCost": 50000,
    "maxItems": 4
};

// Run the Actor and wait for it to finish
const run = await client.actor("fecklesspleb/city-building-permits-feed").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 = {
    "minEstimatedCost": 50000,
    "maxItems": 4,
}

# Run the Actor and wait for it to finish
run = client.actor("fecklesspleb/city-building-permits-feed").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 '{
  "minEstimatedCost": 50000,
  "maxItems": 4
}' |
apify call fecklesspleb/city-building-permits-feed --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,fecklesspleb/city-building-permits-feed"
        }
    }
}
```

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/7kFsKRIrTr74GuceT/builds/T1T1MMBZ8pzw3owpF/openapi.json
