# Building Permits Scraper: 12 Cities, Contractor Leads (`themineworks/building-permits-construction-leads`) Actor

Building permits from 12 US and Canadian city open data portals in ONE schema: contractor name, phone, licence, owner, valuation and a derived project class (solar, roofing, HVAC, new build). Plus a contractor rollup. No API key, no proxy.

- **URL**: https://apify.com/themineworks/building-permits-construction-leads.md
- **Developed by:** [The Mine Works](https://apify.com/themineworks) (community)
- **Categories:** Business, Lead generation, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-usage

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

## 🏗️ Building Permits Scraper: 12 Cities, Contractor Leads

> ⚡ Public municipal open data over plain HTTP. No API key, no proxy, no browser, no login.

> 💸 You are charged only for a permit that actually lands in your dataset. Contractor rollup rows and the run summary are free.

### Overview

Every US city publishes its building permits. Almost none of them publish it the same way. Chicago calls the scope of work `work_description`, Seattle calls it `description`, Philadelphia calls it `approvedscopeofwork`, and Boston writes the job value as the string `"$45,000.00"` while Austin writes it as a number spread across five different valuation columns depending on the trade. Chicago does not have a contractor column at all: it has `contact_1_type` through `contact_15_type`, and the contractor is whichever one has the word CONTRACTOR in its role.

That is the actual work, and it is what this actor does. It pulls permits from twelve municipal portals across three different open data platforms, normalises them into one row shape, and derives the two fields that make a permit useful commercially: a **project class** and a **contractor identity**.

✅ 12 cities, one schema · ✅ Contractor name, phone and licence where published · ✅ Derived project class · ✅ Free contractor rollup · ✅ MCP ready for AI agents

### Cities covered

| Key | City | Platform | Contractor published |
| --- | --- | --- | --- |
| `new_york_ny` | New York, NY (DOB NOW) | Socrata | ✅ business name + licence |
| `chicago_il` | Chicago, IL | Socrata | ✅ name + trade + city |
| `austin_tx` | Austin, TX | Socrata | ✅ name + phone + trade |
| `philadelphia_pa` | Philadelphia, PA | Carto | ✅ name + city |
| `new_orleans_la` | New Orleans, LA | Socrata | ✅ name + trade + licence |
| `baton_rouge_la` | Baton Rouge, LA | Socrata | ✅ name + owner |
| `seattle_wa` | Seattle, WA | Socrata | ✅ when the city fills it |
| `boston_ma` | Boston, MA | CKAN | applicant name only |
| `san_francisco_ca` | San Francisco, CA | Socrata | not published |
| `cincinnati_oh` | Cincinnati, OH | Socrata | not published |
| `montgomery_county_md` | Montgomery County, MD (residential) | Socrata | not published |
| `edmonton_ab` | Edmonton, AB | Socrata | not published |

The last column is stated plainly rather than implied, because a permit dataset that does not name the contractor cannot be turned into a contractor lead by any amount of scraping. Set `contractorLeadsOnly` to `true` and the run keeps only rows that actually carry a company name.

### The derived project class

A raw permit type reads `EXPRESS PERMIT PROGRAM` or `OTC alterations permit`. Nobody buys leads on that. The actor classifies every permit from its combined type, work class and description text into one of: `solar`, `ev_charger`, `battery_storage`, `roofing`, `pool_spa`, `hvac`, `new_construction`, `adu`, `demolition`, `fence_deck`, `sign`, `electrical`, `plumbing`, `fire_safety`, `elevator`, `addition`, `remodel`, `other`.

Order matters in the classifier. Solar is tested before electrical because a residential solar install is almost always filed as an electrical permit, and pools before plumbing for the same reason. So a search for `solar` returns the rooftop PV jobs rather than three thousand service upgrades.

### Input

```json
{
  "cities": ["austin_tx", "chicago_il", "philadelphia_pa"],
  "daysBack": 30,
  "projectClasses": ["solar", "roofing"],
  "minValuation": 5000,
  "contractorLeadsOnly": true,
  "contractorRollup": true,
  "maxPermitsPerCity": 500
}
```

| Field | What it takes |
| --- | --- |
| `cities` | One or more city keys from the table above. Unknown keys are reported and skipped rather than silently ignored. |
| `daysBack` | How far back to look, counted from the permit issue date. |
| `projectClasses` | Keep only these derived classes. Empty means every class. |
| `keywords` | Keep only permits whose type, work class, description or contractor name contains one of these words. |
| `minValuation` | Drop permits below this declared job value. Permits with no published value are dropped when this is set. |
| `contractorLeadsOnly` | Keep only permits that name a contractor company. |
| `contractorRollup` | Append one aggregated row per contractor at the end of the run. |
| `maxPermitsPerCity` | Hard cap per city, and your main cost control. |

### Output

Every permit row:

```json
{
  "city": "austin_tx",
  "city_name": "Austin, TX",
  "state": "TX",
  "permit_number": "2026-102151 EP",
  "permit_type": "Electrical Permit",
  "work_class": "Residential Repair",
  "work_description": "Installation of a new residential roof-mounted solar photovoltaic system with battery energy storage...",
  "project_class": "solar",
  "status": "Active",
  "issued_date": "2026-08-12T00:00:00",
  "valuation_usd": 20000,
  "address": "14016 TYBURN TRL",
  "postal_code": "78717",
  "latitude": 30.48875096,
  "longitude": -97.79060856,
  "contractor_name": "roofio LLC",
  "contractor_phone": "5129223034",
  "contractor_trade": "Electrical Contractor",
  "has_contractor": true,
  "source": "City of Austin issued construction permits"
}
```

And one free rollup row per contractor:

```json
{
  "_type": "contractor",
  "contractor_name": "LS CONTRACTING GROUP, INC.",
  "permit_count": 2,
  "total_valuation_usd": 315840,
  "avg_valuation_usd": 157920,
  "cities": ["Chicago, IL"],
  "trades": ["GENERAL CONTRACTOR", "MASON CONTRACTOR"],
  "project_classes": ["addition", "remodel"],
  "first_permit_date": "2026-07-28T00:00:00",
  "last_permit_date": "2026-08-11T00:00:00"
}
```

Rollup grouping normalises legal suffixes, so `ROOFIO LLC`, `Roofio, L.L.C.` and `roofio llc` are one contractor while `Roofio Austin` stays separate.

### Common use cases

**Solar, roofing and HVAC lead generation.** Filter to your class and city, get the job site address, the owner and the contractor who won the job. New permits appear within a day or two of issue.

**Contractor prospecting.** Sell to contractors instead of homeowners. The rollup ranks every contractor by total job value in the window, so your list is ordered by who is actually building.

**Construction market intelligence.** Count permits and dollars by class, city and month to size a market or track where activity is moving.

**Supplier and manufacturer territory planning.** Permit valuations and volumes are a leading indicator of material demand by postal code.

**Property and investment research.** New construction, ADU and demolition permits flag parcels in transition before they reach a listing site.

**AI agents.** Point an agent at it through MCP and ask which contractors pulled the most solar permits in Austin last month.

### Cost control built in

Three guards run on every job, because a run that returns nothing should also cost nothing.

1. **Run level spend cap.** A per city page cap is not a cost guard while the city count is unbounded, so the ceiling applies to the whole run rather than to any one city.
2. **Zero yield abort.** Two consecutive pages that add nothing new and the city is dropped. One productive page resets the counter, so a healthy run never trips it.
3. **No transport escalation.** A portal that does not answer is skipped and reported in the summary. There is no retry through a paid proxy or a browser, because escalating on a run that is already failing costs many times more and recovers almost nothing.

### Pricing

| Event | Price | You pay when |
| --- | --- | --- |
| Permit delivered | $0.001 | A permit row lands in your dataset |

**$1.00 per 1,000 permits.** Contractor rollup rows, the summary row and empty runs are free.

### Run it on a schedule

1. Run once with the cities and classes you want, then click **Save as a task**.
2. In the Apify Console go to **Schedules → Create new**.
3. Pick a frequency and attach the saved task.
4. Wire the dataset to Sheets, Slack, a CRM or a webhook from the **Integrations** tab.

Set `daysBack` to slightly more than your schedule interval and deduplicate on `permit_number` so a daily run never reprocesses the same permit.

### FAQ

**Is this data public?**
Yes. Every source is an official municipal open data portal, published by the city itself for reuse. No login, no key, no scraping of a protected site.

**Why only twelve cities?**
Because these are the twelve that were probed live and returned real permit rows. Los Angeles requires a login, and San Diego, San Antonio, Denver, Nashville, Raleigh and Charlotte returned 404 or non JSON on their published endpoints. Dallas answers, but publishes its issue date as free text and has not been updated since 2019, so a date filtered query returns zero rows while looking healthy. A city listed here and returning nothing would cost you compute to discover, so those were left out rather than counted.

**How fresh is it?**
Most portals publish daily. In testing, permits issued the previous day were already present in Austin, Chicago, Philadelphia, Boston and New Orleans.

**Why is valuation missing on some rows?**
Because that city does not publish it, or published it as zero. It is left empty rather than guessed. Philadelphia and New Orleans zoning permits frequently have no value at all.

**Can you add my city?**
If it has a public open data portal with a permits dataset, yes. Reach out through the Apify profile with the city and dataset link.

### Use from Claude, ChatGPT and any MCP agent

```
https://mcp.apify.com/?tools=themineworks/building-permits-construction-leads
```

Or call it programmatically:

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });

const run = await client.actor('themineworks/building-permits-construction-leads').call({
  cities: ['austin_tx', 'chicago_il'],
  daysBack: 30,
  projectClasses: ['solar'],
  contractorLeadsOnly: true,
  maxPermitsPerCity: 500,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

Questions, or need a city or field we do not return yet? Reach out through the Apify profile.

# Actor input Schema

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

City keys to pull permits from. Valid keys: new\_york\_ny, chicago\_il, austin\_tx, seattle\_wa, san\_francisco\_ca, philadelphia\_pa, boston\_ma, cincinnati\_oh, montgomery\_county\_md, baton\_rouge\_la, new\_orleans\_la, edmonton\_ab. Every key was verified live against its municipal open data portal.

## `daysBack` (type: `integer`):

How far back to look, counted from the permit issue date. 30 gives you this month's activity, 365 gives a year of history for market sizing.

## `projectClasses` (type: `array`):

Keep only these derived project classes. One of solar, ev\_charger, battery\_storage, roofing, pool\_spa, hvac, new\_construction, adu, demolition, fence\_deck, sign, electrical, plumbing, fire\_safety, elevator, addition, remodel, other. Leave empty for every class.

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

Keep only permits whose type, work class, description or contractor name contains one of these words. Case insensitive. Applied on top of the project class filter.

## `minValuation` (type: `integer`):

Drop permits below this declared job value. Useful for filtering out $500 water heater swaps when you want real projects. Note that not every city publishes a valuation, and permits with no value are dropped when this is set.

## `contractorLeadsOnly` (type: `boolean`):

Keep only permits that carry a contractor company name. Turn this on when you are building a contractor prospect list rather than studying construction activity.

## `contractorRollup` (type: `boolean`):

Append one aggregated row per contractor at the end of the run with permit count, total and average job value, cities, trades and licence. These rows are free: they are derived from permits you already paid for.

## `maxPermitsPerCity` (type: `integer`):

Hard cap per city. This is your main cost and runtime control.

## Actor input object example

```json
{
  "cities": [
    "austin_tx"
  ],
  "daysBack": 30,
  "projectClasses": [
    "solar"
  ],
  "keywords": [],
  "minValuation": 0,
  "contractorLeadsOnly": false,
  "contractorRollup": true,
  "maxPermitsPerCity": 60
}
```

# Actor output Schema

## `results` (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 = {
    "cities": [
        "austin_tx"
    ],
    "daysBack": 30,
    "projectClasses": [
        "solar"
    ],
    "keywords": [],
    "minValuation": 0,
    "contractorLeadsOnly": false,
    "contractorRollup": true,
    "maxPermitsPerCity": 60
};

// Run the Actor and wait for it to finish
const run = await client.actor("themineworks/building-permits-construction-leads").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": ["austin_tx"],
    "daysBack": 30,
    "projectClasses": ["solar"],
    "keywords": [],
    "minValuation": 0,
    "contractorLeadsOnly": False,
    "contractorRollup": True,
    "maxPermitsPerCity": 60,
}

# Run the Actor and wait for it to finish
run = client.actor("themineworks/building-permits-construction-leads").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": [
    "austin_tx"
  ],
  "daysBack": 30,
  "projectClasses": [
    "solar"
  ],
  "keywords": [],
  "minValuation": 0,
  "contractorLeadsOnly": false,
  "contractorRollup": true,
  "maxPermitsPerCity": 60
}' |
apify call themineworks/building-permits-construction-leads --silent --output-dataset

```

## MCP server setup

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

```

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/IPmom8BmKl3PvLktm/builds/4YIPl9QXQJ9xWtFir/openapi.json
