# DC Building Permits - 2009 to Present (`j0401/dc-building-permits`) Actor

Washington DC building permits (public open data, year layers 2009-2026): type/subtype/category, status, work description, applicant and owner, fees, address with ward/ANC/SMD, zoning and the SSL parcel key that joins to DC property records.

- **URL**: https://apify.com/j0401/dc-building-permits.md
- **Developed by:** [Wenhao Yang](https://apify.com/j0401) (community)
- **Categories:** Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.03 / 1,000 dc building permit records

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

## DC Building Permits - 2009 to Present

Every building permit the District of Columbia issues lands in a public register: the permit type and category, the status of the application, the work being described, who applied, who owns the property, what was paid, where it is - and the **SSL parcel key that links the permit to the District's property records**. This actor turns that register into a **charged-per-record search, filter and aggregate tool** across **18 year layers, 2009 through 2026**.

**Built for:** contractor and applicant research, construction-activity and development tracking, permit-history diligence on an address, neighborhood and ward analysis, and anyone who needs **the District's own permit record** rather than a map-by-map lookup.

### What it covers

Each record carries:

- **the permit** - permit id, internal number, type, subtype, category, and the application status
- **the dates** - issue date and last-modified date
- **the work** - the free-text description of work
- **the people** - applicant and owner of record
- **the money** - total fees paid, with the fee type string itemising what the fee was for
- **the place** - full address, ZIP, ward, ANC, SMD, police district, neighborhood cluster, business improvement district, and zoning
- **the parcel key** - `ssl`, the Square-Suffix-Lot identifier that joins this register to the District's CAMA property tables
- **the coordinates** - latitude and longitude, on every row

### The fine print that matters

**The District publishes one layer per calendar year, and this actor makes you pick one.** There is no single table spanning 2009-2026 - the source is partitioned, and `year` selects the layer that gets queried. The newest year is live: the 2026 layer carried **35,691 permits for the year to date**, the most recent issued **2026-09-23**. To trace one address across the decade, query the years you care about one at a time.

Recent years: **2026 - 35,691** / **2025 - 50,086** / **2024 - 47,161** / **2009 - 28,511**. Volumes vary year to year, and older years are smaller; the guard on each layer is set below the smallest of them.

**There is no usable ZIP column, and this actor does not pretend otherwise.** `CITY`, `STATE` and `ZIPCODE` are **null on every row of every year layer** (verified layer by layer). The ZIP lives inside the address string instead (`..., WASHINGTON, DC 20002`), so the `zip` filter matches against the address and the `zip` output field is parsed from it. Coverage varies by year - **99.8% of 2026 rows** carry one, but only **75.9% of 2009 rows** do; the remainder return blank and cannot be found by a ZIP filter.

**Some fields are thin, and they are not sold as features here.** The work description is populated on **41.7%** of 2026 permits and the applicant on **50.5%**; the business improvement district on 19.0% of 2026 permits and the fee-type breakdown on 64.9% of 2025 permits. They are returned as fields, but nothing in this listing rests on them. The fields that *are* dense - permit id, issue date, address, type, status, fees paid, ward, ANC, SMD, coordinates - are filled on every row.

**A row is a permit, and one address often has many.** Supplementary, post-card and home-occupation permits pile up on the same property, so a plain address search returns the whole paper trail rather than one record. That is the register's real granularity and each row is billed once.

### Typical questions

- "Every permit at an **address** - the full paper trail."
- "What has **<applicant or contractor>** filed for?"
- "**Construction** permits in a **ward** or **ZIP**."
- "Permits **issued in the last 90 days** and still open."
- "**High-fee** permits - where the money went."
- "Permits by **zoning** or **ANC**, or on a given **SSL parcel**."
- "Aggregate by **type**, **status**, **category**, **ward** or **zoning**."

### Inputs

| Input | What it does |
|---|---|
| `mode` | `rows` (default) / `aggregate` |
| `year` | which year layer to query (2009-2026); default = the current year |
| `permitId` / `ssl` | exact permit number / parcel key |
| `address` | full-address substring |
| `applicant` / `owner` | name substring on each party |
| `permitType` / `category` / `status` | exact values as the District records them |
| `ward` / `anc` / `zoning` | district and zoning filters |
| `zip` | digits only; matched inside the address string |
| `issuedFrom` / `issuedTo` | issue-date range (inclusive) |
| `minFee` | minimum fees paid |
| `groupBy` | aggregate over status / permitType / category / ward / anc / district / zoning |
| `maxResults` | cap records (default 50) |

**Default run = 50 permits from the current year, newest issue date first** - a plain read of the live layer, fast enough for the daily auto-test. For a targeted query add a filter; for a broad view use `aggregate`.

### Example inputs

**One permit by number**

```json
{ "permitId": "P2612157" }
```

**An address's permits in the current year**

```json
{ "address": "MASSACHUSETTS AVE", "maxResults": 5 }
```

**A ward's recent permits**

```json
{ "ward": "2", "maxResults": 5 }
```

**Permits in a ZIP, issued in a window**

```json
{ "zip": "20002", "issuedFrom": "2026-01-01", "maxResults": 5 }
```

**How this year splits by permit type**

```json
{ "mode": "aggregate", "groupBy": "permitType" }
```

### Low cost

**From $0.00005 per record, down to $0.00003 at Gold** Pay-per-event: you are charged per record delivered, and nothing for the query. Cost scales with what you pull, not with the size of the register, and each record is metered individually - one address's permit history costs a fraction of a cent.

### Example output

**One permit** - `permitId=P2612157` in the 2026 layer returns its type, status,
parties, fees and location:

```json
{
  "platform": "dc-building-permits",
  "source": "dc-dcra-building-permits",
  "year": "2026",
  "mode": "rows",
  "groupKey": "",
  "groupCount": "",
  "groupBy": "",
  "permitId": "P2612157",
  "internalNumber": "252612157",
  "issueDate": "2026-09-23",
  "lastModifiedDate": "2026-09-24",
  "permitType": "SUPPLEMENTAL",
  "permitSubtype": "PLUMBING AND GAS",
  "category": "NA",
  "status": "PERMIT ISSUED",
  "descriptionOfWork": "",
  "address": "3955 MASSACHUSETTS AVE NW, WASHINGTON, DC 20016",
  "zip": "20016",
  "ward": "3",
  "anc": "ANC 3A",
  "smd": "3A02",
  "district": "SECOND",
  "psa": "204",
  "neighborhoodCluster": "Cluster 14",
  "businessImprovementDistrict": "",
  "zoning": "R-1B",
  "ssl": "",
  "applicant": "",
  "ownerName": "TROY, ADAM W",
  "feesPaid": "262",
  "feeType": "26.00 (GASRANGE);26.00 (WATERHEATER);186.00 (PLUMBFIXTURE);23.80 (PLUMBINGEFEE)",
  "latitude": "38.93324576",
  "longitude": "-77.07897127",
  "sourceUpdatedAt": "2026-09-24"
}
```

Note `descriptionOfWork` and `applicant` are blank on this permit and `zip` is
filled from the address - see *the fine print*.

**An aggregate row** - `{"mode": "aggregate", "groupBy": "permitType"}` returns
one row per type, with the record fields blank:

```json
{
  "platform": "dc-building-permits",
  "source": "dc-dcra-building-permits",
  "year": "2026",
  "mode": "aggregate",
  "groupKey": "SUPPLEMENTAL",
  "groupCount": "19176",
  "groupBy": "permitType",
  "permitId": "",
  "address": "",
  "ward": "",
  "ownerName": "",
  "sourceUpdatedAt": "2026-09-24"
}
```

### Notes

- **Source:** DC Department of Buildings / DCRA, published on the District's open
  GIS server. No login, no key.
- **Freshness:** each row carries `sourceUpdatedAt`, the newest edit date in the
  layer (checked on the current year; past years are archives and are not
  required to be fresh).
- **Coverage:** District of Columbia, 2009 to present. One layer per year - the
  register is not a single searchable table.

# Actor input Schema

## `mode` (type: `string`):

rows = permits matching your filters (default). aggregate = one count row per group (see groupBy).

## `year` (type: `string`):

The District publishes one layer per calendar year; this selects which one is queried. Default = the current year (live). Query past years one at a time to trace an address across the decade.

## `permitId` (type: `string`):

Exact permit number, e.g. 'P2612157'. Blank = any.

## `ssl` (type: `string`):

Exact Square-Suffix-Lot parcel identifier - the key that joins this register to DC's CAMA property tables. Blank = any.

## `address` (type: `string`):

Full-address substring, e.g. 'MASSACHUSETTS AVE'. Blank = any.

## `applicant` (type: `string`):

Applicant name substring. Blank = any.

## `owner` (type: `string`):

Owner name substring. Blank = any.

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

Exact type, e.g. 'CONSTRUCTION', 'SUPPLEMENTAL', 'POST CARD', 'HOME OCCUPATION'. Blank = any.

## `category` (type: `string`):

Exact category name as the District records it. Blank = any.

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

Exact application status, e.g. 'PERMIT ISSUED', 'COMPLETED', 'EXPIRED', 'PERMIT CANCELED'. Blank = any.

## `ward` (type: `string`):

DC ward, e.g. '2'. Blank = any.

## `anc` (type: `string`):

Advisory Neighborhood Commission as the District stores it, e.g. 'ANC 2A' (the prefix is part of the value). Blank = any.

## `zip` (type: `string`):

Work-site ZIP, digits only, e.g. '20002'. The District stores no ZIP column here, so this matches the ZIP inside the address string. Blank = any.

## `zoning` (type: `string`):

Exact zoning code, e.g. 'R-1B'. Codes were re-written over the years, so an old code (e.g. 'R-4') only matches the layers it was current in. Blank = any.

## `issuedFrom` (type: `string`):

Earliest issue date (YYYY-MM-DD). Blank = any.

## `issuedTo` (type: `string`):

Latest issue date (YYYY-MM-DD), inclusive. Blank = any.

## `minFee` (type: `number`):

Only permits with at least this much in fees paid (USD). Blank = any.

## `groupBy` (type: `string`):

aggregate mode only: the dimension to count by, within the selected year.

## `maxResults` (type: `integer`):

Maximum rows to return in rows mode (1-2000). Default 50.

## Actor input object example

```json
{
  "mode": "rows",
  "year": "2026",
  "permitId": "",
  "ssl": "",
  "address": "",
  "applicant": "",
  "owner": "",
  "permitType": "",
  "category": "",
  "status": "",
  "ward": "",
  "anc": "",
  "zip": "",
  "zoning": "",
  "issuedFrom": "",
  "issuedTo": "",
  "groupBy": "permitType",
  "maxResults": 50
}
```

# Actor output Schema

## `recordsUrl` (type: `string`):

DC building permit records or aggregates - as JSON

## `datasetUrl` (type: `string`):

No description

## `runUrl` (type: `string`):

No description

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("j0401/dc-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 = {}

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,j0401/dc-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/uXhHFldLkTgS9fwp6/builds/ZRpimpfRNs1em3QRU/openapi.json
