# Building Permit Monitor - Construction Leads (`lowlanddata/building-permit-monitor`) Actor

Get alerted the moment a matching building permit is issued in Chicago, Austin, San Francisco, Seattle or NYC. Filter by keywords, permit type and valuation; alerts only on never-seen permits via official city open-data APIs. Project and contractor business data, no homeowner identities.

- **URL**: https://apify.com/lowlanddata/building-permit-monitor.md
- **Developed by:** [Lowland Data](https://apify.com/lowlanddata) (community)
- **Categories:** Lead generation, Automation, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.60 / 1,000 baseline permits

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Building Permit Monitor — Construction Leads

Watches US city building-permit portals — Chicago, Austin, San Francisco,
Seattle and New York City — for newly issued permits matching your profile: keywords,
permit types, minimum project valuation. It alerts the moment a matching permit
is issued, and it remembers what it has already told you — so an alert always
means a genuinely new project, never a re-scrape of last week's list.

It reads each city's official open-data API (the access route the cities publish
for exactly this), and the output is clean by construction: project, address,
valuation and contractor **business** — never homeowner or applicant identities.
Safe to store, share with your sales team, or import into a CRM.

### Quick start (30 seconds, no account keys needed)

1. Run the actor with the prefilled input (Seattle, last 14 days) — it finishes
   in a few seconds and shows a baseline of recent permits.
2. Replace the demo profile with your own: your cities, your trade's keywords or
   permit types, your minimum project size.
3. Add a **daily schedule** (Actor page → Schedule). From then on, runs are
   quiet until a permit you care about is actually issued.

### Who uses this

- **A roofing contractor in Chicago** monitors `keywords: ["roof", "reroof"]`
  daily. When a permit is issued two neighborhoods over, the alert lands with
  the `address`, `valuationUsd` and `issuedDate` — a lead the same week the
  project goes live, not after the crews are already on site.
- **A solar installer in Austin** watches `keywords: ["solar", "photovoltaic"]`
  — every new solar permit shows which `contractorName` won the job, mapping
  both leads and competitors in one feed.
- **A building-materials supplier** runs `permitTypes: ["new construction"]`
  with `minValuationUsd: 500000` across all five cities — only projects big
  enough to supply, straight into the CRM via the API.
- **A market analyst** tracks permit volumes and valuations per city as a
  construction-economy indicator — the baseline plus daily deltas is a clean
  time series.
- **An HVAC company** pairs `permitTypes: ["mechanical"]` with
  `alertOn: "new-and-updated"` to also see status changes on projects it quoted.

### What you get

One JSON item per **event**: a baseline snapshot per permit on the first run,
then only transitions — newly issued matching permits, and (opt-in) status
changes on permits you have already seen. A new-permit alert looks like:

```json
{
  "portal": "austin",
  "permitNumber": "2026-091811 EP",
  "permitType": "Electrical Permit",
  "permitClass": "Residential",
  "workClass": "New",
  "description": "Install rooftop solar array, 24 panels, 9.6 kW",
  "status": "Active",
  "address": "8316 ROBERT ZION WAY",
  "city": "Austin",
  "state": "TX",
  "zip": "78744",
  "valuationUsd": 48500,
  "appliedDate": "2026-07-16",
  "issuedDate": "2026-07-17",
  "expiresDate": "2027-01-13",
  "contractorName": "Powerhouse Electrical Services, Inc.",
  "contractorTrade": "Electrical Contractor",
  "url": "https://abc.austintexas.gov/web/permit/public-search-other",
  "isBaseline": false,
  "isUpdate": false
}
```

Field honesty notes:

- Every portal has its own vocabulary — `permitType`, `permitClass` and
  `workClass` carry the city's own labels, unnormalized, and every item's labels
  are reusable as your `permitTypes` filter.
- `valuationUsd` appears only when the city publishes a project cost — many
  permits carry none, and absence does not mean small.
- `contractorName` is the business on the permit where the city publishes one
  (Chicago and Austin do; San Francisco and Seattle mostly don't).
- `address` is the project site the city publishes for public accountability.
  Who lives there is deliberately not in the data — see the compliance section.

### How much does permit monitoring cost?

Pay per outcome — checking costs nothing; you pay when the monitor tells you
something new:

- **New matching permit (the alert): $0.02** — also for status-change alerts.
- Baseline item (first run only): $0.002.
- Actor start: $0.005. Everything included - there are no separate
  platform-usage charges, and higher Apify plans get automatic unit-price
  discounts.

Cost feel: a focused trade profile in two cities sees a handful of new permits
per working day → roughly **$2–8/month** for complete coverage, with the
one-time baseline (say 100 permits) costing about $0.20. A run with zero new
matches costs $0.005 all-in. Lead-gen services sell the same permit leads for
$50–300/month.

#### Why an alert costs more than a scraped row

The store's headline ("from $2.00 / 1,000 baseline results") is what a trial
costs: a 100-permit baseline ≈ $0.20. Apify displays every event per 1,000, so
in the pricing table the alert event reads "$20.00 / 1,000" next to raw permit
scrapers at $2–30 per 1,000 rows — but that comparison is between different
things:

- A **raw permit scraper** sells rows: to monitor with rows you re-download
  each city's recent permits every day and diff them yourself.
- **This monitor** sells the transition: it remembers state in your account and
  charges $0.02 exactly when a permit you have not seen matches your profile.
  Nobody buys 1,000 alerts a month.

### Not technical? Let your AI assistant set it up

Paste this prompt together with a line about your trade and cities:

```text
Set up the Apify actor lowlanddata/building-permit-monitor to watch US building
permits for me. Build the input JSON from my business description:
- portals: which of ["chicago","austin","san-francisco","seattle","nyc"] I can serve
- keywords: 3-8 words my kind of project's description would contain
  (e.g. ["roof","reroof"] for roofing, ["solar"] for solar installers)
- permitTypes: my trade's permit labels if I named any (e.g. ["electrical"])
- minValuationUsd: only if I gave a minimum project size
Leave alertOn, emitBaseline and maxItems at their defaults. Run it once and
show me the baseline as a table (description, address, city, valuation,
contractor, issued date). Then create a daily schedule for it and set up a
notification so I get an email when a run produces items.
```

***

### Coverage — measured, not promised

- **Sources:** each city's official open-data permit dataset (Socrata APIs) —
  Chicago, Austin, San Francisco, Seattle and New York City (DOB NOW). Keyless,
  sanctioned access; together several thousand permits/week unfiltered (measured
  July 2026, NYC alone ~3,000/week).
- **More cities are on the roadmap**; each city ships only after its dataset
  passes the same field-level privacy verification.
- Cities update their datasets on their own cadence — typically daily with a
  small lag after issuance. The monitor alerts when the *dataset* publishes the
  permit; that is as fast as official data allows.
- **Permit-type vocabularies differ per city** — `permitTypes` is an honest
  substring match over each city's own labels, not a fake universal taxonomy.
- A portal outage or dataset migration never fails the run: the affected city
  is reported in the run status (uncharged) and its permits alert once it
  recovers.

### How the monitor thinks

- **First run = baseline.** It fetches your profile's matches from the last
  `lookbackDays` (default 14), pushes them flagged `isBaseline: true` at the
  low baseline price, and memorizes them. `emitBaseline: false` seeds silently.
- **After that, only transitions.** Each run queries from the previous run (one
  day of overlap; the memory absorbs duplicates) and pushes only permits it has
  never alerted. Status changes alert as `isUpdate: true` when
  `alertOn: "new-and-updated"`; with the default they stay silent but are
  remembered.
- **Editing the profile starts a fresh baseline** — matching-relevant inputs
  are the profile's identity, so a changed profile can never flood you with
  alerts computed against another profile's memory. `maxItems`, `emitBaseline`
  and schedule changes do not restart anything.
- **Memory** lives in a named key-value store (`stateStoreName`, default
  `permit-monitor-state`) in your account. Run several profiles on separate
  schedules against the same store — they never interfere. `resetState: true`
  wipes the current profile's memory.
- A capped baseline samples every selected city round-robin instead of
  exhausting the first one.
- Re-running after a crash or overlap re-alerts at most the last few unsaved
  items — state is checkpointed during the run.

### Input reference

| Field             | Type    | Default                  | Notes                                                   |
| ----------------- | ------- | ------------------------ | ------------------------------------------------------- |
| `portals`         | array   | all five                 | `chicago`, `austin`, `san-francisco`, `seattle`, `nyc`. |
| `keywords`        | array   | `[]`                     | OR, case-insensitive, over description + type labels.   |
| `permitTypes`     | array   | `[]`                     | Substring match over the city's type/class labels only. |
| `minValuationUsd` | integer | —                        | Permits without a published valuation always pass.      |
| `lookbackDays`    | integer | `14`                     | Baseline reach on the first run (max 60).               |
| `alertOn`         | string  | `"new"`                  | `"new-and-updated"` adds status-change alerts.          |
| `emitBaseline`    | boolean | `true`                   | `false` = silent seeding.                               |
| `resetState`      | boolean | `false`                  | One-shot: wipe this profile's memory.                   |
| `maxItems`        | integer | `500`                    | Overflow alerts on the next run, never lost.            |
| `stateStoreName`  | string  | `"permit-monitor-state"` | Only change to fully isolate monitor instances.         |

### Scheduling and API use

Run it daily and pair the schedule with a Monitoring alert on "dataset items

> 0" to get an email exactly when a permit matched.

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/lowlanddata~building-permit-monitor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"portals": ["austin"], "keywords": ["solar"]}'
```

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('lowlanddata/building-permit-monitor').call({
  portals: ['chicago', 'austin'],
  keywords: ['roof', 'reroof'],
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items); // only new permits since the last run
```

```python
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("lowlanddata/building-permit-monitor").call(run_input={
    "portals": ["seattle"],
    "permitTypes": ["mechanical"],
})
items = client.dataset(run["defaultDatasetId"]).list_items().items
```

Daily runs finish in well under a minute, comfortably inside the 300s sync-call
wall; a 60-day four-city baseline is the one case that can take a couple of
minutes — run it async once.

### Use it with AI agents (MCP)

Claude, Cursor and other MCP-capable agents can run this monitor as a tool
through Apify's hosted MCP server: the agent builds the profile from a
plain-language brief, runs it, and reads the leads back — no glue code.

Claude Code:

```bash
claude mcp add apify --transport http "https://mcp.apify.com?actors=lowlanddata/building-permit-monitor"
```

Cursor or Claude Desktop (add a custom connector / MCP server with this URL):

```text
https://mcp.apify.com?actors=lowlanddata/building-permit-monitor
```

Sign in with your Apify account when prompted — runs are billed to it. Setup
details per client: [Apify MCP docs](https://docs.apify.com/platform/integrations/mcp),
or start from this actor's own MCP page:
[apify.com/lowlanddata/building-permit-monitor/api/mcp](https://apify.com/lowlanddata/building-permit-monitor/api/mcp).

Prompts that work once connected:

- "Watch Chicago and Austin for new roofing permits and show me today's leads."
- "What solar permits over $30k were issued in Austin this week, and which
  contractors got them?"
- "Track new-construction permits over $1M across all five cities and alert me
  daily."

### Is it legal to monitor building permits?

Yes — building permits are public records, and these five cities publish them
through official open-data APIs exactly so they can be reused. No login walls,
no anti-bot bypass — if a portal is unreachable, the run reports it rather than
fighting it. And the output is safe by construction: homeowner and applicant
identities are never extracted (see below).

How you contact a lead is on you — this tool gives you the public project
record, not marketing-law advice for your state.

### Compliance: safe to store, safe to CRM-import

Permit records as published by the cities can contain personal data — owner and
applicant names, phone numbers, even the applicant's own address. This actor's
guarantee is structural: each city's mapper whitelists fields IN (project,
address, valuation, dates, contractor business name/trade), so person-fields
never map at all, and an automated test proves it against each city's real
column names. The project `address` stays — it is the publicly published
project site and the lead's core value; who lives there is deliberately
unreachable. One honest limit: `description` is text the applicant wrote — on
rare occasions someone writes a name into free text. We cannot rewrite public
records; everything structured is guaranteed clean.

### FAQ

**Why did my second run return nothing?** Because nothing new matched — that is
the product working. The first run is the baseline; afterwards you only pay for
permits you have not seen. The run status says how many permits were checked.

**Does the output contain homeowner names or phone numbers?** No — by design.
The city mappers never touch owner/applicant fields. You get the project, the
address, the valuation and the contractor business.

**Which cities are covered?** Chicago, Austin, San Francisco, Seattle and New
York City — cities with official open-data permit APIs. More follow (New York
next); tell us which city you need via the Issues tab.

**Can I monitor several trades or cities separately?** Yes — one schedule per
profile, same actor, same state store. Each profile keeps its own memory.

**How fresh are the alerts?** As fresh as the city's dataset — typically the
permit appears within a day or two of issuance. The monitor can't be faster
than the official data, and doesn't pretend to be.

**What does a status-change alert mean?** With `alertOn: "new-and-updated"`,
a seen permit whose status moves (e.g. Active → Finaled) alerts once as
`isUpdate: true` — useful for tracking projects you quoted.

### Troubleshooting

- **"Every portal failed"** — the open-data platforms are down or unreachable;
  re-run later. Single-portal failures never fail the run.
- **"<city> truncated by the request budget"** — the lookback window matched
  more than ~10,000 permits; lower `lookbackDays` or narrow the profile.
- **"Monitor state record … is not readable"** — the state was written by a
  newer version; run once with `resetState: true` to start a fresh baseline.

Something else off? Open an issue on the actor's Issues tab — reports are read
and answered.

# Actor input Schema

## `portals` (type: `array`):

Which city permit portals to monitor. All five are official open-data APIs - no keys needed.

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

Alert on permits whose description or type contains ANY of these words (case-insensitive). Example: \["solar", "roof", "HVAC"]. Leave empty to match everything and filter by permit type or valuation instead.

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

Only alert on permits whose type or class label contains any of these (case-insensitive substring - portal vocabularies differ). Example: \["electrical", "demolition", "new construction"]. Every dataset item carries its portal's own labels, reusable here.

## `minValuationUsd` (type: `integer`):

Only alert on permits with a reported project cost of at least this many dollars. Permits without a published valuation always pass - portals make the field optional, and dropping them would hide real leads.

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

How many days of already-issued permits the FIRST run of a profile loads as its baseline. Later runs automatically query from the previous run with one day of overlap - re-alerting a seen permit is impossible.

## `alertOn` (type: `string`):

"new" alerts once per permit. "new-and-updated" also alerts when a seen permit's status changes (marked isUpdate: true), e.g. issued to finaled.

## `emitBaseline` (type: `boolean`):

On the very first run of a profile, push the currently matching permits (flagged isBaseline: true, charged at the lower baseline price) so you start with the recent picture. Turn off to seed silently and only ever receive new alerts.

## `resetState` (type: `boolean`):

Discard this profile's memory of already-seen permits and start over with a fresh baseline run. One-shot switch: turn it back off afterwards.

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

Upper bound on items pushed per run (baseline or alerts). Matches beyond the cap are not lost: they alert on the next run.

## `stateStoreName` (type: `string`):

Named key-value store holding the monitor's seen-permit memory. Only change this to run fully isolated monitor instances.

## Actor input object example

```json
{
  "portals": [
    "seattle"
  ],
  "lookbackDays": 14,
  "alertOn": "new",
  "emitBaseline": true,
  "resetState": false,
  "maxItems": 10,
  "stateStoreName": "permit-monitor-state"
}
```

# Actor output Schema

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

Building permits matching your profile, one JSON object per permit. On the first run: the recent baseline; afterwards: only new (or status-changed) matches.

# 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 = {
    "portals": [
        "seattle"
    ],
    "lookbackDays": 14,
    "maxItems": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("lowlanddata/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 = {
    "portals": ["seattle"],
    "lookbackDays": 14,
    "maxItems": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("lowlanddata/building-permit-monitor").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 '{
  "portals": [
    "seattle"
  ],
  "lookbackDays": 14,
  "maxItems": 10
}' |
apify call lowlanddata/building-permit-monitor --silent --output-dataset

```

## MCP server setup

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

```

## OpenAPI specification

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