# Building Permit Lead Scraper (`muhammadafzal/building-permit-lead-scraper`) Actor

Find recent public building permits with project value, address, work scope, contractor or applicant details, status, dates, and source links for local sales prospecting.

- **URL**: https://apify.com/muhammadafzal/building-permit-lead-scraper.md
- **Developed by:** [Muhammad Afzal](https://apify.com/muhammadafzal) (community)
- **Categories:** Lead generation, Real estate
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 building 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/actors/running/actors-in-store.md#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

## Building Permit Lead Scraper

Turn recent public municipal building permits into normalized, filterable leads for contractors, suppliers, property services, real-estate research, and local market analysis.

The Actor reads credential-free public Socrata open-data portals, applies a consistent lead schema, removes duplicates, and stores a truthful run summary separately from the result dataset. Ready-made sources cover Chicago, San Francisco, Austin, Seattle, and New York City. You can also provide up to ten other public Socrata building-permit dataset URLs.

### What building permit data it extracts

Each dataset item is one permit lead. Fields remain stable even when a source does not publish every value; unavailable fields are returned as `null`.

| Field | Meaning |
| --- | --- |
| `permitNumber`, `leadId` | Municipal identifier and stable deduplication key |
| `jurisdiction`, `permitType`, `permitStatus` | Issuing city, permit classification, and status |
| `issueDate`, `applicationDate`, `expirationDate` | Normalized dates in `YYYY-MM-DD` when parseable |
| `workDescription`, `propertyUse`, `housingUnits` | Project scope and use indicators |
| `projectValue`, `fees` | Published valuation and permit fees in USD |
| `address`, `city`, `state`, `postalCode` | Project location |
| `latitude`, `longitude` | Published WGS84 coordinates |
| `contactName`, `contactCompany`, `contactRole`, `contactPhone` | Public contractor, permittee, or applicant data when available |
| `ownerName` | Public owner name or organization when available |
| `sourceDatasetId`, `sourceDatasetName`, `sourceUrl`, `scrapedAt` | Attribution and extraction provenance |
| `rawData` | Optional original Socrata row for audits and custom fields |

The default dataset contains only permit records. Outcome and error diagnostics are written to the `SUMMARY` key-value-store record, so an empty or blocked run never fabricates a lead.

### Good uses—and important limits

Use this Actor to build timely prospect lists for roofing, solar, HVAC, electrical, plumbing, construction materials, inspection, landscaping, insurance, or commercial real-estate workflows. It also works for local development research, construction trend analysis, and territory planning.

Do not use it as proof of legal permit status, a complete national permit database, or a substitute for the issuing authority. Municipal portals publish different fields and update on different schedules. San Francisco does not expose contractor contacts in its main permit dataset, for example, while Austin exposes richer contractor fields. New York City stores issuance dates as text and its public view can contain older records, so use a wider `lookbackDays` window for historical NYC permittee research; the Actor still applies the requested date filter locally. For any source with very selective filters, the Actor scans at most 10,000 rows per dataset and reports that bound in `SUMMARY`.

### Input

| Input | Default | Notes |
| --- | --- | --- |
| `jurisdictions` | `['chicago-il']` | One or more supported city presets |
| `customDatasetUrls` | `[]` | Public HTTPS Socrata `/resource/`, `/api/views/`, or `/d/` URLs |
| `lookbackDays` | `30` | Rolling period ending on `endDate` or today |
| `startDate`, `endDate` | unset / today | Fixed `YYYY-MM-DD` issue-date range; `startDate` overrides the rolling period |
| `keywords` | `[]` | Match any term across work, type, address, company, contact, and property use |
| `permitTypes`, `statuses` | `[]` | Case-insensitive contains filters using each portal's vocabulary |
| `postalCodes` | `[]` | Exact project ZIP/postal-code filters |
| `minProjectValue` | `0` | Minimum reported valuation; values above zero exclude unknown valuations |
| `maxResults` | `100` | Global cap from 1 to 5,000 leads for paying users; free-plan runs are capped at 5 leads even when a higher value is requested |
| `includeContactDetails` | `true` | Set false to suppress contact and owner fields |
| `includeRawData` | `false` | Include the original, source-specific row |
| `maxRequestRetries` | `2` | Bounded retries for `429`, `5xx`, and network failures |

#### Example: recent Chicago roofing leads

```json
{
  "jurisdictions": ["chicago-il"],
  "lookbackDays": 60,
  "keywords": ["roof", "roofing"],
  "minProjectValue": 25000,
  "maxResults": 100
}
```

#### Example: multi-city commercial projects

```json
{
  "jurisdictions": ["austin-tx", "seattle-wa", "san-francisco-ca"],
  "lookbackDays": 90,
  "keywords": ["commercial", "retail", "office"],
  "minProjectValue": 100000,
  "maxResults": 250,
  "includeContactDetails": true
}
```

#### Example: custom public Socrata dataset

```json
{
  "jurisdictions": [],
  "customDatasetUrls": [
    "https://data.example.gov/resource/abcd-1234.json"
  ],
  "startDate": "2026-08-01",
  "endDate": "2026-08-31",
  "maxResults": 50
}
```

Custom datasets must expose a recognizable issue-date field. Common Socrata field names are normalized automatically, but unusual local schemas may produce fewer populated fields. Enable `includeRawData` to inspect those original fields.

### Output example

```json
{
  "leadId": "austin-tx:2026-012345 BP",
  "permitNumber": "2026-012345 BP",
  "jurisdiction": "Austin, TX",
  "permitType": "Building Permit",
  "permitStatus": "Active",
  "issueDate": "2026-09-03",
  "workDescription": "Interior remodel of commercial tenant space",
  "projectValue": 250000,
  "address": "500 CONGRESS AVE",
  "city": "Austin",
  "state": "TX",
  "postalCode": "78701",
  "contactName": "Alex Builder",
  "contactCompany": "Example Construction LLC",
  "contactRole": "General Contractor",
  "contactPhone": "512-555-0100",
  "sourceDatasetId": "3syk-w9eu",
  "sourceDatasetName": "Issued Construction Permits",
  "sourceUrl": "https://data.austintexas.gov/d/3syk-w9eu",
  "scrapedAt": "2026-09-12T08:00:00.000Z",
  "rawData": null
}
```

### API and AI-agent use

Call the Actor through the Apify API and read its default dataset:

```bash
curl -X POST "https://api.apify.com/v2/acts/YOUR_USERNAME~building-permit-lead-scraper/runs?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jurisdictions":["austin-tx"],"lookbackDays":30,"maxResults":25}'
```

For an Apify MCP connection, choose this Actor when the user asks for public building permits, construction leads, contractor prospecting, or municipal development data in the supported cities. The stable dataset schema helps an agent map results into a CRM, spreadsheet, enrichment step, or follow-up workflow. It does not search planning applications outside the configured permit datasets.

### Pay-per-event pricing

| Event | Price | Charged when |
| --- | ---: | --- |
| Actor start | $0.0002 | A run starts |
| Building permit lead | $0.002 | One validated item is written to the default dataset |

A 25-lead run costs up to `$0.0502`; a 100-lead run costs up to `$0.2002`. Empty runs incur only the start event. The Actor checks the run's maximum charge before writing each result and stops cleanly when the remaining limit cannot cover another lead. Platform usage is intended to be included in these event prices rather than passed through separately.

Free-plan protection: Apify identifies the account that started the run through `APIFY_USER_IS_PAYING`. When that value indicates a non-paying user, the Actor enforces a hard five-lead output limit, even if `maxResults` is higher. The `SUMMARY` record reports `requestedMaxResults`, `effectiveMaxResults`, and `freePlanLimited` so downstream workflows can distinguish an intentional plan limit from an empty or failed search. Paying users retain the requested `maxResults` (up to 5,000).

### Reliability and diagnostics

The Actor calls each municipality's public Socrata metadata and resource endpoints directly with bounded timeouts, stable request headers, deterministic pagination, and error-specific retries. Calendar date fields are filtered server-side. Text dates are normalized and filtered locally. A failed city does not discard valid leads from other selected cities: successful records are preserved, the run becomes `partial`, and the source-specific warning appears in `SUMMARY`.

Outcomes are explicit:

- `data`: leads were delivered without warnings.
- `partial`: leads were delivered, but at least one source or scan bound produced a warning.
- `empty`: sources worked but no permit matched.
- `rejected`: input was invalid before billable result work.
- `blocked`: every requested source failed; no result event was charged.

The Actor does not solve CAPTCHAs, bypass authentication, or use private municipal systems. Public portal outages and schema changes are reported rather than hidden behind synthetic results.

### Responsible use

Permit records are public government data, but public availability does not remove privacy, marketing, licensing, or consumer-protection obligations. Check each source's license and terms, honor do-not-call and anti-spam rules, minimize personal data, and use contacts only for lawful, relevant outreach. Set `includeContactDetails` to `false` when names and phone numbers are unnecessary. Always verify a permit on the linked municipal source before making a high-impact decision.

For support, include the run ID, source jurisdiction, redacted input, and the `SUMMARY` record. Never post tokens or personal-data exports in a public issue.

# Actor input Schema

## `jurisdictions` (type: `array`):

Use this when you want ready-made permit sources. Select one or more of Chicago, San Francisco, Austin, Seattle, or New York City. Defaults to Chicago; custom sources can be added separately.

## `customDatasetUrls` (type: `array`):

Use this when the city is not listed above. Enter public HTTPS Socrata dataset, API, or resource URLs such as https://data.example.gov/resource/abcd-1234.json. This is not for private APIs or ordinary web pages.

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

Use this for a rolling lead window ending today. Defaults to 30 days; allowed range is 1–3650. Ignored when Start date is provided.

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

Use this for a fixed issue-date range in YYYY-MM-DD format, for example 2026-08-01. Overrides Recent permit window. This is not a free-form date.

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

Use this to end the issue-date range in YYYY-MM-DD format, for example 2026-08-31. Defaults to today and cannot be before Start date.

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

Use this to keep permits whose description, type, address, or company contains any term, for example solar, roof, or remodel. Matching is case-insensitive; empty means all projects.

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

Use this to keep permit types containing any supplied phrase, for example electrical or new construction. Values differ by city; empty means all types.

## `statuses` (type: `array`):

Use this to keep statuses containing any supplied phrase, for example issued or complete. Values differ by city; empty means all statuses.

## `postalCodes` (type: `array`):

Use this to target exact property ZIP/postal codes, for example 60614. Empty means every location in the selected sources.

## `minProjectValue` (type: `integer`):

Use this to keep permits with a reported or estimated project value at or above this USD amount, for example 100000. Zero includes permits with unknown value.

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

Use this to cap normalized leads across all sources. Defaults to 100; allowed range is 1–5000. Free-plan runs are capped at 5 leads even when a higher value is requested. The Actor stops before exceeding the run charge limit.

## `includeContactDetails` (type: `boolean`):

Use this when public contractor, permittee, applicant, or owner names and phone numbers are needed for lawful outreach. Defaults to true; turn off for location-only research.

## `includeRawData` (type: `boolean`):

Use this for auditing or fields not covered by the normalized schema. Defaults to false because raw municipal rows are large and vary between portals.

## `maxRequestRetries` (type: `integer`):

Use this to control bounded retries for rate limits and temporary portal failures. Defaults to 2; allowed range is 0–5. This does not bypass access controls.

## Actor input object example

```json
{
  "jurisdictions": [
    "chicago-il"
  ],
  "customDatasetUrls": [],
  "lookbackDays": 30,
  "keywords": [],
  "permitTypes": [],
  "statuses": [],
  "postalCodes": [],
  "minProjectValue": 0,
  "maxResults": 100,
  "includeContactDetails": true,
  "includeRawData": false,
  "maxRequestRetries": 2
}
```

# Actor output Schema

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

Schema-valid public permit records normalized for prospecting and market research.

## `summary` (type: `string`):

Outcome, source, filter, warning, and billing diagnostics for this run.

# 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 = {
    "jurisdictions": [
        "chicago-il"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("muhammadafzal/building-permit-lead-scraper").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 = { "jurisdictions": ["chicago-il"] }

# Run the Actor and wait for it to finish
run = client.actor("muhammadafzal/building-permit-lead-scraper").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 '{
  "jurisdictions": [
    "chicago-il"
  ]
}' |
apify call muhammadafzal/building-permit-lead-scraper --silent --output-dataset

```

## MCP server setup

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

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/xMhntSgDnJplFEGDK/builds/871QRwHJ4kUXQLCVR/openapi.json
