# Cincinnati Building Permits - Permits & Named Contacts (`j0401/cincinnati-permits`) Actor

Cincinnati building permits (180,521, 2010 to today): type, work class and status as code and label, dates, address with PIN, and the company on the permit. A contacts mode adds the named owner, contractor and trade parties.

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

## Pricing

from $0.03 / 1,000 cincinnati 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

## Cincinnati Building Permits - Permits & Named Contacts

Every building permit the City of Cincinnati has issued, straight from the
City's own open data portal. **~180.6k permits** covering 2010 to this week
(as of 2026-09-28), plus a depth layer of **444,304 named contacts** - the
owner, contractor, architect, engineer and trade contacts on each permit.

### Low cost

**From $0.00005 per record, down to $0.00003 at Gold.** Pay only for the
records you actually receive - the whole register stays queryable, you are
never billed for the scan behind it.

### What you get

**Permits** - one row per permit filing:

- **Which permit** - the permit number, the type both as a code and as a plain
  label (Building, HVAC, Plumbing, Fire Protection Systems, Wrecking, Signs,
  Elevator, ), the work class (existing or new, again both ways), and the
  permit class (residential, commercial, or non-standard code book)
- **Status** - both the raw code (`CLOSED`, `ISSUED`, `ROUTE`, `WITHDRWN`,
  `APPLIED`, `EXPIRED`, ) and the City's cleaned label ("Permit Finaled",
  "Permit Issued", "In Review")
- **When** - applied, issued, completed, certificate-of-occupancy issued, and
  expires dates, each where the permit has one
- **Where** - the site address, city, ZIP, PIN, the neighborhood, and latitude
  and longitude
- **What** - the work description, the proposed occupancy use, the estimated
  cost, the square footage, the number of units, and the fee
- **Who** - the company on the permit (the contractor, or the literal `OWNER`),
  plus a link to the City's permit record

**Contacts** - one row per named party on a permit, each enriched with its
parent permit's type, status, address, company and issue date, so a contact row
answers "who was on which job" without a second lookup.

### Modes

- **permits** (default) - the permit register, 2010 to today
- **contacts** - the named parties on a permit - ⚠️ **the source carries these
  for permits from 2013 to 2019 only** (see the notes below)
- **aggregate** - counts by permit type, status, work class, neighborhood or
  permit class

### Filters

Permit number, permit type, status, work class, permit class, description,
address, city, ZIP, neighborhood, company name, PIN, applied-date range and
issued-date range. In **contacts** mode also: relationship and contact name.

### Example inputs

**One company's permits**

```json
{ "companyName": "SECO ELECTRIC", "maxResults": 50 }
```

**Permits issued this year**

```json
{ "status": "ISSUED", "issuedFrom": "2026-01-01", "maxResults": 100 }
```

**Permits in one neighborhood, one type**

```json
{ "neighborhood": "OAKLEY", "permitType": "Building", "maxResults": 50 }
```

**Every named party on one permit**

```json
{ "mode": "contacts", "permitNumber": "2016P06260" }
```

**All the contractors the source recorded, 2013-2019**

```json
{ "mode": "contacts", "relationship": "CONTRACTOR", "maxResults": 100 }
```

**Permit volume by type**

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

### Example output

**One permit** - `permitNumber=2026P06867` (empty keys omitted for brevity; every
row carries the full key set):

```json
{
 "platform": "cincinnati-permits",
 "source": "city-of-cincinnati-building-permits",
 "mode": "permits",
 "recordType": "permit",
 "groupKey": "",
 "groupCount": "",
 "groupBy": "",
 "permitNumber": "2026P06867",
 "description": "Fire Protection Systems",
 "appliedDate": "2026-07-21",
 "issuedDate": "2026-08-11",
 "completedDate": "2026-09-04",
 "expiresDate": "2027-08-11",
 "address": "4861 DUCK CREEK RD",
 "city": "CINCINNATI",
 "state": "OH",
 "zip": "45227",
 "jurisdiction": "CINCINNATI",
 "permitClass": "OBC",
 "status": "CLOSED",
 "statusMapped": "Permit Finaled",
 "workClass": "CALT",
 "workClassMapped": "Existing",
 "permitType": "CBPCFAP",
 "permitTypeMapped": "Fire Protection Systems",
 "companyName": "SECO ELECTRIC CO INC",
 "estimatedCost": "2650",
 "totalSqFt": "0",
 "units": "0",
 "pin": "005100070050",
 "proposedUse": "B",
 "fee": "519.33",
 "link": "http://cagis.hamilton-co.org/opal/apd.aspx?QSPerm=2026P06867",
 "latitude": "39.164705899533715",
 "longitude": "-84.4138167585839",
 "neighborhood": "MADISONVILLE",
 "sourceUpdatedAt": "2026-09-25"
}
```

**One contact**, `mode=contacts`, `permitNumber=2016P06260` - note the
`parent*` fields, which come from the related permit:

```json
{
 "mode": "contacts",
 "recordType": "contact",
 "permitNumber": "2016P06260",
 "relationship": "OWNER",
 "contactName": "SOUTHWEST OHIO REGIONAL T",
 "contactAddress1": "602 MAIN ST STE 1100",
 "contactAddress2": "CINCINNATI OH",
 "contactZip": "45202",
 "contactAppliedDate": "2026-07-02",
 "parentPermitType": "CBPCFAP",
 "parentStatus": "ISSUED",
 "parentAddress": "1401 BANK ST",
 "parentCompany": "EXECUTIVE SECURITY SYSTEMS INC",
 "parentIssuedDate": "2016-09-22",
 "sourceUpdatedAt": "2026-09-25"
}
```

**`mode=aggregate`, `groupBy=permitType`**:

```
HVAC                     48,933
Building                 44,904
Plumbing Permits         44,636
Fire Protection Systems  11,186
Excavation/Fill           9,193
Misc. Structures          6,725
Wrecking                  5,741
```

### Notes on the data, from the source

- 🔴 **The contacts layer covers 2013 to 2019 only, and this is the single most
  important thing to know about this actor.** Measured 2026-09-28 by sampling
  250-400 permits per year and joining: contacts cover **2013-2019 at ~100%**
  and **2021 through 2026 at 0%**. The contact table's permit numbers run from
  `2012P00493` to `2019P11609` and nothing beyond, so 2020 is a partial year -
  by permit number it is 0%, and of the 10,032 permits the register dates to
  2020 only 985 (10%) carry a pre-2020 number that could have contacts. So
  `mode=permits`, which includes the company name the register itself carries
  on 89% of rows, is the default and spans the whole register; `mode=contacts`
  is a **2013-2019 depth layer**, not a universal join.
- 🔴 **Costs and sizes are stored as TEXT, so this actor deliberately offers no
  cost or size filter.** `estimatedCost`, `totalSqFt` and `units` are text
  columns in the source, and the obvious workaround is a silent disaster: a
  quoted comparison such as `estimatedCost >= '100000'` is accepted by the API
  and compared as a **string**, where `'200000'` sorts below `'30000'` - it
  would return 148,778 of ~180.6k permits for a $100k floor. Asking for a cost
  filter here raises an error instead. The columns are still returned, as the
  text the source stores.
- 🔴 **Three columns wrap their value in literal double quotes**: the company
  name on 160,614 rows (89%), the PIN on 180,096 (99.8%) and the proposed use
  on all ~180.6k. The quotes are stripped on output, and stripped from your
  search text, so a company search matches whether or not you typed them.
- **The permit number is not unique.** ~180.6k rows carry 162,861 distinct
  permit numbers - one permit is filed once per trade. The row is the unit.
- **The Status column is served twice on purpose, and the mapped form is only
  partly cleaned.** `statusMapped` merges three raw codes into one label -
  `ISSUED`, `ENGRCHNG` and `TEMPCOFO` all read "Permit Issued" - and for sixteen
  other raw codes it repeats the code unchanged (`APP_EXP`, `APRV_NR`,
  `XCLOSED`, `W/REFUND`, ). That is why the raw column carries 24 distinct codes
  and the mapped column 22. No raw code has an empty mapped value; the filter
  matches either form, so a caller can search the code they see or the label.
- **`community_council_neighborhood` is empty on all ~180.6k rows** - a dead
  column in the source. It is not returned and is not in the schema.
- **The contractor on the permit is sometimes the owner.** The company column
  carries the literal `OWNER` on some rows; it is served as the source stores
  it rather than being blanked, because that is the underlying fact.
- **Some coverage is partial**: the completed date is on 80.3% of permits, the
  expires date on 92.0%, the certificate-of-occupancy issued date on 17.5%, the
  square footage on 31.2%, and the estimated cost and units are 0 on 22,298 and
  74,904 rows respectively.
- Invalid filter inputs (a `%` in a search, a ZIP with no digits, a malformed
  date, a cost filter, or a key this actor does not implement) are rejected
  rather than passed to the source.

### Coverage and freshness

Cincinnati building permits, 2010-01-04 to the present, kept current by the
City. The newest permit issue date at the time of writing was 2026-09-25. Every
record carries `sourceUpdatedAt`; the actor also carries count guards on both
tables and a whole-corpus field-coverage guard, so a source that collapses or
changes shape fails loudly instead of returning an empty page as success.

# Actor input Schema

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

permits = the permit register, 2010 to today (default). contacts = the named parties on a permit, which the source carries for permits from 2013 to 2019 only. aggregate = one count row per group (see groupBy).

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

Exact permit number, e.g. '2026P08036'. Blank = any.

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

Substring, matched against the mapped label or the raw code, e.g. 'Building', 'HVAC', 'Plumbing', 'Elevator'. Blank = any.

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

Substring, matched against the mapped label or the raw code, e.g. 'Issued', 'Permit Finaled', 'ROUTE', 'WITHDRWN'. Blank = any.

## `workClass` (type: `string`):

Substring, e.g. 'Existing', 'New', or a raw code such as 'RNEW'.

## `permitClass` (type: `string`):

Exact code: 'RCO' (residential), 'OBC' (commercial) or 'Non-Standard Code Book'. Blank = any.

## `description` (type: `string`):

Work description substring. Blank = any.

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

Site address substring. Blank = any.

## `city` (type: `string`):

City substring. Blank = any.

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

ZIP, matched as a prefix. Blank = any.

## `neighborhood` (type: `string`):

Neighborhood substring, e.g. 'DOWNTOWN', 'HYDE PARK', 'OAKLEY'. Blank = any.

## `companyName` (type: `string`):

The company on the permit (contractor, or 'OWNER'). Substring; typed quotes are accepted and ignored. Blank = any.

## `pin` (type: `string`):

Exact parcel PIN. Blank = any.

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

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

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

Latest application date (YYYY-MM-DD), inclusive. 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.

## `relationship` (type: `string`):

contacts mode only. Role on the permit, matched exactly, e.g. 'CONTRACTOR', 'OWNER', 'BC PLG' (plumbing trade), 'BC CONTR', 'BC HVAC', 'PRMRYCNTCT', 'ADDRESSEE'. Blank = any.

## `contactName` (type: `string`):

contacts mode only. Contact name substring. Blank = any.

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

Maximum records to return in permits/contacts mode (1-10000). Default 50.

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

aggregate mode only: the dimension to count by.

## Actor input object example

```json
{
  "mode": "permits",
  "permitNumber": "",
  "permitType": "",
  "status": "",
  "workClass": "",
  "permitClass": "",
  "description": "",
  "address": "",
  "city": "",
  "zip": "",
  "neighborhood": "",
  "companyName": "",
  "pin": "",
  "appliedFrom": "",
  "appliedTo": "",
  "issuedFrom": "",
  "issuedTo": "",
  "relationship": "",
  "contactName": "",
  "maxResults": 50,
  "groupBy": "permitType"
}
```

# Actor output Schema

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

Cincinnati building permits and permit contacts - 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/cincinnati-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/cincinnati-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/cincinnati-permits --silent --output-dataset

```

## MCP server setup

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