# DC DDOT TOPS Permits - Construction & Occupancy (`j0401/dc-tops-permits`) Actor

Washington DC DDOT right-of-way permits (public open data): 543k construction permits and 983k occupancy permits, 2005-present. Fees broken out by kind, three named parties, ward/ANC/SMD/ZIP, district overlays and pre-computed cycle times. Search by address, owner, permittee, ward or status.

- **URL**: https://apify.com/j0401/dc-tops-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 ddot tops 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 DDOT TOPS Permits - Construction & Occupancy

Anything that touches a Washington DC street, sidewalk or alley has to be filed with DDOT's Transportation Online Permitting System first - construction staging areas, excavations, paving, street fixtures, banners, block parties, moving trucks. This actor turns that register into a **charged-per-record search, filter and aggregate tool** over **two registers at once**: **543,201 construction permits** and **982,887 occupancy permits**.

**Built for:** contractor and right-of-way diligence, street-closure and staging research, procurement and subcontractor intelligence, neighborhood impact studies, and anyone who needs **the District's own permit record** rather than an address-by-address lookup.

### What it covers

Each register is queried on its own (`corpus`), and each record carries:

- **the permit** - permit number, tracking number, status, permit type, issue / application / effective / expiration / approval dates
- **the parties** - property owner, permittee, and applicant company
- **the money** - the total fee with its parts broken out: permit fee, technology fee, inspection amount, meter fee and deposit
- **where** - the work-site address plus ward, ANC, SMD, ZIP, quadrant, neighborhood, historic district, BID and zoning
- **how long it took** - `daysToAssign`, `daysToApprove`, `daysToIssue`, pre-computed by the District rather than left for you to subtract
- **the location** - latitude and longitude, on every row in both registers

### The fine print that matters

**The two registers carry different fields, and this actor tells you rather than guessing.** `permitType` (New / Replace / Repair) exists only on the construction register; `eventType` - the ~34 values like *Construction Staging Area*, *Block Party*, *Banner and Seasonal Display* - exists only on the occupancy register. Asking for a dimension your corpus does not have is **rejected with a clear error**, not silently ignored.

**`permitType` is stored as a code, and it is not empty.** The District keeps this column as an integer whose domain says `0 = New`, `1 = Replace`, `2 = Repair`. Read raw, a column that is `0` on most rows looks unpopulated; decoded, it is filled on **504,624 of 543,201** construction permits. This actor returns the decoded label.

**A row is a work location, not necessarily a whole permit.** A permit covering several street segments is filed as several rows that share one permit number, so counting rows and calling it "permits" overstates - the newest day's batch is 87 rows spread across 7 permit numbers, and the largest single number seen is 50 rows. The row is the billing unit and each one carries its own address and coordinates.

**Some fields are thin, and they are not sold as features here.** The construction register's contractor name is present on only **42,997 of 543,201 rows (7.9%)**, its meter amount on 4.1%, its BID overlay on 7.2% and its historic-district overlay on 20.8%. On the occupancy register the EWR number is present on **0.13%** of rows. **The ANC field is a placeholder - the literal `AN` - on 268,928 of 543,201 construction rows (49.5%)**, where the row's SMD still names a real commission; filtering `anc` therefore misses every permit carrying the placeholder. They are returned as fields, but nothing in this listing rests on them.

**A permit that is "expired" usually just ran its course.** On the construction register **448,875 of 543,201 permits sit at `Permit Expired`** and on the occupancy register **799,563 of 982,887 sit at `EXPIRED`**. That is the normal end state of a permit whose work is finished and whose term lapsed - not a cancelled one. `Cancel/Withdrawn` and `Denied` are the statuses that mean the permit did not proceed.

**Default order is by issue date, deliberately.** The District edits this layer continuously, but most edits are old permits being flipped to expired by a batch job - ordering by last-updated would surface five stale permits rather than the ones issued this week. Issued-date ordering returns genuinely current permits.

### Typical questions

- "Every **right-of-way permit at an address** or in a **ZIP**."
- "What did **<contractor or utility>** pull permits for?"
- "**Street-closure and staging** permits in a ward or ANC."
- "**Moving-truck** and **block-party** occupancy permits."
- "Permits **issued in the last 90 days** and still open."
- "How long DDOT takes: **days to issue**, aggregated by ward."
- "Aggregate by **status**, **ward**, **ANC**, **ZIP**, **event type** or **fiscal year**."

### Inputs

| Input | What it does |
|---|---|
| `mode` | `rows` (default) / `aggregate` |
| `corpus` | `construction` (default) / `occupancy` |
| `permitNumber` / `trackingNumber` | exact permit or tracking number |
| `address` | work-site address substring |
| `owner` / `permittee` / `applicant` | name substring on each party |
| `ward` / `anc` / `zip` / `quadrant` | district filters; `zip` matches as a prefix |
| `status` | exact status, as the register spells it |
| `permitType` | `New` / `Replace` / `Repair` - construction corpus only |
| `eventType` | occupancy-register event kind (e.g. *Block Party*) - occupancy corpus only |
| `fiscalYear` | District fiscal year |
| `issuedFrom` / `issuedTo` | issue-date range (inclusive) |
| `groupBy` | aggregate over status / permitType / eventType / ward / anc / zip / quadrant / fiscalYear |

> Aggregate groups are counted over the same filters as `rows`; `quadrant` and `fiscalYear` are available dimensions too.
> | `maxResults` | cap records (default 50) |

**Default run = 50 construction rows, newest issue date first.** Because one permit can span several rows, a 50-row page may cover only a handful of permit numbers - raise `maxResults`, or filter by permit number, when you need a full permit. 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
{ "permitNumber": "PA489240" }
```

**A ward's recent permits**

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

**Utility work by the owner**

```json
{ "owner": "Washington Gas", "maxResults": 5 }
```

**Occupancy permits - street events and block parties**

```json
{ "corpus": "occupancy", "eventType": "Block Party", "maxResults": 5 }
```

**How the register splits by status**

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

### 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 construction permit** - `permitNumber=PA489240` returns its status, fees,
parties, location and cycle times:

```json
{
  "platform": "dc-tops-permits",
  "source": "dc-ddot-tops",
  "corpus": "construction",
  "mode": "rows",
  "groupKey": "",
  "groupCount": "",
  "groupBy": "",
  "permitNumber": "PA489240",
  "trackingNumber": "489240",
  "status": "Issued",
  "statusCode": "9",
  "permitType": "New",
  "eventType": "",
  "issueDate": "2026-09-25",
  "applicationDate": "2026-02-23",
  "effectiveDate": "2026-08-03",
  "expirationDate": "2027-08-02",
  "lastApprovalDate": "2026-07-14",
  "lastUpdateDate": "2026-09-25",
  "address": "1200 - 1225 BLOCK OF NEW HAMPSHIRE AVENUE NW",
  "ownerName": "Wash Gas & Light Co.",
  "permitteeName": "Wash Gas & Light Co.",
  "applicantCompany": "Wash Gas & Light Co.",
  "contractorName": "",
  "warehouseNumber": "",
  "constructionPermitNumber": "",
  "totalFee": "148.5",
  "technologyFee": "13.5",
  "permitFee": "135",
  "inspectionAmount": "0",
  "meterAmount": "",
  "depositAmount": "0",
  "workDetail": "INSTALL ABOVE GROUND POLE-MOUNTED ELECTRONIC PRESSURE RECORDER, WR#4071366, P#1031491",
  "typeDetailNames": "Fixture; Street Fixture or Furniture (Exception)",
  "roadClosed": "",
  "daysToAssign": "113",
  "daysToApprove": "141",
  "daysToIssue": "215",
  "ward": "2",
  "anc": "AN",
  "smd": "2A06",
  "zip": "20036",
  "quadrant": "NW",
  "neighborhood": "West End, Foggy Bottom, GWU",
  "historicDistrict": "",
  "bid": "",
  "zoning": "CR",
  "fiscalYear": "2026",
  "latitude": "38.90558552",
  "longitude": "-77.04724693",
  "sourceUpdatedAt": "2026-09-27"
}
```

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

```json
{
  "platform": "dc-tops-permits",
  "source": "dc-ddot-tops",
  "corpus": "construction",
  "mode": "aggregate",
  "groupKey": "Permit Expired",
  "groupCount": "448875",
  "groupBy": "status",
  "permitNumber": "",
  "issueDate": "",
  "address": "",
  "ownerName": "",
  "sourceUpdatedAt": "2026-09-27"
}
```

### Notes

- **Source:** DC Department of Transportation, published on the District's open
  GIS server. No login, no key.
- **Freshness:** every record carries `sourceUpdatedAt`, the newest edit date in
  the register (measured: the probe day).
- **Coverage:** District of Columbia only. Permits issued from **2005** in the
  construction register and from **2009** in the occupancy register (applications
  in the construction register reach back to 2004).

# Actor input Schema

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

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

## `corpus` (type: `string`):

construction = DDOT right-of-way construction permits (543,201). occupancy = occupancy permits for work in the public space (982,887). The two registers carry different fields; see each input's note.

## `permitNumber` (type: `string`):

Exact permit number, e.g. 'PA444553-R1'. Blank = any.

## `trackingNumber` (type: `string`):

Exact internal tracking number. Blank = any.

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

Work-site address substring, e.g. 'OAK STREET'. Blank = any.

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

Property owner name substring. Blank = any.

## `permittee` (type: `string`):

Permittee name substring - the party the permit is issued to. Blank = any.

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

Applicant company name substring. Blank = any.

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

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

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

Advisory Neighborhood Commission, e.g. '2A'. Blank = any.

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

Work-site ZIP, matched as a prefix (e.g. '200' for all 200xx ZIPs). Blank = any.

## `quadrant` (type: `string`):

DC quadrant. Blank = any.

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

Exact status as the District records it. Construction corpus uses e.g. 'Issued', 'Permit Expired', 'Cancel/Withdrawn', 'Not Paid'; occupancy corpus uses e.g. 'ISSUED', 'EXPIRED', 'NOTPAID', 'WITHDRAWN', 'REJECTED'. Blank = any.

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

Construction corpus only. The District stores this as a code; it is decoded to New / Replace / Repair here. Blank = any.

## `eventType` (type: `string`):

Occupancy corpus only, exact value - e.g. 'Block Party', 'Construction Staging Area', 'Banner and Seasonal Display', 'Moving Truck', 'Wedding'. 34 values in all. Blank = any.

## `fiscalYear` (type: `string`):

District fiscal year, e.g. '2026'. Blank = any.

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

Earliest issue date (YYYY-MM-DD), e.g. '2025-01-01'. Blank = any.

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

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

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

aggregate mode only. 'permitType' exists on the construction corpus and 'eventType' on the occupancy corpus; asking for the wrong one for your corpus is rejected with a clear error rather than ignored.

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

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

## Actor input object example

```json
{
  "mode": "rows",
  "corpus": "construction",
  "permitNumber": "",
  "trackingNumber": "",
  "address": "",
  "owner": "",
  "permittee": "",
  "applicant": "",
  "ward": "",
  "anc": "",
  "zip": "",
  "quadrant": "",
  "status": "",
  "permitType": "",
  "eventType": "",
  "fiscalYear": "",
  "issuedFrom": "",
  "issuedTo": "",
  "groupBy": "ward",
  "maxResults": 50
}
```

# Actor output Schema

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

DC DDOT TOPS 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-tops-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-tops-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-tops-permits --silent --output-dataset

```

## MCP server setup

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