# Sacramento County Building Permits Scraper (`automation-lab/sacramento-county-building-permits`) Actor

Search Sacramento County GIS building permits by application, type, status, address, parcel and applied dates. Export work, valuation, contractor and dates; county GIS coverage only, no applicants or inspection history.

- **URL**: https://apify.com/automation-lab/sacramento-county-building-permits.md
- **Developed by:** [Automation Lab](https://apify.com/automation-lab) (community)
- **Categories:** Real estate
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.44 / 1,000 permit exporteds

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

## Sacramento County Building Permits Scraper

Refresh Sacramento County building permits for construction market research and sales prioritization. Search the county's public GIS permit layer and export application IDs, property addresses, parcels, work descriptions, valuations, contractors, statuses and dates.

The Actor reads the Sacramento County GIS **Permits** layer anonymously. Coverage follows that layer, primarily county-administered/unincorporated records, not every municipality in Sacramento County. It does not scrape applicant profiles or inspection history.

### Who is it for?

- Construction suppliers researching projects by application type and status.
- Local analysts comparing permit valuations and work descriptions.
- Property researchers matching an application or parcel to county records.
- Data teams refreshing a bounded county permit extract on a schedule.

This is a record extraction tool, not a compliance determination, contractor endorsement, or guaranteed complete county census.

### Why use it?

Use one normalized row per GIS record instead of manually reading map popups. Exact application and parcel filters help with repeat lookups. Address substring and applied-date filters narrow a research extract. Every row carries a source query link and collection timestamp.

No county login, session cookies, browser, residential proxy, paid source subscription, or user-supplied key is required. The runtime does not use AI. There are no hidden external dependency fees.

### Get started

1. Open the Input tab.
2. Leave the filters blank for a sample of the GIS layer, or choose filters below.
3. Set `maxItems`, for example 100.
4. Start the Actor once and wait for it to finish.
5. Inspect the dataset, then export JSON, CSV, Excel or other formats available in Apify.

The Console prefills a small 20-record example; when `maxItems` is omitted the runtime default is 100.

Default input:

```json
{"maxItems":100}
```

### Search Sacramento County building permits

Filters are combined with **AND**. Strings are trimmed at their edges. Exact filters compare the whole field case-insensitively; they do not remove punctuation or internal spaces.

| Input | Meaning |
| --- | --- |
| `application` | Exact application identifier, such as `MISR2021-00002` |
| `applicationType` | Exact source type, such as `Residential` |
| `status` | Exact source status, such as `Completed` |
| `addressContains` | Case-insensitive contiguous substring within Address; not a geocoder |
| `parcelNumber` | Exact source parcel string; preserve leading zeros and formatting |
| `appliedFrom` | Inclusive UTC date on APPLIED_DATE, `YYYY-MM-DD` |
| `appliedTo` | Inclusive UTC date on APPLIED_DATE, `YYYY-MM-DD` |
| `maxItems` | Global saved-record limit, default 100, allowed 1–10000 |

Blank strings, invalid dates, reversed date ranges and unknown input properties are rejected. `%` and `_` are not accepted in address searches because wildcard matching is not exposed. Missing dates do not match a date filter.

Example for a bounded residential extract:

```json
{
  "applicationType":"Residential",
  "status":"Completed",
  "appliedFrom":"2021-01-01",
  "appliedTo":"2021-01-31",
  "maxItems":100
}
```

An exact known application lookup:

```json
{"application":"MISR2021-00002","maxItems":1}
```

### What data is returned?

| Field | Content |
| --- | --- |
| `objectId` | GIS row ID; can change after source republication |
| `application` | County application identifier; preferred comparison key |
| `applicationType`, `applicationSubtype` | Published type and subtype |
| `status` | Status in the GIS snapshot |
| `address`, `parcelNumber` | Property address and parcel string |
| `projectName`, `workDescription` | Project and proposed work text |
| `valuation` | Published valuation in USD; zero is preserved |
| `contractor` | Published contractor field; may be `OWNER BUILDER` |
| `openedAt`, `appliedAt`, `issuedAt`, `finaledAt` | Source dates converted to ISO UTC |
| `sourceUrl` | Allowlisted GIS query link for this row |
| `scrapedAt` | UTC collection time |

Unavailable optional values are `null`, not fabricated. There are no applicant profiles, email/phone enrichment, owner lookup, documents, geometry, inspection events, or full inspection history. A finaled date is just a source date, not an inspection report.

### Output example

This abbreviated real application example omits property and contractor identifiers:

```json
{
  "application":"MISR2021-00002",
  "applicationType":"Residential",
  "status":"Completed",
  "projectName":"AREA: R01:  Main Breaker Replacement",
  "workDescription":"New Main 100 amp breaker",
  "valuation":0,
  "appliedAt":"2021-01-01T22:23:03.663Z",
  "issuedAt":"2021-01-06T08:00:00.000Z",
  "finaledAt":"2021-01-11T08:00:00.000Z"
}
```

The actual dataset includes every field in the table above. Use the source link to verify the current published row; the county may update it after extraction.

### Source and freshness

The source is the [Sacramento County GIS Permits layer](https://services1.arcgis.com/5NARefyPVtAeuJPU/arcgis/rest/services/Permits/FeatureServer/0). It is published by the Sacramento County GIS organization on ArcGIS Online. Refresh frequency and historical completeness are controlled by the county; this Actor does not promise real-time portal updates. Compare `scrapedAt` with the source's own dates rather than treating collection time as a filing date.

### Limits and ordering

Results are ordered by descending GIS `OBJECTID`, **not guaranteed newest application date**. Pages contain at most 200 records. The limit applies globally after source filters, not per page. Zero and unlimited collection are not supported.

For repeat refreshes, use explicit applied-date windows and compare rows outside the Actor. There is no incremental cache, built-in alert, modified-since filter, historical snapshot service, or guaranteed change feed. Re-running produces a fresh per-run dataset. Compare primarily by `application`; investigate multiple source rows rather than assuming one application can never have more than one GIS row.

Pagination uses a strict object-ID cursor to avoid repeated rows within one run. It is not a transactional snapshot: changes or republication while a run is executing can affect completeness. A limit of 10000 is a cap, not a guarantee that a query has that many matches. Split large studies by applied-date range if appropriate.

### How much does it cost to extract Sacramento County building permits?

Pay per event: a **$0.005 start fee** per valid run plus a per-permit charge. The BRONZE price is **$0.0024 per permit**. No-result valid runs pay the start fee only; invalid input is rejected before charging.

| BRONZE example | Estimated total Actor charge including start |
| --- | --- |
| 1 permit | $0.0074 |
| 10 permits | $0.029 |
| 100 permits | $0.245 |

Spend tiers depend on your qualifying aggregate monthly Apify Store spend, not private volume thresholds for this Actor. Higher tiers may reduce the per-permit price; the Pricing tab is authoritative. Estimates exclude separately applicable platform/account charges and may change with refunds, disputes, corrections or taxes. No earnings or payout guarantee is offered.

Rejected/duplicate source records and failed requests are not separately charged. If a run fails after delivering some rows, those rows remain partial output and may have their permit charges. A billing or upstream failure is not reported as a successful complete export.

### Integrations

- Export CSV into a spreadsheet for a construction opportunity review.
- Send the default dataset to a database or BI pipeline and join on application/parcel.
- Schedule overlapping applied-date windows with Apify Schedules, then compare statuses downstream.
- Use Apify webhooks or Make/Zapier to move a completed dataset to your own systems.

Scheduling and change comparison are configured outside this Actor. A scheduled applied-date window does not detect every change to older records.

### API examples

Use your Apify token from a protected environment variable. Start one run and preserve its ID. For long runs, poll the same run instead of creating another.

```bash
curl -X POST "https://api.apify.com/v2/acts/automation-lab~sacramento-county-building-permits/runs" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"applicationType":"Residential","maxItems":100}'
```

JavaScript with the Apify client:

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({token: process.env.APIFY_TOKEN});
const run = await client.actor('automation-lab/sacramento-county-building-permits')
  .call({maxItems: 100});
if (run.status !== 'SUCCEEDED') throw new Error(`Run ${run.id}: ${run.status}`);
const {items} = await client.dataset(run.defaultDatasetId).listItems({limit: 100});
console.log(items);
```

Python:

```python
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ['APIFY_TOKEN'])
run = client.actor('automation-lab/sacramento-county-building-permits').call(
    run_input={'maxItems': 100})
if run['status'] != 'SUCCEEDED':
    raise RuntimeError(f"Run {run['id']}: {run['status']}")
items = client.dataset(run['defaultDatasetId']).list_items(limit=100).items
print(items)
```

### MCP and AI clients

The Actor itself uses no AI. Clients can discover its schema through the scoped Apify MCP endpoint when the Actor is available to that account.

```bash
claude mcp add --transport http apify \
  "https://mcp.apify.com?tools=automation-lab/sacramento-county-building-permits"
```

Claude Desktop, Cursor, and VS Code: add this equivalent remote HTTP MCP configuration using the client's MCP settings and supported transport:

```json
{"mcpServers":{"apify":{"type":"http","url":"https://mcp.apify.com?tools=automation-lab/sacramento-county-building-permits"}}}
```

Configure authentication through your client's protected credential mechanism. Example prompts: “Look up application MISR2021-00002 and show its status, work description and valuation.” Or: “Export up to 20 Residential permits applied during January 2021; show application, status and valuation.”

1. Discover actual tool names and argument schemas with `tools/list`. Actor selection also exposes run/data helper tools; it does not expose exactly one tool. Discovery alone is not authorization to execute.
2. For an authorized invocation, start once and retain the run ID/status/storage IDs. These are metadata, not permit rows. `call-actor` waits at most 0–45 seconds (default 30). If pending, follow that same run with bounded backoff, for example 2/4/8/10 seconds within a 120-second total deadline. Set the client timeout above the chosen wait plus transport margin. On timeout check the known run, never blindly restart; if its ID is unknown report uncertainty. At the deadline report pending and stop.
3. On success fetch untransformed dataset JSON in pages of 20 rows with explicit fields/offsets, at most 100 source rows and 64 KiB of serialized UTF-8 content admitted to model context. Enforce bytes host-side before injection; omit or summarize oversized rows with disclosure. If the client cannot intercept responses, do not claim a hard byte guarantee. Advance source offsets, not transformed row counts. Report continuation offset and truncation when incomplete; keep full exports outside model context.
4. Report terminal failures honestly, with any rows marked partial. No universal token-saving claim is made: discovery caching and visible context depend on the client.

### Reliability and troubleshooting

Transient network errors, 429 and server 5xx responses receive at most three attempts per page with bounded backoff. Authentication, invalid query, malformed JSON and unexpected response structure fail closed. No automatic premium proxy fallback is enabled.

### FAQ

**Why no results?** Verify the exact type/status or parcel formatting, remove a filter, and widen the applied-date range. A legitimate empty match succeeds with an empty dataset.

**Why is a city permit missing?** This county GIS layer is not every city's permit portal. Incorporated-city records may be outside its coverage.

**Why are some dates or valuations missing?** The county has not supplied the field for that row. A zero valuation is preserved and differs from null.

**Can I retrieve applicants or inspections?** No. Neither feature is implemented or implied by the finaled date.

**What if the source fails halfway through?** The run fails with partial rows retained, not a false success. Fix the input/source condition before retrying; do not combine partial datasets without deduplication.

### Legality and responsible use

Public property and contractor fields may contain personal information, including sole-trader names or incidental names in work descriptions. Use only for a lawful, appropriate research purpose and respect applicable privacy and outreach rules. Public availability is not permission for spam. Verify significant decisions against official county records. This Actor is independent and is not affiliated with or endorsed by Sacramento County or Esri.

Processing uses the public Esri-hosted county GIS source, Apify compute/storage and our private GlitchTip diagnostics. Source requests include only your selected search filters; no third-party model receives input or records. No source credentials are needed. We do not deliberately log addresses, contractor names or raw response bodies; operational logs contain counts and error categories.

Failed operations send sanitized diagnostic input, exceptions and Actor/build/run IDs to our private GlitchTip service for repair. Secret fields and URL query strings are removed; reports are retained for 30 days. Search filter values that are not secrets may remain in sanitized diagnostics. Do not enter private credentials or sensitive free text into filters.

Results and run input/logs are stored in your Apify account under its configured storage retention and deletion controls. The Actor adds no persistent global dataset, cookie jar, cross-run cache, or private archive of successful exports. Delete run datasets/input/logs through Apify when no longer needed; diagnostic deletion requests can be raised through the Actor Issues tab.

### Support and related Actors

Report reproducible problems through this Actor's Apify Issues tab with the run ID and a non-sensitive example. Do not post credentials. User reports receive a substantive response within 14 calendar days.

Other jurisdictions are separate products, not substitutes for Sacramento coverage:

- [Hillsborough County Building Permits Scraper](https://apify.com/automation-lab/hillsborough-county-building-permits-scraper)
- [Los Angeles Building Permits Scraper](https://apify.com/automation-lab/los-angeles-building-permits-scraper)

# Changelog

This Actor's version history is a separate document: https://apify.com/automation-lab/sacramento-county-building-permits/changelog.md

# Actor input Schema

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

Global maximum number of distinct GIS permit records saved per run, after all filters. Default 100, range 1–10000; zero/unlimited is not supported. Pages contain at most 200 records. Results use descending GIS object ID, not guaranteed chronological order. Source coverage and run billing/time limits still apply.

## `application` (type: `string`):

Exact whole application ID, case-insensitive after trimming outer whitespace. Combined with every other supplied filter; not a URL or a partial ID.

## `applicationType` (type: `string`):

Exact whole source application type, case-insensitive after trimming outer whitespace, for example Residential. Not a keyword or category substring.

## `status` (type: `string`):

Exact whole source status, case-insensitive after trimming outer whitespace, for example Completed. Unknown status values legitimately return no results.

## `addressContains` (type: `string`):

Case-insensitive contiguous substring within the source Address field, after trimming outer whitespace. Internal spaces remain significant. Percent and underscore wildcard characters are rejected; this is not a geographic radius search.

## `parcelNumber` (type: `string`):

Exact whole source parcel number after trimming outer whitespace, case-insensitive. Preserve leading zeros and source formatting; punctuation is not normalized.

## `appliedFrom` (type: `string`):

Inclusive UTC calendar date on APPLIED_DATE, in YYYY-MM-DD format. Records with missing applied dates are excluded when either date filter is used. Does not filter issued or finalized dates.

## `appliedTo` (type: `string`):

Inclusive UTC calendar date on APPLIED_DATE, in YYYY-MM-DD format. Must not precede appliedFrom. Combined with other filters; does not mean last modified date.

## Actor input object example

```json
{
  "maxItems": 20
}
```

# Actor output Schema

## `overview` (type: `string`):

Exported permits with identifiers, property/work details, valuation, contractor, dates and source links.

# 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 = {
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("automation-lab/sacramento-county-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 = { "maxItems": 20 }

# Run the Actor and wait for it to finish
run = client.actor("automation-lab/sacramento-county-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 '{
  "maxItems": 20
}' |
apify call automation-lab/sacramento-county-building-permits --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automation-lab/sacramento-county-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/gnRu1Y8mIKwtCW75E/builds/hbLsAqbwjPkn61flI/openapi.json
