# US Building Permits — Accela Citizen Access (Any City) (`egra_van/accela-building-permits`) Actor

Get new building permits from any US city or county on Accela Citizen Access: roofing, solar, pool, HVAC, new construction and remodel permits with address, dates, status, valuation and contractor. Daily monitor mode with Telegram, Slack, webhook (n8n/Make) and email alerts.

- **URL**: https://apify.com/egra\_van/accela-building-permits.md
- **Developed by:** [Argentin Vazdautan](https://apify.com/egra_van) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $8.50 / 1,000 building permit record (us)s

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

## US Building Permits — Accela Citizen Access (Any City)

Get **new building permits as daily leads** from any US city or county that runs **Accela Citizen Access** (the "ACA" permit portals at `aca-prod.accela.com/...` and self-hosted `…/CitizenAccess`). For every permit you get record number, type, trade category, status, opened date, address with city/state/ZIP, description and a link to the record. Turn on details to add **job valuation, contractor name, license, phone and address**, parcel number and the portal's extra fields.

Built for **roofers, solar installers, HVAC and pool contractors, remodelers, suppliers, lead-generation agencies and n8n / Make / Zapier users** who want fresh permits every morning instead of paying for a one-off city extraction.

*Keywords: building permit leads, new permits API, Accela permits scraper, roofing permits, solar permits, pool permits, HVAC permits, new construction permits, contractor leads, permit data export.*

### What you can do

- 🏗️ **Search one or many cities/counties at once**: 30+ known portals in a dropdown (Tampa, Hillsborough, Pinellas, Pasco, Sacramento, Clark County NV, Pima County AZ, Salt Lake City, Denver, Atlanta, Louisville, Indianapolis, Oklahoma City, Virginia Beach, …) plus **any other Accela agency** by code or portal URL
- 🔧 **Filter by trade**: `roofing`, `solar`, `pool`, `hvac`, `electrical`, `plumbing`, `new-construction`, `remodel`, `demolition`, … or any keyword; or let the portal filter by its own record types
- 💰 **Details**: valuation, contractor company, license number and type, phone, address, parcel number, applicant company
- 🔔 **Monitor mode**: each run saves only permits it has not seen before, with alerts on **Telegram, Slack, email or a webhook** (n8n, Make, Zapier). Schedule it daily
- 🛡️ **Privacy by default**: owner and applicant names are left out unless you switch them on
- ⚡ **Efficient and robust**: plain HTTP with ASP.NET postbacks (no browser for most portals), the portal's CSV export for big pulls, automatic retries and a real-browser fallback for portals that block plain requests

### Quick start

**Roofing and solar permits in Tampa and Hillsborough County, last 7 days, with contractor and valuation:**

```json
{
  "agencies": ["TAMPA", "HCFL"],
  "lastNDays": 7,
  "permitTypes": ["roofing", "solar"],
  "includeDetails": true
}
```

**Every building permit opened in September in a city that is not in the list:**

```json
{
  "customAgencies": ["https://aca-prod.accela.com/SPOKANE/Cap/CapHome.aspx?module=Building"],
  "dateFrom": "2026-09-01",
  "dateTo": "2026-09-30",
  "maxRecordsPerAgency": 0
}
```

**Daily pool-permit leads to a Make/n8n webhook (schedule once a day):**

```json
{
  "agencies": ["PINELLAS", "PASCO", "HCFL"],
  "permitTypes": ["pool"],
  "includeDetails": true,
  "lastNDays": 5,
  "monitorName": "tampa-bay-pools",
  "webhookUrl": "https://hook.eu1.make.com/your-hook-id"
}
```

### Output

One item per permit (dataset views: **Permits** and **Contractors & valuation**):

```json
{
  "agency": "PINELLAS",
  "agencyName": "Pinellas County, FL",
  "module": "Building",
  "recordNumber": "BLD-26-04001",
  "recordType": "Residential Solar",
  "category": "solar",
  "status": "In Review",
  "openedDate": "2026-09-27",
  "issuedDate": null,
  "expirationDate": "2027-03-26",
  "address": "1037 SEMINOLE BLVD",
  "city": "Seminole",
  "state": "FL",
  "zip": "33772",
  "fullAddress": "1037 SEMINOLE BLVD, SEMINOLE FL 33772",
  "parcelNumber": "19-29-15-10001-000-0100",
  "description": "Install 11 kW roof mounted photovoltaic system with battery",
  "projectName": null,
  "valuation": 32100,
  "contractorName": "BRIGHT SKY SOLAR INC",
  "contractorPerson": "QUALIFIER 1",
  "contractorLicense": "CVC5730001",
  "contractorLicenseType": "Certified Solar Contractor",
  "contractorPhone": "(727) 555-2001",
  "contractorAddress": "501 INDUSTRIAL WAY, LARGO, FL, 33771",
  "applicantName": null,
  "applicantCompany": "PERMIT RUNNERS INC",
  "ownerName": null,
  "ownerMailingAddress": null,
  "moreDetails": { "Job Value($)": "$32,100.00", "Construction Type": "V-B" },
  "detailsFetched": true,
  "detailUrl": "https://aca-prod.accela.com/PINELLAS/Cap/CapDetail.aspx?Module=Building&TabName=Building&capID1=26CAP&capID2=00000&capID3=…",
  "portalUrl": "https://aca-prod.accela.com/PINELLAS/Cap/CapHome.aspx?module=Building&TabName=Building",
  "source": "grid",
  "scrapedAt": "2026-09-27T17:07:15.510Z"
}
```

*(Example values from the test portal.)* Fields the portal does not publish are `null`. Without details you get everything up to `description` plus the links. `issuedDate` is only filled where the portal's results list has an issued-date column; most portals list the **opened (applied) date**, which is what the date search filters on. In monitor mode items also carry `monitorName`, `isNew` and `detectedAt`. The run summary (`OUTPUT` in the key-value store) lists per agency how many records were read, matched and saved, how the portal was read (HTTP / browser, grid / CSV) and any error.

### Input

All fields are optional; with no input the Actor returns the last 7 days of Pinellas County, FL building permits.

| Field | Default | What it does |
|---|---|---|
| `agencies` | `["PINELLAS"]` | Known portals (dropdown). |
| `customAgencies` | – | Other portals: agency code (`KERNCO`) or full URL of the search page, also self-hosted ones. |
| `module` | agency's usual | `Building`, `Permits`, `Development`, … as in `CapHome.aspx?module=…`. |
| `lastNDays` / `dateFrom` / `dateTo` | 7 days | Period of the record **opened** date. |
| `permitTypes` | all | Trade names (roofing/roof, solar, pool, hvac, electrical, plumbing, new-construction/new, addition, remodel, demolition, windows, fence, sign, accessory-structure, mobile-home) or any keyword. |
| `excludeKeywords` | – | Drop permits mentioning these words. |
| `statuses` | any | Keep permits whose status contains e.g. `Issued`. |
| `recordTypes` | – | Let the portal filter by its "Record Type" dropdown (text contains). |
| `maxRecordsPerAgency` | 500 | Newest first; `0` = no limit. |
| `includeDetails` | false | Detail page per permit: contractor, valuation, parcel, extra fields. |
| `includePersonalNames` | false | Owner / applicant names and owner mailing address (with details). |
| `monitorName` | – | Monitor mode: only permits not seen before. |
| `telegramBotToken` + `telegramChatId`, `slackWebhookUrl`, `webhookUrl`, `emailTo` | – | Alerts in monitor mode. |
| `proxyConfiguration` | Apify Proxy | Use RESIDENTIAL (US) if a portal refuses datacenter IPs. |
| `exportMode` | auto | `grid` (links for every permit), `csv` (portal's "Download results"), `auto` (CSV only for >100 results without details). |

Advanced: `searchWindowDays` (7), `maxPagesPerAgency` (100), `browserFallback` (true), `forceBrowser` (false), `requestDelayMs` (700), `maxRetries` (3), `saveDebugPages` (true), `stateStoreName`, `reportAllOnFirstRun`, `resetState`, `notifyMaxItems`, `webhookMaxItems`, `notifyOnNoChanges`, `emailSubject`.

#### Finding an agency code

Open the city's permit search in your browser. If the address looks like `https://aca-prod.accela.com/TAMPA/Cap/CapHome.aspx?module=Building`, the agency code is `TAMPA` and the module is `Building`. You can also paste the whole address into `customAgencies`. If the module is wrong, the log lists the modules the portal offers.

### Monitor mode and alerts

1. Set `monitorName` (e.g. `"tampa-roofing"`) and your filters, then **schedule** the Actor (daily is typical).
2. The **first run is a silent baseline**: it remembers the permits currently in the period without saving them (turn on `reportAllOnFirstRun` to get them too).
3. Every following run saves only **new** permits and sends one message per channel. Keep `lastNDays` at 3–7 so permits that the city enters a few days late are still caught; duplicates never come back.
4. The webhook receives JSON: `{ "event": "permits.new", "monitorName", "summary": { "newPermits", "checked", "agencies", "failedAgencies" }, "resultsUrl", "runId", "datasetId", "message", "permits": [ … ] }`. In n8n use a *Webhook* trigger node, in Make a *Custom webhook*.

Changing the filters of a monitor starts a new baseline so you are not flooded with old permits. `resetState` forgets everything.

### Pricing (pay per event)

| Event | Price | When |
|---|---|---|
| `permit` | $0.01 | Each permit saved to the dataset |
| `permit-details` | $0.01 | Each permit whose detail page was loaded (`includeDetails`) |

Searching, paging and monitor baselines are free; failed detail pages are not charged. 1,000 permits cost **$10**, or **$20 with contractor and valuation**. Set a *maximum cost per run* and the Actor stops cleanly at that budget; in monitor mode, permits that did not fit are reported on the next run.

### How it works

ACA portals are ASP.NET WebForms applications. The Actor opens the module's **General Search** page, fills the start/end date fields and posts the form back with the page's `__VIEWSTATE` and `ACA_CS_FIELD` like a browser would, then follows the result grid's pager (10 rows per page) or clicks **Download results** for a CSV of the whole list. Long periods are split into windows (7 days by default, newest first). Grid columns differ per agency, so they are mapped by their header text. When exactly one record matches, ACA opens it directly; that is handled too.

If a portal blocks plain HTTP (Cloudflare/WAF answer), the Actor rotates to a new proxy IP, then loads the portal once in a real Chrome and continues over HTTP with the browser's cookies, and finally works entirely inside the browser. Pages that do not look as expected are saved to the key-value store as `DEBUG-…` and the log says what the portal answered.

### Limits and good to know

- Only **public** search pages work. Modules behind a login and portals with a **CAPTCHA** on the search are reported with a clear message and skipped.
- The date search filters on the record's **opened/applied** date. To get only issued permits, set `statuses: ["Issued"]` with a period long enough to cover the city's issuing delay (e.g. `lastNDays: 30`); in monitor mode note that a permit is reported once, the first time it matches.
- Coverage and field names depend on each city's configuration. Some cities do not publish contractor or valuation on the detail page.
- Please keep the default delay between requests; these are city servers.

### Legal and privacy

Building permits are public records, and this Actor only reads pages any visitor can open without an account. Owner and applicant names are **off by default**; if you switch them on, you are responsible for complying with privacy, telemarketing (TCPA / Do-Not-Call) and anti-spam laws when contacting people, and with the terms of use of each portal. The Actor is not affiliated with Accela, Inc. or any government agency.

### Support

Missing a city, a wrong field, or a portal that changed? Open an issue on the Actor's page with the portal URL and the run ID. The `OUTPUT` summary and `DEBUG-…` pages usually show exactly what happened.

# Actor input Schema

## `agencies` (type: `array`):

Accela Citizen Access portals to search, by agency code (the part after aca-prod.accela.com/ in the portal address). Each one uses its usual permit module (Building, Permits or Development) unless you set "Module". Not in the list? Add it under "Other agencies".

## `customAgencies` (type: `array`):

Any other Accela Citizen Access portal: an agency code such as "SPOKANE" or "KERNCO", or the full URL of its search page, e.g. "https://aca-prod.accela.com/TAMPA/Cap/CapHome.aspx?module=Building" or a self-hosted one like "https://accela.fortworthtexas.gov/CitizenAccess/Cap/CapHome.aspx?module=Development". A module in the URL is used for that portal.

## `module` (type: `string`):

ACA module to search, as in the portal address (…/CapHome.aspx?module=Building). Empty = each agency's usual permit module (Building for most; Permits or Development for some). Other modules such as Enforcement or Planning also work if the portal offers a public date search.

## `lastNDays` (type: `integer`):

Permits opened (applied/filed) in the last N days, today included. Used when "Opened from" is empty. For a daily monitor keep 3–7 so permits the city enters late are still caught.

## `dateFrom` (type: `string`):

Start of the period (record opened/applied date, the date ACA's general search filters on), e.g. 2026-09-01 or "30 days". Overrides "Last N days".

## `dateTo` (type: `string`):

End of the period (inclusive). Empty = today.

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

Keep only matching permits. Trade names match the permit's category: roofing (or "roof"), solar, pool, hvac, electrical, plumbing, new-construction (or "new"), addition, remodel, demolition, windows, fence, sign, accessory-structure, mobile-home. Any other word is searched in record type, description and project name (word start, case-insensitive). Empty = all permits.

## `excludeKeywords` (type: `array`):

Skip permits whose record type, description or project name contains any of these words, e.g. "commercial" or "fence".

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

Keep only permits whose status contains one of these words, e.g. "Issued" or "Finaled" (case-insensitive). Statuses differ per city; see the "status" field of a first run. Empty = any status.

## `recordTypes` (type: `array`):

Optional: search only these entries of the portal's "Record Type" dropdown (text contains, case-insensitive), e.g. "Roof" or "Residential Solar". Faster on busy portals because the portal filters. If nothing matches, the log lists the portal's record types and all types are searched.

## `maxRecordsPerAgency` (type: `integer`):

Stop after saving this many matching permits per agency (newest first). 0 = no limit.

## `includeDetails` (type: `boolean`):

Open each permit's detail page to add contractor name, license, phone and address, job valuation, parcel number, applicant company and extra fields. One extra request per permit and one extra "permit details" charge.

## `includePersonalNames` (type: `boolean`):

With details on, also output the property owner's name and mailing address and the applicant's name when the portal publishes them. Off by default: these are private individuals; check the laws that apply to you (e.g. telemarketing and privacy rules) before using them for outreach.

## `monitorName` (type: `string`):

Turns on monitor mode: the Actor remembers which permits it has seen under this name and each run saves (and alerts about) only new ones. Schedule it daily. The first run only remembers the current permits unless "Report all on first run" is on.

## `reportAllOnFirstRun` (type: `boolean`):

In monitor mode, save and alert about all matching permits of the period on the first run too (normally the first run is a silent baseline).

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

Forget everything this monitor has seen and create a new baseline.

## `telegramBotToken` (type: `string`):

Token of your Telegram bot (from @BotFather). Alerts are sent in monitor mode.

## `telegramChatId` (type: `string`):

Chat, group or channel ID that receives the alerts (the bot must be a member).

## `slackWebhookUrl` (type: `string`):

Slack Incoming Webhook URL (https://hooks.slack.com/services/…).

## `webhookUrl` (type: `string`):

Receives a JSON POST with a summary and the new permits (event "permits.new"), ready for n8n, Make or Zapier.

## `emailTo` (type: `string`):

Email address(es) for alerts, comma-separated. Sent through the apify/send-mail Actor.

## `emailSubject` (type: `string`):

Custom subject for email alerts. Empty = "<monitor>: N new building permit(s)".

## `notifyMaxItems` (type: `integer`):

How many new permits are listed in Telegram, Slack and email messages (the rest is summarised).

## `webhookMaxItems` (type: `integer`):

How many new permits are included in the webhook JSON body.

## `notifyOnNoChanges` (type: `boolean`):

Also send a message when a monitor run finds no new permits (useful as a heartbeat).

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

Apify Proxy is recommended. If a portal refuses datacenter IPs, choose RESIDENTIAL with country US.

## `exportMode` (type: `string`):

"grid" reads the results table page by page (10 rows per request) and gives every permit a direct link. "csv" clicks the portal's "Download results" when available: one request for the whole list, but no per-permit links, so details cannot be fetched. "auto" uses CSV only for pulls above 100 permits without details and outside monitor mode, and falls back to the grid when the export does not work.

## `searchWindowDays` (type: `integer`):

Long periods are searched in windows of this many days, newest first, so a portal's result limits are not hit.

## `maxPagesPerAgency` (type: `integer`):

Safety limit of result pages (10 permits each) read per search window.

## `browserFallback` (type: `boolean`):

If a portal blocks plain HTTP requests, retry with cookies from a real Chrome, then fully inside Chrome.

## `forceBrowser` (type: `boolean`):

Do every request inside Chrome from the start (slower; for portals known to need JavaScript).

## `requestDelayMs` (type: `integer`):

Pause between requests to one portal, to be polite to city servers.

## `maxRetries` (type: `integer`):

How often a failed request or blocked session is retried (with a new session and IP) before an agency is given up.

## `saveDebugPages` (type: `boolean`):

Store unexpected portal pages in the key-value store (DEBUG-…) so problems can be diagnosed.

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

Named key-value store that keeps monitor memory between runs.

## Actor input object example

```json
{
  "agencies": [
    "PINELLAS"
  ],
  "lastNDays": 7,
  "maxRecordsPerAgency": 50,
  "includeDetails": false,
  "includePersonalNames": false,
  "reportAllOnFirstRun": false,
  "resetState": false,
  "notifyMaxItems": 10,
  "webhookMaxItems": 100,
  "notifyOnNoChanges": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "exportMode": "auto",
  "searchWindowDays": 7,
  "maxPagesPerAgency": 100,
  "browserFallback": true,
  "forceBrowser": false,
  "requestDelayMs": 700,
  "maxRetries": 3,
  "saveDebugPages": true,
  "stateStoreName": "accela-permits-monitor"
}
```

# Actor output Schema

## `results` (type: `string`):

All permits saved by this run.

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

Permits with contractor and valuation columns (filled when details are on).

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

Per-agency counts, how each portal was read (HTTP/browser, CSV/grid) and errors.

# 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 = {
    "agencies": [
        "PINELLAS"
    ],
    "lastNDays": 7,
    "maxRecordsPerAgency": 50,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("egra_van/accela-building-permits").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 = {
    "agencies": ["PINELLAS"],
    "lastNDays": 7,
    "maxRecordsPerAgency": 50,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("egra_van/accela-building-permits").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 '{
  "agencies": [
    "PINELLAS"
  ],
  "lastNDays": 7,
  "maxRecordsPerAgency": 50,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call egra_van/accela-building-permits --silent --output-dataset

```

## MCP server setup

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

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/FdM5gaat1bmfr4NTx/builds/htdfxX6JOaVtvumX8/openapi.json
