# Building Permit Monitor — 30 US Cities, New Permits, No Login (`chimerical_quicklime/building-permit-monitor`) Actor

Watch 30 US cities (NYC, LA, Chicago, SF, Seattle, Denver, Boston and more) and get only NEW building permits since the last run, filtered by type, keyword, value or contractor, with address, coordinates, value and contractor license. Daily schedule. No login. MCP-ready. $10 per 1,000 permits.

- **URL**: https://apify.com/chimerical\_quicklime/building-permit-monitor.md
- **Developed by:** [Khrystyna Skotte](https://apify.com/chimerical_quicklime) (community)
- **Categories:** Real estate, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 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

## Building Permit Monitor — new permits daily across 30 US cities

Get a **daily feed of only the NEW building permits** (and status changes) issued across NYC, Los Angeles,
Chicago, San Francisco, Seattle, Denver, Boston, Philadelphia, Nashville, Washington DC, Detroit, Miami and
18 more cities, filtered by **permit type, job value, description keywords and contractor**. The monitor
remembers every permit it has already reported, so a scheduled run emits just the delta, posts a summary to
your webhook, and costs you only for what is actually new. One actor instead of 30 single-city scrapers.

Built for contractors, building-material suppliers, and solar / roofing / HVAC / plumbing lead-gen teams who
need to know *today* which jobs were just permitted, where, for how much, and by whom.

### Coverage

All sources are official open-data feeds (Socrata, ArcGIS FeatureServer, CKAN, Carto). Every one was verified
live on 2026-09-28. "Typical lag" is how far behind the calendar the newest published permit usually is — set
`lookbackDays` above the lag for slow cities.

| Key | City | Source | Issue-date field | Typical lag | Contractor | Value |
|---|---|---|---|---|---|---|
| `nyc` | New York City, NY | DOB NOW: Build Approved Permits (Socrata `rbx6-tga4`) | `issued_date` | daily | yes + license | yes |
| `los-angeles` | Los Angeles, CA | LADBS Permits (Socrata `pi9x-tg5x`) | `issue_date` | daily | no | yes |
| `chicago` | Chicago, IL | Building Permits (Socrata `ydr8-5enu`) | `issue_date` | daily | yes | yes |
| `san-francisco` | San Francisco, CA | DBI Building Permits (Socrata `i98e-djp9`) | `issued_date` | daily | no | yes |
| `seattle` | Seattle, WA | SDCI Building Permits (Socrata `76t5-zqzr`) | `issueddate` | daily | no | yes |
| `austin` | Austin, TX | Issued Construction Permits (Socrata `quv8-5ckq`) | `issue_date` | 2–4 weeks | no | yes |
| `denver` | Denver, CO | Residential + Commercial Construction Permits (ArcGIS) | `DATE_ISSUED` | daily | yes | yes |
| `boston` | Boston, MA | Approved Building Permits (CKAN) | `issued_date` | daily | applicant | yes |
| `philadelphia` | Philadelphia, PA | L\&I Permits (Carto `permits`) | `permitissuedate` | daily | yes | no |
| `nashville` | Nashville, TN | Building Permits Issued (ArcGIS) | `Date_Issued` | daily | contact | yes |
| `washington-dc` | Washington, DC | Building Permits in Last 30 Days (ArcGIS) | `ISSUE_DATE` | daily, 30-day window | applicant | no |
| `detroit` | Detroit, MI | BSEED Building Permits (ArcGIS) | `issued_date` | daily | no | yes |
| `fort-worth` | Fort Worth, TX | Development Permits, status = Issued (ArcGIS) | `Status_Date` | daily | no | yes |
| `charlotte` | Charlotte / Mecklenburg County, NC | Building Permits (ArcGIS) | `issuedate` | daily | no | yes |
| `raleigh` | Raleigh, NC | Building Permits (ArcGIS) | `issueddate` | daily | yes + license | yes |
| `columbus` | Columbus, OH | Building Permits (ArcGIS) | `ISSUED_DT` | daily | applicant business | yes |
| `cleveland` | Cleveland, OH | Building Permits (ArcGIS) | `ISSUE_DATE` | daily | yes + license | yes |
| `minneapolis` | Minneapolis, MN | CCS Permits (ArcGIS) | `issueDate` | daily | applicant | yes |
| `miami` | Miami, FL | Building Permits Since 2014 (ArcGIS) | `IssuedDate` | daily | yes | yes |
| `san-antonio` | San Antonio, TX | Building Permits (CKAN) | `DATE ISSUED` | daily | no | yes |
| `pittsburgh` | Pittsburgh, PA | PLI Permits (WPRDC CKAN) | `issue_date` | weekly | yes | yes |
| `cincinnati` | Cincinnati, OH | Building Permits (Socrata `uhjb-xac9`) | `issueddate` | daily | yes | yes |
| `mesa` | Mesa, AZ | Building Permits (Socrata `dzpk-hxfb`) | `issued_date` | daily | yes + license | yes |
| `tucson` | Tucson, AZ | Permits (ArcGIS) | `ISSUEDATE` | daily | no | yes |
| `tacoma` | Tacoma, WA | Accela Permits (ArcGIS) | `issued_date` | daily | no | yes |
| `tempe` | Tempe, AZ | Building Permits (ArcGIS) | `IssuedDateDtm` | daily | yes + license | yes |
| `scottsdale` | Scottsdale, AZ | Permits (ArcGIS) | `IssueDate` | daily | builder | yes |
| `memphis` | Memphis, TN | DPD Building Permits (ArcGIS) | `Issued_Date` | 2–4 weeks | no | yes |
| `montgomery-county-md` | Montgomery County, MD | Residential Building Permits (Socrata `m88u-pqki`) | `issueddate` | daily | no | yes |
| `portland` | Portland, OR | Building Permits (PortlandMaps ArcGIS) | `ISSUEDATE` | 4–8 weeks | no | yes |
| `dallas` | Dallas, TX | Permits 2008–2024 (ArcGIS) | `ISSUE_DATE` | **stale — feed stopped Nov 2024** | no | yes |

`cities: ["all"]` runs every live city. `dallas` is excluded from `all` and from the defaults; request it
explicitly only if you want historical data. Houston and Phoenix publish no machine-readable permit feed and
are not covered (Mesa, Tempe and Scottsdale cover much of the Phoenix metro).

### How it works

1. For each selected city, queries the official feed for permits **issued in the last `lookbackDays`**
   (server-side date filter, newest first, up to 5,000 per city per run).
2. Normalises every row into the same 20-field record (see Output) and applies your filters.
3. Compares each permit against the monitor's saved state (`city → permitNumber → status`).
4. Emits a record when the permit is unseen (`changeType: "new"`) or its status changed since last seen
   (`changeType: "status_change"`, with `previousStatus`).
5. Saves the state, then POSTs a run summary to `webhookUrl` if set.

State lives in a named key-value store `permit-monitor-<hash of monitorId>` in your Apify account, capped at
50,000 permits (oldest are dropped first). Delete the store to reset a monitor.

### Input

| Field | Default | Notes |
|---|---|---|
| `cities` | `["nyc","los-angeles","chicago"]` | City keys from the table above, or `["all"]` |
| `permitTypes` | `[]` | Case-insensitive substrings matched against permit type and work class, e.g. `["electrical","roof","demolition"]`. OR-ed |
| `minValue` | `0` | Minimum estimated/declared job value in USD. When set, permits with no published value are dropped, so Philadelphia and DC (no value field) will not match |
| `keywords` | `[]` | Case-insensitive substrings matched against the work description (plus type/class), e.g. `["solar","PV","photovoltaic"]` |
| `contractors` | `[]` | Case-insensitive substrings matched against the contractor name |
| `lookbackDays` | `7` | Permits issued within N days. Raise it for Austin, Memphis, Portland (see lag column) |
| `maxNewPerCity` | `5` | Cap per city per run. Also bounded by `maxItems / number of cities` so one big city cannot starve the others. Permits beyond the cap stay unseen and come out next run |
| `maxItems` | `10` | Overall cap per run |
| `firstRunMode` | `emitAll` | `emitAll` reports every current match on the first run; `baseline` records them silently |
| `webhookUrl` | `""` | Optional POST target for the run summary |
| `monitorId` | `default` | One state store per ID, so several watchlists can run side by side |

Where a city encodes the trade in its type or work class (Tucson `Solar`, Chicago `Electrical Work`,
Philadelphia `Electrical`), use `permitTypes`. Where it only appears in the free-text description (Los Angeles,
Mesa, Boston), use `keywords`. Using both with the same terms is safe: a permit matches if it passes each
filter you set.

### Recommended setup for a daily feed

1. Create a task with your cities and filters and set `monitorId` (`solar-southwest`, `roofing-tx`).
2. **First run: set `firstRunMode` to `baseline`** with `lookbackDays` 30. This records everything current
   and emits nothing, so day one is not a 5,000-row backlog.
3. Set `lookbackDays` back to 7 (or 30 for the slow cities), `maxNewPerCity` and `maxItems` high enough for
   your cities (NYC alone issues ~3,000 permits a week; Chicago ~500; Seattle ~50).
4. **Schedule the task daily at 07:00 in your local time zone.** Most cities refresh their feed overnight.
5. Point `webhookUrl` at Slack (incoming webhook), Zapier, Make, or your own endpoint.

The default input (`{}`) runs in `emitAll` mode on NYC, Los Angeles and Chicago so you see real permits on the
first try.

### Example: solar and roofing leads in the Southwest, jobs over $10,000

```json
{
  "cities": ["los-angeles", "mesa", "tempe", "scottsdale", "tucson"],
  "keywords": ["solar", "photovoltaic", "PV", "reroof", "re-roof", "roofing"],
  "minValue": 10000,
  "lookbackDays": 7,
  "maxNewPerCity": 200,
  "maxItems": 1000,
  "firstRunMode": "baseline",
  "webhookUrl": "https://hooks.slack.com/services/XXX/YYY/ZZZ",
  "monitorId": "solar-southwest"
}
```

Other quick profiles:

- **Commercial general contractors, big jobs**: `cities: ["nyc","chicago","denver","nashville","miami"]`, `permitTypes: ["new building","commercial","new construction"]`, `minValue: 250000`
- **Track a competitor**: `cities: ["all"]`, `contractors: ["renewal by andersen"]`
- **Demolition (site-prep, hauling, salvage)**: `permitTypes: ["demolition","demo"]`, `cities: ["all"]`

### Output

One record per new or changed permit:

```json
{
  "city": "New York City",
  "state": "NY",
  "permitNumber": "B01358922-S2-GC-CX",
  "permitType": "General Construction",
  "workClass": "Initial Permit",
  "description": "This subsequent filing SI MS/GC (Job B01358922-S1) to correct and update plan for legalization ...",
  "status": "Permit Issued",
  "issuedDate": "2026-09-25",
  "appliedDate": "2026-09-03",
  "address": "147 DIAMOND STREET, Brooklyn",
  "zip": "11222",
  "latitude": 40.727487,
  "longitude": -73.947937,
  "estimatedValue": 6050,
  "contractorName": "CLIFF REFRIGERATION & A/C",
  "contractorLicense": "GC 614451",
  "ownerName": "Maureen Marsh",
  "sourceUrl": "https://data.cityofnewyork.us/resource/rbx6-tga4.json",
  "changeType": "new",
  "previousStatus": null,
  "firstSeenAt": "2026-09-29T00:17:40.009Z",
  "monitorId": "default"
}
```

Fields a city does not publish are `null` (see the Contractor / Value columns above). `sourceUrl` is the
permit's own page where the city provides one (Austin, Seattle, Columbus, Cleveland, Cincinnati, Tacoma,
Tucson), otherwise the dataset endpoint. Descriptions are truncated to 300 characters.

### Webhook payload

```json
{
  "monitorId": "solar-southwest",
  "runAt": "2026-09-29T07:00:12.000Z",
  "since": "2026-09-22",
  "newCount": 14,
  "statusChangeCount": 2,
  "emitted": 16,
  "seenTotal": 1830,
  "baseline": false,
  "cities": { "tucson": { "fetched": 141, "matched": 18, "new": 12, "statusChange": 0, "emitted": 12, "deferred": 6, "latestIssuedDate": "2026-09-27", "error": null } },
  "permits": [ "...first 50 records..." ]
}
```

The same summary is saved as `SUMMARY` in the run's default key-value store.

### Pricing

$0.005 per run start plus $0.01 per emitted permit ($10 per 1,000). Baseline runs and runs with nothing new
cost only the start fee.

### Notes and limits

- Only permits with an issue date inside the lookback window are considered; applications not yet issued are
  not reported (except Fort Worth, where the window is the date the status became *Issued*).
- Washington DC's feed only holds the last 30 days, so `lookbackDays` above 30 has no extra effect there.
- Pittsburgh publishes weekly; Austin and Memphis run 2–4 weeks behind; Portland 4–8 weeks.
- Permit numbers are unique within a city; the state is keyed per city so the same number in two cities does
  not collide.
- No login, API key or proxy is needed. The actor is MCP-ready for AI agents.

# Actor input Schema

## `cities` (type: `array`):

City keys to monitor: nyc, los-angeles, chicago, san-francisco, seattle, austin, denver, boston, philadelphia, nashville, washington-dc, detroit, fort-worth, charlotte, raleigh, columbus, cleveland, minneapolis, miami, san-antonio, pittsburgh, cincinnati, mesa, tucson, tacoma, tempe, scottsdale, memphis, montgomery-county-md, portland. Use "all" for every live city. (dallas is available but its source stopped in Nov 2024.)

## `permitTypes` (type: `array`):

Case-insensitive substrings matched against the permit type and work class, e.g. solar, electrical, roof, plumbing, demolition, new building. A permit matches if any term matches. Empty = all types.

## `minValue` (type: `integer`):

Only permits whose estimated/declared value is at least this amount. 0 = no minimum (permits without a published value are kept).

## `keywords` (type: `array`):

Case-insensitive substrings matched against the work description (and type/class). OR-ed. Empty = no keyword filter.

## `contractors` (type: `array`):

Case-insensitive substrings matched against the contractor name. Useful for tracking competitors or your own crews. Empty = all contractors.

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

Only consider permits issued within this many days. 7 is a safe overlap for a daily schedule; some cities publish with a 2-4 week lag (see README).

## `maxNewPerCity` (type: `integer`):

Cap per city. Permits beyond the cap stay unseen and are emitted on the next run.

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

Overall cap on emitted records per run.

## `firstRunMode` (type: `string`):

What to do when the monitor has no saved state yet. emitAll: treat every current match as new and emit it. baseline: silently record every current match as seen and emit nothing, so the next scheduled run reports only what is new since.

## `webhookUrl` (type: `string`):

Optional. After each run a JSON summary {monitorId, runAt, newCount, statusChangeCount, cities{...}, permits\[first 50]} is POSTed here (Slack/Zapier/Make/your API).

## `monitorId` (type: `string`):

Name of this watchlist. Each monitor ID keeps its own seen-state in a key-value store named permit-monitor-<hash>, so you can run several profiles (e.g. "solar-southwest", "roofing-tx") side by side.

## Actor input object example

```json
{
  "cities": [
    "nyc",
    "los-angeles",
    "chicago"
  ],
  "permitTypes": [],
  "minValue": 0,
  "keywords": [],
  "contractors": [],
  "lookbackDays": 7,
  "maxNewPerCity": 5,
  "maxItems": 10,
  "firstRunMode": "emitAll",
  "webhookUrl": "",
  "monitorId": "default"
}
```

# Actor output Schema

## `records` (type: `string`):

Dataset of new or changed building permits found in this run (JSON).

# 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 = {
    "cities": [
        "nyc",
        "los-angeles",
        "chicago"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("chimerical_quicklime/building-permit-monitor").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 = { "cities": [
        "nyc",
        "los-angeles",
        "chicago",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("chimerical_quicklime/building-permit-monitor").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 '{
  "cities": [
    "nyc",
    "los-angeles",
    "chicago"
  ]
}' |
apify call chimerical_quicklime/building-permit-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,chimerical_quicklime/building-permit-monitor"
        }
    }
}
```

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/QDv3bIoE3ySK3aR91/builds/mH12xiJ5fbMDmta4A/openapi.json
