# Accela & EnerGov Building Permit Portal Scraper (Any City) (`owlabee/accela-energov-permit-scraper`) Actor

Scrape building permits from US cities and counties on Accela Citizen Access or Tyler EnerGov Citizen Self Service portals: permit type, status, dates, valuation, address, contractor. Normalized JSON, daily new-permit monitor with webhooks. No login, no proxies. $0.003/permit.

- **URL**: https://apify.com/owlabee/accela-energov-permit-scraper.md
- **Developed by:** [Owlabee](https://apify.com/owlabee) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 permit record scrapeds

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

## Accela & EnerGov Building Permit Portal Scraper (Any City)

Scrape building permits from US cities and counties that publish nothing on an open-data portal and run **Accela Citizen Access (ACA)** or **Tyler EnerGov Citizen Self Service (CSS)** instead. The Actor talks to the same public search the portal's web page uses (EnerGov's JSON search API, ACA's General Search page), paginates newest-first, and writes every permit in one normalized JSON schema: permit number, type and work class, status, applied / issued / finalized / expiration dates, address, parcel, valuation, contractor, owner and applicant. The schema is the same as our **US Building Permits Scraper** (26 open-data cities), so records from both Actors are interchangeable in one pipeline.

Built for contractor lead generation (roofing, solar, HVAC, pools, remodels), real-estate and proptech data teams, insurers and lenders. Monitor mode flags only the permits not seen on the previous run, so a daily schedule plus a webhook is a new-permit feed for any of the **31 verified jurisdictions** (17 EnerGov, 14 Accela) below. No login, no proxies, no CAPTCHA solving, no browser. Results are available as JSON, CSV, Excel, through the API, webhooks and MCP.

Unofficial: not affiliated with Accela, Inc. or Tyler Technologies, Inc.

### How to use

1. Pick one or more **Jurisdictions** (or `all`, `all-energov`, `all-accela`) and the **Issued within (days)** window.
2. Optionally filter by **Permit type keywords**, **Work description keyword** or **Minimum valuation**, turn on **Fetch permit details** for contractor, owner, valuation and parcel, and set **Max permits per jurisdiction** together with the run's maximum cost.
3. Click **Start**. For a feed, schedule the run daily with **Only new permits since last run** on and a **Webhook URL**; the first run is the baseline.

### Input

| Field | Type | Required | Description |
|---|---|---|---|
| `jurisdictions` | string\[] | yes | Registry ids (table below), or `all`, `all-energov`, `all-accela`. Each portal queried is one `portal-run` event. |
| `issuedWithinDays` | integer | no | Window in days (1–365, default 30). EnerGov filters on the **issued** date; ACA's General Search filters on the **record (application) date**, so on ACA this means "applied within". |
| `permitTypes` | string\[] | no | Case-insensitive words matched against the permit type and work class (`roof`, `electrical`, `residential`…). |
| `keyword` | string | no | Case-insensitive text matched against the description (EnerGov applies it server-side, ACA client-side). |
| `minEstimatedCost` | integer | no | Drop permits whose valuation is below this amount; permits with no valuation are kept. Valuation needs `includeDetails`. |
| `maxResultsPerJurisdiction` | integer | no | Newest-first cap per jurisdiction (default 500). |
| `includeDetails` | boolean | no | Fetch each permit's detail record/page (valuation, contractor, license, owner, applicant, parcel). One extra request and one `permit-detail-fetched` event per permit. Default off. |
| `onlyNewSinceLastRun` | boolean | no | Monitor mode: emit only permits not seen before (per jurisdiction, up to 50,000 ids), flagged `isNew: true`. Details are fetched only for new permits. |
| `stateStoreName` | string | no | Key-value store that holds the seen-sets (default `accela-energov-permit-scraper-state`). |
| `includeRaw` | boolean | no | Attach the untouched portal row (and detail) as `raw`. |
| `accelaAppId` | string (secret) | no | Optional free Accela developer app id. When set, Accela jurisdictions are read through the official Construct API's anonymous mode instead of the ACA page (see FAQ). |
| `webhookUrl` | string | no | POST target for the run summary plus up to 500 emitted permits. |

### Output example

One dataset item per permit:

```json
{
  "jurisdiction": "hernando-county-fl",
  "jurisdictionName": "Hernando County, FL",
  "permitNumber": "ELER-000154-2026",
  "permitType": "General Electric - Residential",
  "permitSubtype": "Electrical",
  "workDescription": "Replace 200A service panel and meter can",
  "status": "Issued",
  "appliedDate": "2026-10-14T10:40:15",
  "issuedDate": "2026-10-16T15:12:23",
  "finalizedDate": null,
  "expirationDate": "2027-04-14T15:12:23",
  "estimatedCost": 3630,
  "address": "1234 Example Ave, SPRING HILL, FL, 34606",
  "city": "SPRING HILL",
  "state": "FL",
  "zip": "34606",
  "parcelId": "1097388",
  "latitude": null,
  "longitude": null,
  "contractorName": "RIVERA'S ELECTRIC INC.",
  "contractorLicense": "ID-000000718",
  "ownerName": null,
  "applicantName": "RIVERA'S ELECTRIC INC.",
  "sourceApi": "energov",
  "sourceUrl": "https://hernandocountyfl-energovweb.tylerhost.net/apps/selfservice#/permit/8c0f…",
  "sourceRecordId": "8c0f…",
  "fetchedAt": "2026-10-17T10:50:12.345Z",
  "detailFetched": true,
  "isNew": true
}
```

`sourceApi` is `energov`, `accela-aca` or `accela-api`. Without `includeDetails`, `estimatedCost`, `contractorName`, `contractorLicense`, `ownerName` and `applicantName` are `null` and `detailFetched` is `false`. On Accela Citizen Access the list page has no issue date, so `issuedDate` is `null` and `appliedDate` carries the record date. Coordinates are only available through the Accela API engine.

A portal that cannot be queried produces one error item instead of failing the run:

```json
{ "jurisdiction": "denver-co", "jurisdictionName": "Denver, CO", "error": "CAPTCHA", "errorMessage": "Portal shows a CAPTCHA on its search page; not supported (no CAPTCHA solving)", "sourceApi": "accela-aca", "sourceUrl": "https://aca-prod.accela.com/DENVER/Cap/CapHome.aspx?module=Development&TabName=Development", "fetchedAt": "2026-10-17T10:50:12.345Z" }
```

Error codes: `HTTP_<status>`, `RATE_LIMITED`, `BAD_RESPONSE`, `PORTAL_ERROR`, `NOT_ENERGOV`, `UNSUPPORTED_PORTAL`, `CAPTCHA`, `API_FORBIDDEN`, `FETCH_FAILED`. The run's `SUMMARY` record (default key-value store) lists per-jurisdiction counts, pages, seconds, portal version and charges.

### Jurisdictions

All 31 portals were verified live on 2026-10-17 from a datacenter IP with the exact requests the Actor sends (`docs/jurisdictions.md` in the repository has the evidence and the rejected candidates). Volume is the number of permits the portal returned for the previous 7 days.

| Id | Jurisdiction | Engine | Date filter | 7-day volume | Notes |
|---|---|---|---|---|---|
| `richmond-va` | Richmond, VA | EnerGov | issued | ~270 | All permit modules incl. trades and right-of-way |
| `overland-park-ks` | Overland Park, KS | EnerGov | issued | ~220 | Prefill / daily test |
| `southlake-tx` | Southlake, TX | EnerGov | issued | ~70 | Prefill / daily test |
| `doral-fl` | Doral, FL | EnerGov | issued | ~70 | |
| `hernando-county-fl` | Hernando County, FL | EnerGov | issued | ~370 | |
| `riverside-county-ca` | Riverside County, CA | EnerGov | issued | ~420 | Includes transport and land-use permits |
| `santa-fe-nm` | Santa Fe, NM | EnerGov | issued | ~120 | |
| `danville-ca` | Danville, CA | EnerGov | issued | ~40 | |
| `carson-ca` | Carson, CA | EnerGov | issued | ~40 | |
| `imperial-beach-ca` | Imperial Beach, CA | EnerGov | issued | 8 | |
| `wilmington-nc` | Wilmington, NC | EnerGov | issued | ~40 | |
| `la-vergne-tn` | La Vergne, TN | EnerGov | issued | ~40 | |
| `grand-traverse-county-mi` | Grand Traverse County, MI | EnerGov | issued | ~220 | Incl. well & septic records |
| `reading-pa` | Reading, PA | EnerGov | issued | 3 | Low volume |
| `shelton-wa` | Shelton, WA | EnerGov | issued | 9 | Low volume |
| `snohomish-wa` | Snohomish, WA | EnerGov | issued | 11 | Low volume |
| `rockland-county-ny` | Rockland County, NY | EnerGov | issued | 3 | County plan review and health permits |
| `sacramento-ca` | Sacramento, CA | Accela ACA | applied | 100+ | 10 rows per page |
| `tampa-fl` | Tampa, FL | Accela ACA | applied | 100+ | |
| `hillsborough-county-fl` | Hillsborough County, FL | Accela ACA | applied | 100+ | |
| `oakland-ca` | Oakland, CA | Accela ACA | applied | 200+ | 20 rows per page |
| `san-mateo-county-ca` | San Mateo County, CA | Accela ACA | applied | ~80 | 25 rows per page |
| `stockton-ca` | Stockton, CA | Accela ACA | applied | 100+ | |
| `monterey-county-ca` | Monterey County, CA | Accela ACA | applied | ~100 | 4 MB result pages; slower |
| `santa-clara-county-ca` | Santa Clara County, CA | Accela ACA | applied | ~140 | 50 rows per page |
| `mesa-az` | Mesa, AZ | Accela ACA | applied | 100+ | Module `Permits` |
| `omaha-ne` | Omaha, NE | Accela ACA | applied | 100+ | Module `Permits` |
| `oklahoma-city-ok` | Oklahoma City, OK | Accela ACA | applied | 100+ | Module `Permits` |
| `fort-worth-tx` | Fort Worth, TX | Accela ACA | applied | 100+ | Module `Development` |
| `denver-co` | Denver, CO | Accela ACA | applied | 100+ | Module `Development` |
| `charlotte-nc` | Charlotte, NC | Accela ACA | applied | 100+ | City land-development records; building permits are Mecklenburg County's |

ACA's General Search caps its total at "100+" / "200+"; the Actor pages through until the window or `maxResultsPerJurisdiction` is exhausted. ACA rows that are unsubmitted application drafts (`26TMP-…`, no record page) are skipped and counted in the summary.

### Pricing

Pay per event. You pay only for what the Actor produces; set a **maximum cost per run** in the run options to cap spend (the Actor stops cleanly when the cap is reached).

| Event | Price | Notes |
|---|---|---|
| `permit-scraped` | $0.003 | Per dataset item: a normalized permit or an error item for a portal that could not be queried |
| `permit-detail-fetched` | $0.005 | Per permit whose detail record/page was fetched and merged (`includeDetails`); in addition to `permit-scraped` |
| `portal-run` | $0.01 | Per jurisdiction portal queried per run, including portals that return an error item |
| Actor start | $0.00005 | Apify platform event |

**Examples:** 1,000 permits from 5 portals without details ≈ **$3.05**; 1,000 permits with details ≈ **$8.05**; a daily monitor of 10 portals that finds 40 new permits with details ≈ $0.42 per day. Compute is billed by Apify separately (≈ $0.001 per 100 permits at 512 MB).

### Limits and fair use

- Requests to each portal are spaced at least 1 s apart, with exponential backoff on 429/5xx; three portals run in parallel. A 30-day crawl of a large ACA city with details is a few hundred requests and takes a few minutes.
- `issuedWithinDays` is capped at 365 and ACA agencies commonly cap their own date range at one year.
- Only public data is read. No login, no proxies, no CAPTCHA solving, no browser. A portal that shows a CAPTCHA or blocks datacenter IPs returns an `UNSUPPORTED_PORTAL` / `CAPTCHA` error item and is listed as unsupported in `docs/jurisdictions.md`.
- Portal software changes: EnerGov releases differ per city (2022.3 to 2026.2 are in the registry) and Accela updates ACA several times a year. When a portal changes shape the Actor emits an error item rather than wrong data; failures are watched daily and patched.
- Memory 256–1024 MB; a run with `all` and details fits comfortably in 512 MB.

### Related Actors

- **US Building Permits Scraper — City & County Permit Records**: the same schema for 26 open-data cities and counties (Socrata, CKAN, ArcGIS, Carto).
- **US Professional License Verifier**: verify the contractor licenses that appear in these permits.
- **Website Change Monitor**: watch a permit-office news page for fee or process changes.

### FAQ

**Does this need proxies?** No. Both portal families answer plain HTTP from datacenter IPs for every jurisdiction in the registry; the Actor never uses residential proxies. If an agency starts blocking, it is marked unsupported rather than worked around.

**How is cost controlled?** Every item is charged as it is saved. Set the run's maximum cost, `maxResultsPerJurisdiction`, a short `issuedWithinDays` window and leave `includeDetails` off unless you need valuation and contractor fields. Monitor mode pays for details only on new permits.

**Why is `issuedDate` null on Accela portals?** ACA's General Search filters and lists by the record (application) date and shows no issue date in the grid or on the detail page, so `appliedDate` is filled and `status` (Issued, Finaled, Applied…) tells you where the permit is. EnerGov portals filter on the issue date and fill `issuedDate`.

**What does `accelaAppId` do?** Accela's Construct API has a "no authorization required" mode for citizen apps: with a free developer-portal app id the Actor can read records (with addresses, parcels, professionals and owners in one JSON call) from agencies that allow anonymous access. The engine follows the public v4 reference and was not exercised against a live agency during the build, so treat it as best-effort: agencies that refuse return an `API_FORBIDDEN` error item, and removing the id switches back to the page engine.

**Can you add my city?** If it runs EnerGov Citizen Self Service or Accela Citizen Access with a public General Search page, usually yes. Agencies that load their search panel through partial postbacks (San Diego, Detroit, Salt Lake City, Alameda County, Santa Barbara County in our survey) or show reCAPTCHA (Fresno, Detroit) are not supported.

**Can an AI agent use this?** Yes. Example prompts: "List the roofing and solar permits applied for in Sacramento and Tampa in the last 7 days with contractor names." · "Every morning, find new residential permits in Overland Park and Hernando County, only ones not seen before, and POST them to my CRM webhook." · "Which contractors pulled the most electrical permits in Richmond, VA over the last 30 days?"

### Legal

Public records from public portals, read with the same requests the portal's own web page sends, at a polite rate, without login or circumvention. Owner, applicant and contractor names appear as published by the agency. **This data is not a consumer report and may not be used for credit, employment, insurance, housing or any other purpose covered by the Fair Credit Reporting Act (FCRA).** Verify permits with the issuing agency before relying on them. Not affiliated with Accela, Inc. or Tyler Technologies, Inc.; portal and product names belong to their owners.

# Actor input Schema

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

Cities and counties to query. Each entry is one Accela Citizen Access (ACA) or Tyler EnerGov Citizen Self Service portal from the verified registry; pick "All supported jurisdictions" to crawl every entry, or the per-vendor groups. Each portal queried is one paid portal-run event; see the README table for engine, date semantics and volume per jurisdiction.

## `issuedWithinDays` (type: `integer`):

Only permits whose portal date falls in the last N days (1–365). EnerGov portals filter on the issued date; Accela Citizen Access general search filters on the record (application) date, so on ACA this is "applied within". Applied server-side on every portal, so narrow windows are fast and cheap.

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

Keep only permits whose type or work class contains one of these words, case-insensitive (for example "electrical", "roof", "solar", "demolition", "residential"). Each portal uses its own type vocabulary, so prefer generic words. Applied client-side after the date filter.

## `keyword` (type: `string`):

Keep only permits whose description (or type) contains this text, case-insensitive (for example "solar", "re-roof", "kitchen", "ADU"). EnerGov applies it server-side on the description; Accela applies it client-side.

## `minEstimatedCost` (type: `integer`):

Drop permits whose declared job value / valuation is below this amount. Valuation is only known when includeDetails is on; permits with no published value are kept (estimatedCost is null).

## `maxResultsPerJurisdiction` (type: `integer`):

Upper bound on permit records saved per jurisdiction per run (newest first). Use it together with the run's maximum cost to control spend.

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

Open every permit's detail record or page to add valuation (estimatedCost), contractor and license, owner, applicant and parcel number. One extra request per permit, charged as a permit-detail-fetched event; roughly 1 s per permit per portal. Off for the daily Store test.

## `onlyNewSinceLastRun` (type: `boolean`):

Monitor mode for schedules: remember the permit ids already seen per jurisdiction (up to 50,000 each) in the state store and emit only permits not seen before, flagged isNew: true. The first run is a baseline and emits (and charges for) everything in the window. Details are fetched only for new permits.

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

Named key-value store that holds the seen-permit sets for monitor mode. Use a different name per schedule if two schedules query the same jurisdiction with different filters.

## `includeRaw` (type: `boolean`):

Attach the untouched portal row (and detail, when fetched) as "raw" on every permit, so you can read portal-specific fields the normalized schema does not carry. Roughly doubles dataset size.

## `accelaAppId` (type: `string`):

Optional app id from a free Accela developer-portal app. When set, Accela jurisdictions are queried through the official Construct API in its anonymous mode (JSON, includes addresses, parcels, professionals, owners) instead of the Citizen Access page. Agencies that do not allow anonymous API access return an error item; remove the id to use the page engine again. Stored encrypted and never logged.

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

If set, the Actor POSTs a JSON run summary plus the emitted permits (max 500; only the new ones in monitor mode) to this URL when it finishes. Failures are logged, never fatal.

## Actor input object example

```json
{
  "jurisdictions": [
    "overland-park-ks",
    "southlake-tx"
  ],
  "issuedWithinDays": 7,
  "maxResultsPerJurisdiction": 25,
  "includeDetails": false,
  "onlyNewSinceLastRun": false,
  "stateStoreName": "accela-energov-permit-scraper-state",
  "includeRaw": false
}
```

# Actor output Schema

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

One item per permit in the default dataset (jurisdictionName, permitNumber, permitType, status, issuedDate, appliedDate, estimatedCost, address, contractorName, ownerName, detailFetched, isNew, sourceUrl; error items carry error and errorMessage). Fields are described in dataset_schema.json.

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

SUMMARY record in the run's default key-value store: portals queried, permits and details produced per jurisdiction, charges and webhook delivery status, written when the run finishes.

# 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": [
        "overland-park-ks",
        "southlake-tx"
    ],
    "issuedWithinDays": 7,
    "maxResultsPerJurisdiction": 25,
    "includeDetails": false,
    "onlyNewSinceLastRun": false,
    "includeRaw": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("owlabee/accela-energov-permit-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": [
        "overland-park-ks",
        "southlake-tx",
    ],
    "issuedWithinDays": 7,
    "maxResultsPerJurisdiction": 25,
    "includeDetails": False,
    "onlyNewSinceLastRun": False,
    "includeRaw": False,
}

# Run the Actor and wait for it to finish
run = client.actor("owlabee/accela-energov-permit-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": [
    "overland-park-ks",
    "southlake-tx"
  ],
  "issuedWithinDays": 7,
  "maxResultsPerJurisdiction": 25,
  "includeDetails": false,
  "onlyNewSinceLastRun": false,
  "includeRaw": false
}' |
apify call owlabee/accela-energov-permit-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,owlabee/accela-energov-permit-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/CY7Bpjl0aXDEcDCG8/builds/sGe4Z9DcvoyghNvih/openapi.json
