# Contractor Lead Finder — Live Building Permits (`michaeljames/contractor-lead-finder`) Actor

Find contractors pulling building permits right now. Ranked leads from official city open data: businesses, not raw permit rows. Phones in Austin.

- **URL**: https://apify.com/michaeljames/contractor-lead-finder.md
- **Developed by:** [Michael H](https://apify.com/michaeljames) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

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

## Contractor Lead Finder — Live Building Permits

**Find the contractors who are actively working right now, ranked by how good a prospect they are.**

Every day, US cities publish the building permits they issue — and most of those permits name the contractor doing the work. This Actor reads those official city feeds, works out which trade each job belongs to, and rolls thousands of individual permits up into a ranked list of **businesses you can actually call**.

You get contractors, not paperwork.

***

### Who this is for

- **Suppliers and distributors** selling to trade businesses — find the roofers, HVAC shops and electricians who just pulled permits in your territory.
- **Marketing, web and software vendors** selling services to contractors — a permit is proof a business is real, active, and spending money this month.
- **Subcontractors** looking for general contractors with live projects.
- **AI agents** doing sourcing or prospecting research — the output is clean, typed JSON with a documented schema.

***

### What makes this different

Most permit tools hand you permit rows. A permit row is an *event*. If you are selling something, you don't want 4,000 events — you want the 180 *businesses* behind them, sorted by which ones are worth a phone call.

That is the default output here. Each contractor comes back as one record:

```json
{
  "contractorName": "A-Plus Energy Management",
  "leadScore": 82,
  "leadScoreReasons": [
    "6 permits in window (+31)",
    "direct phone on file (+20)",
    "$184,000 total job value (+10)",
    "most recent permit 2 day(s) ago (+15)",
    "identifiable trade: HVAC / Mechanical (+5)"
  ],
  "phoneFormatted": "(512) 450-1980",
  "primaryTradeLabel": "HVAC / Mechanical",
  "trades": [
    { "trade": "hvac", "label": "HVAC / Mechanical", "permits": 5 },
    { "trade": "electrical", "label": "Electrical", "permits": 1 }
  ],
  "permitCount": 6,
  "totalJobValue": 184000,
  "avgJobValue": 30667,
  "firstPermitDate": "2026-07-29",
  "lastPermitDate": "2026-08-14",
  "addressCity": "Austin",
  "marketsActive": ["Austin"],
  "samplePermitNumbers": ["2026-103853 MP", "2026-101120 MP"],
  "sampleWork": ["Replacement of complete existing central heat and air system..."]
}
```

**The score is explained, not hidden.** `leadScoreReasons` shows exactly how every point was earned, so you can argue with the ranking instead of trusting it blindly.

#### How the ranking works

| Signal | Max points | Why |
|---|---|---|
| Permit volume in window | 40 | The strongest signal a trade business is real and spending. A shop pulling eight permits a month has payroll and a budget. |
| Reachability | 20 | A phone number on file. A lead you cannot call is worth much less. |
| Total job value | 20 | How much money is moving through the business. |
| Recency | 15 | A permit from last week beats one from three months ago. |
| Identifiable trade | 5 | Makes it possible to write a relevant pitch. |

***

### Cities covered

| City | Contractor name | Phone | Job value | Notes |
|---|---|---|---|---|
| **Austin, TX** | ✅ | ✅ **yes** | partial | The best source in the country for this — the only verified city publishing contractor phone numbers |
| **New York City, NY** | ✅ | — | ✅ | Applicant business name + business address; very high volume |
| **Louisville, KY** | ✅ | — | ✅ | Coarser trade matching (no free-text work description) |
| **Sacramento, CA** | ✅ | — | ✅ | Current year only |
| **Phoenix, AZ** | ✅ | — | — | |
| **Seattle, WA** | ⚠️ ~9% | — | ✅ | Contractor named on only about 1 permit in 11 |
| **Denver, CO** | ✅ | — | ✅ | Publishes with a multi-month lag; CC BY 3.0 credit attached to every record |

**Cities deliberately not included, and why.** Philadelphia and Nashville both publish excellent permit feeds, but their open-data terms bar commercial republication without written permission, so they are switched off in code. Chicago's terms are bespoke and have not yet been read. This Actor only serves data it is allowed to serve — if you need one of those cities, ask, and it will be added the day the permission exists.

Adding a city is a config change, not a code change — see `src/cities.js`.

***

### Where the data comes from, and why you can use it

**Official municipal open-data APIs only. No scraping. No terms-of-service grey area.**

Every source is a documented, public API published by the city itself — Socrata/SODA, ArcGIS REST, or Carto SQL — and every source's actual license or terms were read before it was enabled. The read date and the source URL are recorded in the code and reported in every run summary. Three kinds of permission are accepted:

- **Public-domain dedications** — `USGOV_WORKS` (Austin), `PUBLIC_DOMAIN` (Seattle), `PDDL` (Louisville).
- **Attribution licenses** — `CC BY 3.0` (Denver). The required credit line is attached to every record.
- **Open-data statutes and city terms that permit redistribution** — NYC Administrative Code § 23-502(d) ("without any registration requirement, license requirement or restrictions on their use"); Sacramento and Phoenix open-data terms (explicit grant to use, reproduce and redistribute; no commercial restriction).

Sources whose terms restrict commercial use, or whose terms have not been read, are **shipped disabled** and cannot be selected, even by editing the input. The license gate is enforced in code and covered by tests, not documented as an intention.

Every record carries an `_attribution` array with one entry per source: the source name, link, license, the credit line the city asks for, and the retrieval time. If you republish results, carry that credit forward — for Denver, NYC and Louisville it is a condition of the license, not a courtesy.

**Accuracy disclaimer:** cities do not warrant the completeness or accuracy of their permit data, and neither does this Actor. It republishes what the city published. Verify anything you intend to rely on.

***

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `cities` | array | `["austin"]` | Which city feeds to query |
| `outputMode` | string | `"contractors"` | `contractors` = ranked lead list · `permits` = raw permit rows |
| `trades` | array | `[]` (all) | 16 trades: roofing, solar, hvac, plumbing, electrical, pool, remodel, addition, newConstruction, demolition, windowsDoors, fenceDeck, foundation, siding, signage, fireSprinkler |
| `daysBack` | integer | `14` | 7–30 is the useful range for outreach |
| `minJobValue` | integer | `0` | See caveat below |
| `requireContractor` | boolean | `true` | Permits with no named contractor cannot become leads |
| `requirePhone` | boolean | `false` | Effectively limits results to Austin |
| `zipCodes` | array | `[]` | Restrict to specific job-site ZIPs |
| `maxResults` | integer | `50` | Hard cap on results **and therefore on cost**. 50 contractor leads cost at most $2 |
| `socrataAppToken` | string | — | Optional free token; raises rate limits |

> **Caveat on `minJobValue`:** trade permits (electrical, plumbing, mechanical) frequently carry no declared valuation, and Phoenix publishes none at all. Setting a minimum above 0 filters those out entirely. Leave it at 0 unless you specifically want large projects.

#### Example — roofers and solar installers in Austin, last 3 weeks, phone required

```json
{
  "cities": ["austin"],
  "trades": ["roofing", "solar"],
  "daysBack": 21,
  "requirePhone": true,
  "outputMode": "contractors",
  "maxResults": 150
}
```

#### Example — every trade across four cities, raw permit rows

```json
{
  "cities": ["austin", "nyc", "louisville", "phoenix"],
  "daysBack": 7,
  "outputMode": "permits",
  "maxResults": 1000
}
```

***

### Pricing

You are billed **per result delivered** — never per permit scanned. A run that reads 5,000 permits and finds 60 contractors bills 60 results.

`maxResults` is a hard ceiling on both output and cost. The default is 50, so a first run with default settings costs at most $2 for results. Raise it when you want the full list.

Zero results costs nothing for results, and neither does a run that fails — the only charge in those cases is Apify's automatic Actor-start fee, a fraction of a cent.

***

### Fresh leads every week, on autopilot

New permits are issued every day, so last month's list goes stale. Save your input as a **Task**, then add an Apify **Schedule** (for example, every Monday at 7 am) and set `daysBack` to `7`. Each Monday you get only the contractors who pulled permits in the past week, ranked and ready to call. Apify can email you when the run finishes.

### Use it from your own tools

Everything on this page is also an API. Start a run and fetch the results from your CRM, a spreadsheet, **n8n**, **Make** or **Zapier** with the Apify integrations, or from code through the Apify API. AI agents can call it as a tool through Apify's MCP server. The output is typed JSON with a documented schema, so it drops straight into a pipeline.

***

### Output notes for developers

- Every record carries `_attribution` (one entry per source, with license and credit line) and `_retrievedAt`.
- A `RUN_SUMMARY` record is written to the key-value store with per-city row counts, timings, license tier and review date, any per-city errors, and the exact date window used.
- Missing values are `null`, never `""`, `"N/A"`, or a placeholder. City junk values (`" "`, `"N/A"`, `"78727-____"`) are normalized away.
- Phones are E.164 (`+15124501980`) plus a display form (`(512) 450-1980`).
- Dates are ISO `YYYY-MM-DD`. Three different upstream date formats — ISO strings, epoch milliseconds, and `MM/DD/YYYY` text — are normalized for you.
- Money is a number, never a string, despite every upstream API returning strings.
- If one city's API fails, the run continues with the others and records the failure in `RUN_SUMMARY.sources[].error`. If no city can be served, the run fails cleanly with an explanation and charges nothing.

***

### Limits and honest expectations

- **Contractor phone numbers exist in Austin only.** Every other city publishes a name, and sometimes a mailing address. If your workflow depends on phones, Austin is your city.
- **Seattle names a contractor on roughly 9% of permits.** That is the city's data, not a bug here.
- **Denver runs months behind.** Good for market research, bad for fresh outreach.
- **Trade classification is keyword-based** on the text the city publishes. Where a city publishes no work description (Louisville, Denver), classification is coarser. Each record reports its own `tradeConfidence` so you can filter on it.
- **A permit is evidence of activity, not an endorsement.** It tells you a business is working and spending. It tells you nothing about whether they want what you are selling.

***

### Questions

Open an issue on this page, or ask for a city you need. Replies within 2 business days.

# Actor input Schema

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

Which city permit feeds to search. Every source is an official municipal open-data API whose published terms permit redistribution (public domain, U.S. Government Works, CC BY 3.0, or city open-data terms read on 2026-09-08). Attribution is attached to every result.

## `outputMode` (type: `string`):

Contractor profiles roll many permits up into one ranked business record — this is the lead list. Permit rows gives you the raw individual permits instead.

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

Only return work in these trades. Leave empty for all trades. Trade keywords are pushed into the city API query, so filtering here also makes the run faster.

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

How far back to search from today. Fresh permits make better leads; 7-30 days is the useful range for outreach.

## `minJobValue` (type: `integer`):

Skip permits below this declared job value. Set 0 for no minimum. Note: Phoenix does not publish job values, so this filter removes all of its rows if set above 0.

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

Strongly recommended. Permits without a named contractor cannot become a lead.

## `requirePhone` (type: `boolean`):

Austin is currently the only source that publishes contractor phone numbers, so turning this on effectively limits results to Austin.

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

Optional. Restrict to these 5-digit job-site ZIP codes.

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

Hard cap on results returned — and therefore on what you are charged. You are billed per result delivered, never per permit scanned. The default of 50 keeps a first run at $2 or less; raise it once you like the results.

## `socrataAppToken` (type: `string`):

Optional free token from any Socrata portal. Raises rate limits on Austin, NYC and Seattle. Leave blank to use the shared anonymous pool.

## Actor input object example

```json
{
  "cities": [
    "austin"
  ],
  "outputMode": "contractors",
  "trades": [],
  "daysBack": 14,
  "minJobValue": 0,
  "requireContractor": true,
  "requirePhone": false,
  "zipCodes": [],
  "maxResults": 50
}
```

# Actor output Schema

## `contractors` (type: `string`):

Ranked contractor businesses with score, reasons, trades and contact details (the default output).

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

One row per building permit, when the input asks for permit rows.

## `runSummary` (type: `string`):

Per-city row counts, license tier and review date per source, and the date window used.

# 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"
    ],
    "maxResults": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("michaeljames/contractor-lead-finder").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"],
    "maxResults": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("michaeljames/contractor-lead-finder").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"
  ],
  "maxResults": 50
}' |
apify call michaeljames/contractor-lead-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,michaeljames/contractor-lead-finder"
        }
    }
}
```

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/SljJEeYygYnomjLDF/builds/PwCBIMsnY1cfcIPXk/openapi.json
