# Seattle Building Permits Scraper (`crawlerbros/seattle-building-permits-scraper`) Actor

Scrape live building, trade, and land-use permit records from the City of Seattle's official permit portal. Filter by date range, permit type, status, and address.

- **URL**: https://apify.com/crawlerbros/seattle-building-permits-scraper.md
- **Developed by:** [Crawler Bros](https://apify.com/crawlerbros) (community)
- **Categories:** Real estate, Automation, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
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

## Seattle Building Permits Scraper

Scrape live building, trade, and land-use permit records from the City of Seattle's official permit portal (services.seattle.gov — the Seattle Department of Construction & Inspections' public permit system). Search by date range, permit/record number, permit type, status, and street address, and export clean, structured permit data — no login, no cookies, no residential proxy required.

### What this actor does

- **Searches Seattle's live permit database** — the same "Find Trade, Construction & Land Use Permit Records" tool the City publishes for the public, automated for bulk export.
- **Filters on every axis the portal supports:** creation-date range, exact/partial record number, permit type (54 official categories), record status, and street address (number range, direction prefix, name, type, direction suffix).
- **Optional deep enrichment** — turn on "Fetch permit detail pages" to also pull contractor name/address and parcel number straight from each permit's detail page.
- **Always returns clean records** — every field is real data scraped directly from the portal; empty/unavailable fields are simply omitted, never faked.

### Output per permit record

| Field | Description |
|---|---|
| `recordNumber` | The permit/record number, e.g. `7156800-EL` |
| `permitType` | Permit/record type label, e.g. `Electrical Permit: Over the Counter` |
| `status` | Current record status, e.g. `Issued`, `Completed`, `In Process` |
| `createdDate` | Date the record was created (ISO `YYYY-MM-DD`) |
| `expirationDate` | Permit expiration date, if applicable (ISO `YYYY-MM-DD`) |
| `address` | Street address of the permitted work, when on file |
| `description` | Project description / scope of work |
| `detailUrl` | Direct link to the permit's detail page on the portal |
| `recordId` | Portal-internal composite ID (stable unique key) |
| `sourceUrl` | Canonical URL for this record |
| `workLocationAddress` | *(fetchDetails only)* Work location address from the detail page |
| `contractorName` | *(fetchDetails only)* Contractor name on file |
| `contractorAddress` | *(fetchDetails only)* Contractor's registered address |
| `contractorLicense` | *(fetchDetails only)* Contractor's license number, when disclosed |
| `ownerName` | *(fetchDetails only)* Property/business owner name on file, when disclosed |
| `ownerAddress` | *(fetchDetails only)* Property/business owner's address, when disclosed |
| `parcelNumber` | *(fetchDetails only)* Development site parcel number |
| `permitIssuedDate` | *(fetchDetails only)* Date the permit was issued (ISO `YYYY-MM-DD`) |
| `applicationCompletedDate` | *(fetchDetails only)* Date the application was completed (ISO `YYYY-MM-DD`) |
| `inspectionsCompletedDate` | *(fetchDetails only)* Date all required inspections were completed (ISO `YYYY-MM-DD`) |
| `certificateOfOccupancyIssuedDate` | *(fetchDetails only)* Date the certificate of occupancy was issued (ISO `YYYY-MM-DD`) |
| `projectValue` | *(fetchDetails only)* Declared project value in USD, when on file |
| `totalIssuedValue` | *(fetchDetails only)* Total issued construction valuation in USD |
| `totalIntakeValue` | *(fetchDetails only)* Total intake (as-submitted) construction valuation in USD |
| `reviewLevel` | *(fetchDetails only)* Plan review level, e.g. `Field`, `Full +` |
| `occupancyGroup` | *(fetchDetails only)* Building occupancy classification, e.g. `B Restaurant < 50 Occupants` |
| `typeOfConstruction` | *(fetchDetails only)* Building code construction type, e.g. `Type IIB` |
| `actionType` | *(fetchDetails only)* Permit action type, e.g. `Add/Alt`, `New` |
| `recordType` | Always `"permit"` |
| `scrapedAt` | UTC timestamp of when the record was scraped |

Note: the `applicationCompletedDate` / `projectValue` / `occupancyGroup`-style fields above come from a generic per-permit-type "Application Information" section on the detail page, so which of them are present depends on the permit type — e.g. a Construction Permit exposes valuation and occupancy fields, while an Electrical Permit typically only exposes `reviewLevel` and `permitIssuedDate`. Fields the portal doesn't have data for are simply omitted, never returned as `null`.

Fields that aren't available for a given permit (e.g. a permit still `In Process` has no `expirationDate`) are simply left out of that record — you will never see `null`, `""`, or placeholder values.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `createDateFrom` | string | `2015-01-01` | Only include permits created on/after this date (`YYYY-MM-DD`) |
| `createDateTo` | string | today | Only include permits created on/before this date (`YYYY-MM-DD`) |
| `recordNumber` | string | – | Exact/partial permit number to look up |
| `permitType` | select | any | One of the 54 official Seattle permit/record types |
| `recordStatuses` | array (select) | any | Keep only these statuses (e.g. `Issued`, `Completed`) |
| `addressNumberLow` / `addressNumberHigh` | integer | – | Street number range |
| `streetDirectionPrefix` | select | any | `N` / `S` / `E` / `W` / `NE` / `NW` / `SE` / `SW` |
| `streetName` | string | – | Street name only, e.g. `FREMONT` |
| `streetSuffixType` | select | any | `AVE`, `ST`, `BLVD`, … (20 official types) |
| `streetDirectionSuffix` | select | any | Directional suffix after the street type |
| `fetchDetails` | boolean | `false` | Also open each permit's detail page for contractor/parcel data (slower) |
| `maxItems` | integer | `50` | Max permit records to return (1–1000) |

#### Example: recent electrical permits in a neighborhood

```json
{
  "createDateFrom": "2026-01-01",
  "streetName": "FREMONT",
  "streetSuffixType": "AVE",
  "recordStatuses": ["Issued"],
  "maxItems": 50
}
```

#### Example: look up one specific permit

```json
{
  "recordNumber": "7156800-EL",
  "fetchDetails": true
}
```

#### Example: all construction permits address-ranged and status-filtered

```json
{
  "permitType": "DPDPermits/Building/Construction/NA",
  "addressNumberLow": 100,
  "addressNumberHigh": 9999,
  "streetSuffixType": "AVE",
  "recordStatuses": ["Issued", "In Process"],
  "maxItems": 200
}
```

### Use cases

- **Market research** — track construction and remodel activity by neighborhood or street.
- **Lead generation** — find newly issued permits and their contractors for sales outreach.
- **Compliance & due diligence** — verify a property's permit history before a transaction.
- **Journalism & civic analysis** — monitor development trends across Seattle.
- **Contractor competitive intelligence** — see which contractors are active on which permit types.

### FAQ

**Does this require a login or API key?**
No. The Seattle permit portal is a public government resource; this actor uses no cookies, no API keys, and no residential proxies.

**How far back does the data go?**
The portal's online records generally start around 2005; this actor's date filters default to `2015-01-01` onward for the most useful window. Use `createDateFrom` to reach further back — availability depends on what the portal has indexed.

**Why do some permits have no address or description?**
Certain record types (e.g. `Upload Documents`, `Contractor Disclosure Form`) are administrative sub-records that the portal itself doesn't attach an address or description to. This actor omits those fields rather than showing them blank.

**What does "Fetch permit detail pages" add, and why is it off by default?**
It opens each result's individual detail page to extract contractor/owner name and address, license number, parcel number, construction valuation, key milestone dates (application completed, inspections completed, certificate of occupancy issued), and building classification fields (occupancy group, type of construction, action type) — whichever of these the portal has on file for that permit type. It roughly doubles run time because each record requires an extra page load, so it's opt-in.

**Can I filter by permit status without knowing the exact spelling?**
Yes — `recordStatuses` is a dropdown of every status value used across the portal (e.g. `Issued`, `Completed`, `In Process`, `Withdrawn`), so you never have to guess exact text.

**Why don't Temporary Certificate (`26TMP-*`) records show up when I filter by status?**
A handful of record types — Temporary Certificates and some administrative sub-records — render a genuinely blank Status cell on the portal itself. When `recordStatuses` is set, this actor excludes those status-less rows rather than guessing, so every record in a filtered result reliably has the status you asked for. Leave `recordStatuses` empty to include them alongside everything else.

# Actor input Schema

## `createDateFrom` (type: `string`):

Only include permit records created on or after this date (YYYY-MM-DD). Leave blank to default to 2015-01-01.

## `createDateTo` (type: `string`):

Only include permit records created on or before this date (YYYY-MM-DD). Leave blank to default to today.

## `recordNumber` (type: `string`):

Exact or partial permit record number to look up, e.g. `7156800-EL` or `6821791`. Leave blank to browse all permits in the date range.

## `permitType` (type: `string`):

Restrict results to one permit/record type from Seattle's official list.

## `recordStatuses` (type: `array`):

Only keep permits whose status matches one of the selected values. Leave empty to include every status.

## `addressNumberLow` (type: `integer`):

Lower bound of the street number range, e.g. `100` for `100 5TH AVE`.

## `addressNumberHigh` (type: `integer`):

Upper bound of the street number range.

## `streetDirectionPrefix` (type: `string`):

Directional prefix before the street name, e.g. `N` in `N 45TH ST`.

## `streetName` (type: `string`):

Street name only, without number/suffix, e.g. `FREMONT` for `8611 FREMONT AVE N`.

## `streetSuffixType` (type: `string`):

Street type suffix, e.g. `AVE`, `ST`, `BLVD`.

## `streetDirectionSuffix` (type: `string`):

Directional suffix after the street type, e.g. `N` in `8611 FREMONT AVE N`.

## `fetchDetails` (type: `boolean`):

When enabled, opens each permit's detail page to also extract contractor name/address and parcel number. Roughly doubles run time.

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

Hard cap on the number of permit records to return.

## Actor input object example

```json
{
  "createDateFrom": "2024-01-01",
  "permitType": "",
  "recordStatuses": [],
  "streetDirectionPrefix": "",
  "streetSuffixType": "",
  "streetDirectionSuffix": "",
  "fetchDetails": false,
  "maxItems": 25
}
```

# Actor output Schema

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

Dataset containing all scraped Seattle permit records.

# 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 = {
    "createDateFrom": "2024-01-01",
    "createDateTo": "",
    "permitType": "",
    "recordStatuses": [],
    "streetDirectionPrefix": "",
    "streetSuffixType": "",
    "streetDirectionSuffix": "",
    "fetchDetails": false,
    "maxItems": 25
};

// Run the Actor and wait for it to finish
const run = await client.actor("crawlerbros/seattle-building-permits-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 = {
    "createDateFrom": "2024-01-01",
    "createDateTo": "",
    "permitType": "",
    "recordStatuses": [],
    "streetDirectionPrefix": "",
    "streetSuffixType": "",
    "streetDirectionSuffix": "",
    "fetchDetails": False,
    "maxItems": 25,
}

# Run the Actor and wait for it to finish
run = client.actor("crawlerbros/seattle-building-permits-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 '{
  "createDateFrom": "2024-01-01",
  "createDateTo": "",
  "permitType": "",
  "recordStatuses": [],
  "streetDirectionPrefix": "",
  "streetSuffixType": "",
  "streetDirectionSuffix": "",
  "fetchDetails": false,
  "maxItems": 25
}' |
apify call crawlerbros/seattle-building-permits-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,crawlerbros/seattle-building-permits-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/kDHhzJuc4uR1Xy4w4/builds/aae0wRxrZqK3SPB9u/openapi.json
