# Us Municipal Building Permits Accela Scraper (`arezdev/us-municipal-building-permits-accela-scraper`) Actor

Search public Accela Citizen Access permits by agency, date and keyword. Extract permit status, project addresses, disclosed job values and contractor details. Export construction records for project monitoring, market research and prospecting.

- **URL**: https://apify.com/arezdev/us-municipal-building-permits-accela-scraper.md
- **Developed by:** [Arezdev](https://apify.com/arezdev) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $6.00 / 1,000 building permit extracteds

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

**Find municipal construction opportunities with permit dates, project addresses, job values, and public contractor details in one clean dataset.**

Search public Accela Citizen Access records for solar, roofing, HVAC, electrical, planning, and commercial projects. Export JSON, CSV, Excel, or retrieve results through Apify's dataset API. Direct HTTP requests preserve session cookies without running a browser.

Best for monitoring public permits on compatible Accela Citizen Access portals. Verify the agency URL and search module first, then use a short date range. Agency names are presets, not universal coverage; use CUSTOM when a municipality changes its portal.

### How to use

1. Choose an agency and public module, or select `CUSTOM` and enter the current official Citizen Access URL.
2. Set an inclusive filed-date range and optionally enter a keyword such as `solar`, `roofing`, or `HVAC`.
3. Set the maximum records and whether to retrieve contractor details, then start the Actor.
4. Filter the dataset to `status: SUCCESS` for extracted permits. ERROR rows explain unavailable pages without a permit event fee.

The form starts with San Antonio (the COSA portal), the Building module, September 1-5, 2026, and a three-row sample. The API default is 25 rows. The legacy SANJOSE selector remains available, but its former Accela endpoint returned HTTP 404 during review. Use the municipality's current official portal when a preset becomes unavailable.

Preset agency identifiers are used literally as paths on `https://aca-prod.accela.com/`. Agencies can use another host or identifier, and a preset does not guarantee current availability. For example, [San Antonio's public portal](https://aca-prod.accela.com/COSA/Cap/CapHome.aspx?module=Building) uses COSA; enter that URL with CUSTOM. Verify the agency's official portal before changing the target.

### What data can you extract?

| Fields | Meaning |
| --- | --- |
| `agencyCode`, `permitNumber` | Selected agency and public record identifier |
| `status` | Extraction result: SUCCESS or ERROR |
| `permitType`, `permitStatus` | Agency's permit type and business status |
| `filedDate` | Filed date normalized to YYYY-MM-DD when recognized |
| `address`, `projectDescription` | Public project location and description |
| `jobValuation` | Public job value in USD as a number |
| `contractorName`, `contractorBusiness` | Public professional and business |
| `contractorPhone`, `contractorLicenseNumber` | Public contractor phone and license |
| `applicantName` | Public applicant name |
| `detailUrl`, `scrapedAt` | Record link and extraction time in UTC |
| `errorMessage` | Safe ERROR diagnostic; null on successful records |

Every row has the same flat fields. Unpublished information remains `null`. Agency layouts and available fields vary; see [Accela's record documentation](https://help.accela.com/Content/User/recordDetails.htm). Where several professionals are present, the first public professional is used.

### Input settings

| Setting | Default | Notes |
| --- | --- | --- |
| `agencyCode` | SANANTONIO | SANJOSE, ATLANTA_GA, TAMPA, DENVER, CLEVELAND, SACRAMENTO, OAKLAND, SANANTONIO, CUSTOM |
| `customAccelaPortalUrl` | Unset | Required for CUSTOM; agency base or full CapHome URL |
| `moduleName` | Building | Building, Permits, or Planning |
| `startDate`, `endDate` | 2026-09-01, 2026-09-05 | Inclusive, valid calendar dates |
| `keywordFilter` | Unset | Case-insensitive literal substring in listing number, type, status, address, and description; up to 256 characters |
| `fetchContractorDetails` | true | Also retrieves valuation and applicant data |
| `maxItems` | 25 | 1–2,000; editor prefill 3; includes ERROR rows |
| `timeoutSecs` | 20 | 5–60 seconds per HTTP attempt |
| `proxyConfiguration` | Unset | Direct first; datacenter fallback on 403/429/503 |

Set `useApifyProxy` to `false` to force direct connections. Providing `proxyUrls` forces those proxies exclusively. Otherwise, blocked requests try SHADER datacenter and default datacenter access. A configured final tier, including residential groups, is allowed only when `useApifyProxy` is explicitly true. Runtime-supplied Apify proxy credentials are required; this Actor never requests the account profile to discover them. Proxy traffic can incur separate charges.

```json
{
  "agencyCode": "ATLANTA_GA",
  "moduleName": "Building",
  "startDate": "2026-09-01",
  "endDate": "2026-09-05",
  "keywordFilter": "solar",
  "fetchContractorDetails": true,
  "maxItems": 3,
  "timeoutSecs": 20,
  "proxyConfiguration": { "useApifyProxy": false }
}
```

Temporary HTTP 502, 503 and 504 responses on GET requests receive at most one retry per route, with a cancellable wait of up to two seconds. Longer Retry-After instructions are not shortened, and form submissions are not retried this way.

Native CSV export is preferred when enabled and sufficient for requested detail extraction. Otherwise, the Actor reads the grid, follows ASP.NET pagination, and preserves fresh WebForms state. Filtering precedes detail requests; permit numbers are deduplicated within the run. Portal JavaScript is never executed.

Both legacy CSV postbacks and current Accela downloads through `Export2CSV.ashx` are supported. The current download is fetched directly after its export postback using the same validated portal session.

### Output example

Illustrative data showing the exact output shape:

```json
{
  "agencyCode": "ATLANTA_GA",
  "permitNumber": "EXAMPLE-2026-001",
  "status": "SUCCESS",
  "permitType": "Residential Solar PV",
  "permitStatus": "Issued",
  "filedDate": "2026-09-02",
  "address": "100 Example Avenue",
  "projectDescription": "Install rooftop solar system",
  "jobValuation": 24500,
  "contractorName": "Example Contractor",
  "contractorBusiness": "Example Solar LLC",
  "contractorPhone": "+1 415-555-0100",
  "contractorLicenseNumber": "EXAMPLE-123456",
  "applicantName": "Example Applicant",
  "detailUrl": null,
  "scrapedAt": "2026-10-19T12:00:00.000Z",
  "errorMessage": null
}
```

Spreadsheet formula prefixes are escaped in dataset strings. Valid phone numbers and numeric values are preserved.

### Use the results in Clay, n8n, Make, or your API

Import CSV into Clay and join by permit number and address. In n8n or Make, start the Actor, wait for completion, retrieve dataset items, and filter SUCCESS rows. Use Apify schedules for daily date windows and deduplicate across runs by `(agencyCode, permitNumber)`.

Retrieve `/v2/datasets/{datasetId}/items?format=json&clean=true` from `api.apify.com` for API integrations. Keep authentication in your automation platform's secret configuration.

### Pricing and limits

The custom pay-per-event event is `permit-scraped`. Only SUCCESS records carry that event. **ERROR rows cost $0 in permit event fees.** Apify compute, storage, proxy, and separately configured platform events can still apply. Do not configure a separate default-dataset-item charge if ERROR records must remain free of result event fees.

The actual price is configured in Apify Console and displayed on the Store page; this repository does not set a live price. At an example rate of $0.005 per successful permit, 1,000 successes cost $5 in permit event fees. A run with 900 successes and 100 errors costs $4.50 at that rate. This is an example, not an advertised live price. See [Apify PPE configuration](https://docs.apify.com/sdk/js/docs/concepts/pay-per-event).

Non-paying Apify users can save up to 25 total rows per run, including errors. Paying users can request up to 2,000. Local runs skip the free-plan gate but obey `maxItems`. Budget exhaustion stops new work and exits cleanly. Writes are serialized and flushed every 100 records or five seconds, and on shutdown.

Cloud defaults are 256 MB and a one-hour timeout. Up to 25 detail workers share four simultaneous network response slots. Decompressed response limits are 4 MiB for HTML/CSV, 2 MiB for JSON, and 15 MiB for XML. Recoverable incomplete HTML can yield a successful record when recognizable detail content remains; logs identify the recovery. Incomplete search pages do not continue pagination. Narrow the date range when exports are oversized.

### FAQ and troubleshooting

**Why does a run finish successfully with ERROR rows?**

Network errors, TLS failures, unavailable portals, and unsupported pages become diagnostics. Invalid input is the only application-level reason for exit code 1. Check row statuses to confirm whether records were retrieved.

**Why are contractor details or valuation null?**

The agency may not publish those fields, the record may lack them, or detail fetching is disabled. A failed requested detail preserves listing data in an uncharged ERROR row.

**Why are there no rows?**

An explicit portal no-records message produces an empty dataset. Check the module, dates, and keyword. Unrecognized result pages produce an ERROR instead of silently claiming zero matches.

**Can this solve CAPTCHAs or log in?**

It supports public, browser-free searches and reports challenges or login requirements safely. Confirm anonymous access and a current official URL.

**How are custom URLs protected?**

Each destination and redirect must resolve entirely to public unicast addresses. Connections pin validated IPs while preserving the HTTP authority and TLS server name. Proxy CONNECT tunnels also target validated destination IPs. Local networks, URL credentials, nondefault destination ports, insecure redirect downgrades, cross-origin form submissions, and external detail links are blocked. TLS validation stays enabled.

**How do I build and test locally?**

Use Node.js 22 or newer:

```sh
npm ci
npm run build
npm test
npm run smoke -- ATLANTA_GA
```

The smoke command performs a real three-record direct-only search without initializing the Apify SDK or reading credential files. With no argument it uses the required default agency; it fails validation when no SUCCESS is returned. Append `listing` after the agency to skip detail requests.

For a full local Actor run, write input to `storage/key_value_stores/default/INPUT.json` and run `npm start`. Build the container with `docker build -t accela-permits .`; the final image runs as non-root `node`.

Before publication, configure Limited Permissions, register `permit-scraped` and its live price in Apify Console, and validate the chosen agency from the intended cloud region. The repository does not publish or change account settings.

# Actor input Schema

## `agencyCode` (type: `string`):

Agency portal preset. San Antonio maps to COSA. Availability can change; use CUSTOM for a current official Accela portal. The legacy San Jose preset may be unavailable.

## `customAccelaPortalUrl` (type: `string`):

Required for CUSTOM. Agency base URL or full Cap/CapHome.aspx URL. Public HTTP/HTTPS URLs without credentials only.

## `moduleName` (type: `string`):

Search module enabled by the selected agency.

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

Inclusive start date in YYYY-MM-DD format.

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

Inclusive end date in YYYY-MM-DD format. Must not precede the start date.

## `keywordFilter` (type: `string`):

Optional case-insensitive literal substring matched against listing number, type, status, address, and description. Examples: solar, roofing, HVAC.

## `fetchContractorDetails` (type: `boolean`):

Open matching detail pages for public contractor, applicant, and valuation fields. Missing public fields remain null.

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

Maximum combined SUCCESS and ERROR rows. Free Apify users are limited to 25 saved rows per run.

## `timeoutSecs` (type: `integer`):

Timeout per HTTP attempt. Redirects and permitted fallbacks can require additional attempts.

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

Direct first, then datacenter on HTTP 403/429/503. Explicitly enable Apify Proxy for a configured final tier, including residential groups. Set useApifyProxy to false for direct-only access. Custom proxy URLs are exclusive.

## Actor input object example

```json
{
  "agencyCode": "SANANTONIO",
  "moduleName": "Building",
  "startDate": "2026-09-01",
  "endDate": "2026-09-05",
  "fetchContractorDetails": true,
  "maxItems": 3,
  "timeoutSecs": 20
}
```

# Actor output Schema

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

Default dataset items. Review each record status before using extracted data downstream.

# 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 = {
    "agencyCode": "SANANTONIO",
    "maxItems": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("arezdev/us-municipal-building-permits-accela-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 = {
    "agencyCode": "SANANTONIO",
    "maxItems": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("arezdev/us-municipal-building-permits-accela-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 '{
  "agencyCode": "SANANTONIO",
  "maxItems": 3
}' |
apify call arezdev/us-municipal-building-permits-accela-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,arezdev/us-municipal-building-permits-accela-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/86dJXTTKejdkXnuNv/builds/AeVhMO9X6EsiczmNf/openapi.json
