# Construction Permit Leads - Scored Daily Call List (`oldbie/construction-permit-leads`) Actor

Monitors newly issued building permits across US metros, classifies each by trade (roofing, HVAC, plumbing, electrical, foundation, remodel), scores lead quality, and emits only permits you have not seen before.

- **URL**: https://apify.com/oldbie/construction-permit-leads.md
- **Developed by:** [Travis Havard](https://apify.com/oldbie) (community)
- **Categories:** Lead generation, Automation, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $50.00 / 1,000 new permit leads

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

## Construction Permit Leads — Scored Daily Call List

**A permit is a signed statement that someone is about to spend money on construction.** This actor finds those statements the day they are filed, sorts them by how worth calling they are, and hands you a call sheet.

Built for roofers, HVAC and plumbing contractors, foundation companies, restoration crews, solar installers, and the suppliers who sell to them.

***

### Two things you will not find in another permit tool

#### 1. Houston, TX

The City of Houston stopped publishing Permit Activity Reports on **2025-12-01**. There is no open-data feed, no CSV, no API — permits are exposed only through an interactive search portal. Every other permit scraper you can buy is a wrapper around Socrata open data, so none of them cover Houston at all.

This one drives Houston's actual reporting system and returns same-day permits with owner, address, valuation and work description. It is the hardest city in the country to get permit data from, and it is the fourth-largest metro in the country.

#### 2. A call sheet, not a spreadsheet

Every run writes **`hot-leads.txt`** — plain text, best lead first, with a one-line reason for every score. You do not have to open the dataset to use this product.

```
==============================================================================
  CALL TODAY — permits scoring 75+
  generated 2026-07-29
==============================================================================

  1 lead(s) worth a call, out of 10 total this run.
  Cities: Houston, TX (1)
  Trades: Roofing (1)

------------------------------------------------------------------------------
  #1   SCORE  76   ROOFING   Houston, TX
------------------------------------------------------------------------------
  ADDRESS   : 7501 HARWIN DR BLD A 77036
  OWNER     : HARWIN WHOLESALE CENTER
  CONTRACTOR: -- not published by this city --
  VALUE     : $14,000 (estimated)
  ISSUED    : 2026-07-28
  WORK      : RETAIL ROOF OVERLAY 1-1-2-M-B 2021 IBC
  PERMIT    : C1-DUP/CHNG  #26058326
  WHY       : contractor not published by this city | est. $14,000 (city published no value)
```

Find it under **Storage → Key-value store → hot-leads.txt**, or straight off the run's Output tab. Set the threshold to 0 to turn the file off.

***

### What happens to each permit before you see it

| | |
|---|---|
| **Classified by trade** | Tagged `roofing`, `hvac`, `plumbing`, `electrical`, `foundation`, `remodel`, `new_construction`, `solar`, `pool`, `demolition`, `fence_deck`. Reads the *work description*, not just the city's filing category — which matters, because most cities file everything as one generic type. A permit typed "Addition/Alteration" that says `REPLACE ROOF STORM DAMAGE` is a roofing lead, and you get it when you filter for roofing. |
| **Tagged with every trade it qualifies for** | `Roof replacement, mechanical included` is a real job for the roofer *and* the HVAC contractor. Both of them get it. The `trades` field lists all of them; `trade` is the primary. |
| **Scored 0–100** | Weighted heavily toward **freshness**, because a three-day-old permit already has four competitors calling it. Job size, whether a contractor is attached yet, and address quality also factor in. Scores are normalized across cities, so 75 means the same thing in Houston as it does in Chicago. |
| **Filtered to your business** | Your trades, your ZIP codes, your minimum job size. |
| **Deduplicated across runs** | Run it daily and you get *only today's new permits* — never the same row twice, and never billed for one twice. |

Sort by `lead_score` descending and work top-down. That is the whole workflow.

***

### Coverage

Every city below was queried live and confirmed to be returning **current** data. A city is not listed until it passes that check.

| City | Source | Freshness | Valuation | Contractor | Site ZIP |
|---|---|---|---|---|---|
| **Houston, TX** | Permit portal | Same/next day | Yes | Not published | Yes |
| **Austin, TX** | Open data | 1 day | ~10%, rest estimated | Yes | Yes |
| **Baton Rouge, LA** | Open data | 1 day | ~66% | Yes (39% owner-pulled) | Yes |
| **Chicago, IL** | Open data | 1 day | Yes | Yes | **Not published** |
| **Montgomery County, MD** | Open data | 1 day | Yes | Not published | Yes |
| **New York City, NY** | Open data | 1 day | Yes | Yes | Yes |
| **Norfolk, VA** | Open data | 1 day | Yes | Not published | **Not published** |
| **Cincinnati, OH** | Open data | 2 days | Yes | Yes | **~8% only** |
| **Seattle, WA** | Open data | 3 days | Yes | Often blank | Yes |
| **Los Angeles, CA** | Open data | 4 days | Yes | Not published | Yes |
| **New Orleans, LA** | Open data | **~7 days** | Yes | Yes | ~66% |

That table is the product. Every column was measured against live data on
2026-07-29, not copied from a city's documentation, and every "Not published"
is a field we checked for and did not find.

Five things worth knowing before you buy:

- **Houston requires ZIP codes.** Its report needs a search term and cannot be swept city-wide. The run will stop and tell you so rather than quietly returning nothing.
- **Austin's valuations are mostly placeholders.** The city publishes the field but fills it on roughly 10% of rows, and the median filled value is **$1** — clerks typing past a required box. Those rows carry a trade-median estimate flagged `valuation_is_estimated: true` instead of a one-dollar job. Your minimum-valuation filter never discards a row on an estimated value, so you do not lose real leads to a field the city never really filled in.
- **Chicago publishes no job-site ZIP.** It publishes contact ZIPs, which are the *contact's* address, not the job. Mapping those would silently mis-target your ZIP filter, so the field is left empty and Chicago rows are excluded when a ZIP filter is set.
- **Houston, Los Angeles, Norfolk and Montgomery County publish no contractor.** A blank contractor there means the city does not collect it, not that the homeowner is still shopping — and the score and the call sheet both say so rather than pretending otherwise. Scores are normalized so those cities still reach the same 0–100 band.
- **New Orleans runs about a week behind.** Raise the lookback window to 10+ days for it, or it returns nothing. The run log names any city that came back empty and tells you why, so you are never left guessing whether a quiet day was real.

A note on why this list is not longer: most "open permit data" is a frozen archive. Sweeping 19 candidate feeds to add the five newest cities here, **12 failed** — Dallas's endpoint returns HTTP 200 over data whose newest record is from **December 2019**, Kansas City stops in 2024, Providence in 2020, and Fort Worth is not a Socrata host at all despite listing a permits dataset. Norfolk's feed contains permits dated five months in the *future*, which will fool any tool that measures freshness with a plain `MAX(date)`.

None of those are listed here. Be skeptical of any permit tool advertising a large city count — ask which of those cities returned a row this week.

***

### Output

One row per permit:

```json
{
  "lead_score": 78,
  "trade": "roofing",
  "trades": ["roofing", "remodel"],
  "issued_date": "2026-07-27",
  "valuation": 28000,
  "valuation_is_estimated": false,
  "address": "1234 Beechnut St, Houston TX 77072",
  "zip": "77072",
  "work_description": "REPLACE ROOF STORM DAMAGE",
  "permit_type": "Building Permit",
  "permit_number": "26-0891234",
  "contractor_name": "",
  "contractor_published": false,
  "owner_name": "SMITH JOHN",
  "source_city": "houston_tx",
  "source_url": "https://www.houstonpermittingcenter.org/sold-permits-search",
  "scraped_at": "2026-07-27T13:04:11Z"
}
```

Export to CSV or Excel, or push straight into your CRM via the Apify API.

### Reading `lead_score`

| Score | Meaning |
|---|---|
| **75–100** | Call today. Fresh, real money, often no contractor attached yet. |
| **50–74** | Worth a call or a mailer this week. |
| **25–49** | Marketing list, not a phone list. |
| **0–24** | Old, tiny, or missing an address. |

**`contractor_name` empty with `contractor_published: true`** means the homeowner pulled the permit themselves and has not hired anyone. If you sell to homeowners, these are your best rows and the score reflects that. If you sell *to contractors*, flip on **Only permits with a named contractor** to invert it.

***

### Recommended setup

**Roofing contractor in southwest Houston:**

```
cities:            houston_tx
lookbackDays:      2
trades:            roofing
minValuation:      8000
zipWhitelist:      77072, 77036, 77081, 77074, 77099    <- required for Houston
requireContractor: false
onlyNew:           true
```

**Anywhere else — no ZIPs needed:**

```
cities:            chicago_il, nyc_ny, seattle_wa, los_angeles_ca
lookbackDays:      2
trades:            roofing, hvac
minValuation:      10000
onlyNew:           true
```

Then **Schedule** it daily at 6:00 AM. You get that morning's new permits before you leave the yard.

***

### Pricing

Pay per event. You are charged for **new leads actually delivered** — not for rows scraped, not for duplicates you already received, not for permits your filters excluded.

That distinction is the whole cost story. Tools that bill per raw row re-bill you for the same permits every single morning; a daily schedule with this actor bills you once per permit, ever.

**Worked example.** A Houston roofer running the setup above typically sees 8–25 qualifying new roofing permits a day — roughly 240–750 delivered leads over a month of daily runs, or **$12–$37/month**. Dedicated permit-lead services start around $300/month and run past $2,000. If your average roof job is $14,000, the arithmetic is not close.

Set a **max cost per run** in the Console and the actor stops cleanly when it reaches your cap — it will never quietly overrun your budget.

***

### FAQ

**Where does this data come from?**
Public municipal permit records. Building permits are public records by statute.

**Is this legal?**
Yes. The actor reads only public, logged-out pages and never creates an account, never logs in, and never bypasses any access control. It also rate-limits itself so it does not burden city servers.

**Why is Houston slower than the other cities?**
The open-data cities are direct feeds. Houston has no feed — the actor queries the city's reporting system once per ZIP per day, pacing itself between requests. That is also why Houston data is hard to get anywhere else.

**Houston returned fewer records than the city says exist.**
Houston's report format suppresses repeated values on grouped sub-permits at the same site, and those are not recoverable from the output. The run log prints the exact capture rate on every query — typically 75–100%. No other tool reports this, because no other tool covers Houston.

**Some valuations look like round numbers.**
Those are estimates, flagged `valuation_is_estimated: true`. Many cities leave valuation blank on trade permits, and some — Austin especially — accept `0` or `1` to satisfy a required field. Rather than drop those leads, show `$0`, or price a roof replacement at one dollar, the actor substitutes a trade median and says so. Your minimum-valuation filter never discards a row on an estimated value.

**A permit is classified under the wrong trade.**
Send the exact `work_description` and the correct trade. Classification is rule-based and deterministic, so a reported miss becomes a test case and a fix in the next release rather than a shrug.

**Can you add my city?**
Ask. Cities with a live open-data feed can be added quickly. Cities with only a search portal take longer but are usually worth the most — because nobody else bothers.

# Actor input Schema

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

Which metro permit sources to pull. All open-data cities are verified live and return results with no further setup. NOTE: Houston is the only city that REQUIRES ZIP codes — its report cannot be swept city-wide, so selecting Houston without filling in 'ZIP codes' below returns nothing.

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

How far back to search from today. 5 is the safe default: it covers weekend gaps, late data entry, and the cities that publish on a 3-4 day delay (Los Angeles, Seattle). Deduplication means a wider window costs you nothing on a daily schedule - you are never charged twice for the same permit. Raise to 10+ for New Orleans.

## `trades` (type: `array`):

Filter permits by classified trade. Leave empty to return every trade.

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

Skip permits below this declared value. Filters out $200 water-heater swaps. Set 0 to disable.

## `zipWhitelist` (type: `array`):

Only return permits in these ZIPs. REQUIRED FOR HOUSTON - Houston's report needs a search term and cannot be swept city-wide, so Houston returns nothing without ZIPs. Optional for all open-data cities. Note: Chicago publishes no job-site ZIP, so Chicago rows are excluded when this filter is set. Example: 77072, 77036, 77081

## `requireContractor` (type: `boolean`):

Turn ON if you sell to contractors (supply, subcontracting). Turn OFF if you sell to homeowners - owner-pulled permits are your best homeowner leads.

## `onlyNew` (type: `boolean`):

Deduplicates against this actor's persistent store. This is what makes scheduled daily runs useful instead of re-billing you for the same rows.

## `maxResults` (type: `integer`):

Hard cap on results, as a cost guard.

## `hotLeadThreshold` (type: `integer`):

Leads scoring at or above this are written to a plain-text call sheet (hot-leads.txt) in the run's storage, so the good ones are visible without opening the dataset. 75 is 'call today'. Set 0 to turn the file off.

## `proxyConfiguration` (type: `object`):

Residential proxies are recommended for the Houston Accela portal. Socrata cities do not need a proxy.

## `debugScreenshots` (type: `boolean`):

Saves a screenshot to the key-value store at each browser step. Use this when calibrating selectors after a portal redesign.

## Actor input object example

```json
{
  "cities": [
    "chicago_il",
    "nyc_ny",
    "seattle_wa",
    "los_angeles_ca"
  ],
  "lookbackDays": 5,
  "trades": [],
  "minValuation": 5000,
  "zipWhitelist": [],
  "requireContractor": false,
  "onlyNew": true,
  "maxResults": 500,
  "hotLeadThreshold": 75,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "debugScreenshots": false
}
```

# Actor output Schema

## `leads` (type: `string`):

Every new lead delivered by this run, one row per permit. Sort by lead\_score descending and work top-down.

## `callSheet` (type: `string`):

Plain-text list of only the leads scoring at or above your threshold, best first, with a one-line reason for each score. Open this and start dialing.

# 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": [
        "chicago_il",
        "nyc_ny",
        "seattle_wa",
        "los_angeles_ca"
    ],
    "zipWhitelist": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("oldbie/construction-permit-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": [
        "chicago_il",
        "nyc_ny",
        "seattle_wa",
        "los_angeles_ca",
    ],
    "zipWhitelist": [],
}

# Run the Actor and wait for it to finish
run = client.actor("oldbie/construction-permit-leads").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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": [
    "chicago_il",
    "nyc_ny",
    "seattle_wa",
    "los_angeles_ca"
  ],
  "zipWhitelist": []
}' |
apify call oldbie/construction-permit-leads --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=oldbie/construction-permit-leads",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/lWfMeihjEIKrXoLZo/builds/yrpVSuZzHb3NBWveQ/openapi.json
