# Building Permit Address Lookup - Open & Expired Permit Check

Check an address in 10 supported US cities for building permits: permit history, open permits, and permits issued but never finaled. One verdict row per address. No homeowner personal data.

Canonical product page: https://mayd-it.com/data-tools/actors/building-permit-address-lookup/
Public Actor: https://apify.com/maydit/building-permit-address-lookup

## Example input

```json
{
  "city": "Austin",
  "lookbackYears": 10
}
```

## Full guide

![Permit records, address by address — illustrative workflow](https://api.apify.com/v2/key-value-stores/YGxD9JRpduom8Q5Zd/records/building-permit-address-lookup-0edecb7a0faa.png?signature=VY0lGR0FaSmCtDU9rgP7)

## Permit records, address by address

Review specific properties without sorting a whole-city permit feed. Each successful address check brings together matching permits, available open/expired indicators and a plain-language summary for further investigation.

[Open this Actor in Console](https://console.apify.com/actors/yhmUbg1s3WK9fSkRj/input) · [First run](#try-this-actor) · [Results](#result-preview) · [Cost](#usage-and-pricing) · [Full guide](#full-guide)

## Try this Actor

1. **Choose a small scope.** Keep **Austin** selected and use the built-in three-address sample already documented below. For another supported city, replace all sample addresses with addresses in that city. Review the lookback window and permit cap.
2. **Run and review.** Open the dataset's address table, then inspect `matchConfidence`, `permitCapReached` and `verdictBasis`. Check the run's key-value-store `SUMMARY` for saved-row counts and stops.
3. **Use the results.** Export CSV for the address summary or JSON for the nested `permits` evidence and source links. Verify fuzzy matches with the municipal records.

**Read this before using the data:** Coverage is ten supported portals, one city per run. Some cities cannot support open or expired verdicts. An expired-but-open record is a flag to investigate, not proof of unpermitted work or a substitute for an official search.

## Result preview

The dataset view highlights these columns:

- **Address** · `queryAddress`
- **City** · `city`
- **State** · `state`
- **Address match confidence** · `matchConfidence`
- **Permits** · `permitCount`
- **Permit fetch cap reached** · `permitCapReached`

Use the original source links and the field definitions below when interpreting the results. CSV works well for flat columns; JSON preserves nested values.

## Usage and pricing

Base result rate on the FREE tier: **$20 per 1,000 results**. The [Pricing tab](https://apify.com/maydit/building-permit-address-lookup/pricing) shows your plan's result rate and any Actor Start charge. Set a small result limit for the first run; the requested limit is not a guarantee of that many results.

---

## Full guide

Check a list of street addresses against a city's building permit records and get back **one verdict row per address**, not a bulk permit dump. For every address this Actor reports the permit history, how many permits are still open, and - the field most people come for - whether any permit was **issued, passed its expiration date, and never finaled**. That condition can warrant a closer look during property research. Check the matching permit and issuing authority: an expired or open record alone does not establish unpermitted work, unfinished work or a transaction defect.

This is built for people checking specific properties: real estate agents and buyers doing pre-offer due diligence, insurance underwriters assessing a risk, title and escrow officers clearing a file, contractors checking whether a prior job at an address was ever closed out, and property managers auditing a portfolio. You submit a list of addresses and get back a verdict for each one, rather than a whole-city permit feed you then have to filter yourself.

It queries official municipal open-data portals (Socrata / SODA), normalizes every city into one schema, and tells you honestly how good each address match was.

---

### What you get

One row per address you submit. Every row carries the verdict fields plus a nested `permits` array with the matching permits.

| Field | Type | Description |
|---|---|---|
| `queryAddress` | string | The address exactly as you submitted it |
| `normalizedAddress` | string | The canonical form used for matching (`4414 BANISTER LN`) |
| `city`, `state` | string | The portal that was queried |
| `matchConfidence` | string | `exact`, `normalized`, `fuzzy`, or `none` - the weakest grade among the permits this row counts; see below |
| `permitCount` | number | Permits matched to this address within the lookback window |
| `permitCapReached` | boolean | **true** when the portal returned `maxPermitsPerAddress` records for this address, so older permits were not fetched and every count in the row is a lower bound. The summary then says "at least N" (or, when none of the fetched records matched, that none matched among the records checked) |
| `earliestPermit`, `latestPermit` | string | Date range of the matched permits (`YYYY-MM-DD`) |
| `openPermitCount` | number / null | Permits not finaled, completed, withdrawn, or cancelled. Permits with neither a status nor a completion date are not counted here - see `statusUnknownCount`. `null` when the portal publishes no status |
| `statusUnknownCount` | number / null | Permits that carry neither a status nor a completion date, so whether they are open cannot be determined (in Chicago: sign, elevator equipment, scaffolding and Easy Permit records). `null` when the portal publishes no status |
| `issuedNeverFinaledCount` | number / null | Open permits that were actually issued (not just applications in review) |
| `expiredButOpen` | boolean / null | **true** when at least one permit is issued, past expiry, and never finaled. `null` when the portal publishes no expiration date |
| `expiredButOpenCount` | number / null | How many permits meet that condition |
| `humanSummary` | string | A plain-English sentence you can paste into a report |
| `verdictBasis` | string | States exactly what evidence the verdict rests on for this city |
| `permits[]` | array | Matched permits: id, type, class, status, description, valuation, applied / issue / expiry / completion dates, lat/lon, source link, per-permit `matchConfidence`, `isOpen` (`null` when unknown) and `expiredButOpen` |
| `portal`, `scrapedAt` | string | Source dataset and run timestamp |

#### How `matchConfidence` works

Addresses in permit systems are typed by hand, so this Actor grades every match instead of pretending they are all equal:

- **`exact`** - the portal's street line is character-identical to yours once case and punctuation are ignored.
- **`normalized`** - identical after expanding abbreviations (`LANE` / `LN`, `WEST` / `W`), writing numbered streets one way (`34th` / `34`, `3rd` / `03rd`, `Fifth` / `5`), and setting aside a unit or building designator. Same property, different spelling.
- **`fuzzy`** - same house number and street name, but one side carries tokens the other does not: a street direction you did not enter (leading, or trailing like the `S` in `Rainier Ave S`), a leading direction you entered that the portal omits, a missing street type, or extra words. **Verify these before relying on them.**
- **`none`** - nothing matched. The row is still returned, with `permitCount: 0`.

A row's `matchConfidence` is the **weakest** grade among the permits it counts, so a row reads `exact` only when every permit in it matched exactly; each permit also carries its own grade.

Street directions and street types have to agree. A direction or street type that conflicts with the one you typed never matches: `500 E Cesar Chavez St` does not pick up `500 W`, `1000 4th Ave S` does not pick up `1000 4th Ave`, and `200 Park Avenue South` does not pick up `200 Park Avenue`. If you leave out a direction (leading or trailing), permits that add one are used only when nothing matches the address exactly as you typed it - `100 Broadway` does not pick up `100 East Broadway`, `1000 4th Ave` does not pick up `1000 4th Ave S`, `200 Park Avenue` does not pick up `200 Park Avenue South`. When they are used (`3820 Rainier Ave` finds the permits at `3820 Rainier Ave S`), they are graded `fuzzy` and the summary names the direction you left out; when they carry more than one direction (`500 E` and `500 W`), the summary says they may be different buildings and asks you to add it.

Numbered streets can be typed the normal way: `151 West 34th Street` and `151 West 34 Street` find the same New York permits.

#### What is deliberately NOT in the output

**No homeowner names. No personal phone numbers. No personal email addresses.** Several of these portals publish the property owner's name right next to the permit, and this Actor strips it. Permit party names are emitted only when the name reads as a business (`Vargas Electric LLC`, `Kind Roofing`); anything that reads as an individual is withheld and flagged with `contractorNameWithheld: true`.

This is the differentiator, not an oversight. Competitors in this category bolt on skip-tracing and contact enrichment, so what starts as a permit check arrives as a lead list attached to somebody's home address. This Actor does not do that. It reads public government records, returns a verdict about the property, and stops there - there is no personal-contact data in the output to strip out later, and nothing here is sourced from a private marketplace or directory. If you need homeowner contact data, this is not the tool, and that is on purpose.

---

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `addresses` | array | 3 sample Austin addresses | Street addresses to check, one per line. House number required; unit, city, state and ZIP are optional and ignored when matching |
| `city` | select | `austin` | Which municipal portal to query. **One city per run** - addresses from other cities will not match |
| `lookbackYears` | integer | `10` | How far back to search from today (1-60) |
| `maxPermitsPerAddress` | integer | `100` | Cap on permits fetched per address before matching, newest first. A row that hits it says so with `permitCapReached: true` |
| `maxRunSeconds` | integer | `240` | Stop cleanly at this point and keep what has been written |
| `socrataAppToken` | string | empty | Optional Socrata token to raise rate limits on long lists. Not required |

#### Supported cities and what each one can actually tell you

Not every portal publishes the data an open/expired verdict needs. Rather than guess, this Actor returns `null` for verdicts a city cannot support and says so in `verdictBasis` and `humanSummary`.

| City | Open verdict | Expired-but-open verdict |
|---|---|---|
| Austin, TX | yes | yes |
| Seattle, WA | yes | yes |
| Cincinnati, OH | yes | yes |
| New York, NY | yes | yes |
| Chicago, IL | yes, except permit types published with no status (signs, elevator equipment, scaffolding, Easy Permit), which are counted in `statusUnknownCount` | no - portal publishes no expiration date |
| San Francisco, CA | yes | no - portal publishes no expiration date |
| Los Angeles, CA | yes | no - portal publishes no expiration date |
| Mesa, AZ | yes | no - portal publishes no expiration date |
| Baton Rouge, LA | no - portal publishes no permit status | no |
| Marin County, CA | no - portal publishes no permit status | no |

---

### Example output

A real result for `2603 Ware Rd` in Austin, copied verbatim and unedited from a run of this Actor. The permits are shown in full, in the order the Actor emits them (most recent first).

```json
{
  "queryAddress": "2603 Ware Rd",
  "normalizedAddress": "2603 WARE RD",
  "city": "Austin",
  "state": "TX",
  "matchConfidence": "exact",
  "permitCount": 2,
  "permitCapReached": false,
  "earliestPermit": "2024-01-17",
  "latestPermit": "2024-02-23",
  "openPermitCount": 1,
  "statusUnknownCount": 0,
  "issuedNeverFinaledCount": 1,
  "expiredButOpen": true,
  "expiredButOpenCount": 1,
  "humanSummary": "2 permits on record between 2024-01-17 and 2024-02-23. 1 still open (not finaled or closed out). 1 of those is past the permit expiry date with no final inspection on record - worth investigating before closing or binding coverage.",
  "verdictBasis": "open/closed derived from permit status and completion date; expiry derived from the published permit expiration date",
  "lookbackYears": 10,
  "permits": [
    {
      "permitId": "2024-004889 MP",
      "permitType": "Mechanical Permit",
      "permitClass": "Residential",
      "status": "Final",
      "workDescription": "REPLACEMENT OF LONESTAR UPFLOW NATURAL GAS 3.5 TON 15.20 SEER CONDENSOR AND 2 FLEX DUCT OUTLETS",
      "sourceAddress": "2603 WARE RD",
      "zip": "78741",
      "valuation": null,
      "contractorName": null,
      "appliedDate": "2024-01-15T00:00:00.000",
      "issueDate": "2024-02-23T00:00:00.000",
      "expiryDate": "2024-03-01T00:00:00.000",
      "completedDate": "2024-03-01T00:00:00.000",
      "latitude": 30.22428863,
      "longitude": -97.73227205,
      "sourceUrl": "https://abc.austintexas.gov/web/permit/public-search-other?t_detail=1&t_selected_folderrsn=13265815",
      "contractorNameWithheld": false,
      "matchConfidence": "exact",
      "isOpen": false,
      "expiredButOpen": false
    },
    {
      "permitId": "2024-004892 EP",
      "permitType": "Electrical Permit",
      "permitClass": "Residential",
      "status": "Active",
      "workDescription": "Installing an outdoor meter loop. Disconnect/Reconnect",
      "sourceAddress": "2603 WARE RD",
      "zip": "78741",
      "valuation": null,
      "contractorName": "JON WAYNE ELECTRICAL",
      "appliedDate": "2024-01-15T00:00:00.000",
      "issueDate": "2024-01-17T00:00:00.000",
      "expiryDate": "2024-08-10T00:00:00.000",
      "completedDate": null,
      "latitude": 30.22428863,
      "longitude": -97.73227205,
      "sourceUrl": "https://abc.austintexas.gov/web/permit/public-search-other?t_detail=1&t_selected_folderrsn=13265818",
      "contractorNameWithheld": false,
      "matchConfidence": "exact",
      "isOpen": true,
      "expiredButOpen": true
    }
  ],
  "portal": "https://data.austintexas.gov/resource/3syk-w9eu",
  "scrapedAt": "2026-09-24T03:28:07.832Z"
}
```

The second permit above - the electrical one - was issued on 2024-01-17, expired on 2024-08-10, and carries no completion date. That is what `expiredButOpen: true` means. The mechanical permit listed first was finaled on 2024-03-01, so it counts toward `permitCount` and nothing else.

---

### FAQ

**What exactly counts as an "open" permit?**
A permit with no completion or final-inspection date whose status is not a closed-out one (finaled, completed, closed, signed-off, withdrawn, cancelled, void, revoked, rejected, denied, or a certificate of completion or occupancy issued). A status of **Expired** is deliberately counted as open, because an expired permit is precisely one that was never finaled. A status that names a finished step before the permit itself is finished - Seattle's "Application Completed" and "Reviews Completed", Los Angeles's "CofO in Progress" - counts as open. Permits still in review with no issue date count as open too, and are excluded from `issuedNeverFinaledCount` so you can tell the two apart. A permit that carries neither a status nor a completion date is counted as neither open nor closed: its `isOpen` is `null` and it goes into `statusUnknownCount`.

**Does `expiredButOpen: true` prove there is unpermitted work?**
No, and the Actor does not claim it does. It means the city's own published record shows a permit that was issued, has passed its expiration date, and carries no final inspection or completion date. Records also lag reality - a permit may have been finaled without the portal being updated. Treat it as a flag to investigate with the building department, not as a finding.

**Why is one address `fuzzy` and another `exact`?**
Because permit clerks type addresses by hand. `4414 BANISTER LN BLDG 1` and `4414 Banister Ln` are the same parcel but not the same string, so that match is graded `normalized`. A match missing a directional (`1847 Newcastle Ave` against `1847 N NEWCASTLE AVE`) is graded `fuzzy`. A conflicting direction or street type (`500 E` against `500 W`) is not a match at all. The grade is in every row and on every permit so you can decide how much to trust it.

**Can I check addresses in several cities in one run?**
No. Each run queries one city portal, because the address filter is built for that portal's specific schema. Run the Actor once per city; results append to the same dataset if you want them together.

**Why are contractor names sometimes blank?**
Either the portal publishes no contractor for that permit (San Francisco and Los Angeles publish none at all on these datasets), or the name looked like a person rather than a business and was withheld on purpose. `contractorNameWithheld: true` distinguishes the second case from the first.

**What happens if the run is too slow for a long address list?**
It stops cleanly at `maxRunSeconds`, keeps every row already written, and logs how many addresses were left. Raise `maxRunSeconds` or split the list.

---

### Data source, access, and limits

**Permit data** comes from official municipal open-data portals running Socrata (SODA), one dataset per city, listed in the table above. These are public government APIs that need no login and no API key. The optional `socrataAppToken` only raises rate limits on long runs; everything works without it.

**That is the only source.** Every field in the output traces back to a government open-data record, so there is no third-party site in the path that can rate-limit the run, block it, or change its terms underneath you. Nothing is read from a private marketplace, contractor directory, or lead broker.

**Known limits.** Coverage is the ten cities listed, not nationwide. Verdict quality varies by city exactly as the capability table states. Open-data portals refresh on their own schedules (typically daily to weekly), so very recent permits may not appear yet, and a permit finaled last week may still read as open. Addresses without a house number cannot be matched and are reported as errors. Large commercial parcels can exceed the default 100-permit cap - the row then says `permitCapReached: true` and "at least N"; raise `maxPermitsPerAddress` for those. New York is matched on house number and street name across all five boroughs, so the same address in two boroughs is not told apart.

**Billing.** This Actor is billed per result on the Apify platform: one result is one address verdict row, regardless of how many permits that address turns out to have. Addresses that fail to look up are not emitted and therefore are not charged. Check the pricing shown on the Actor's Apify Store page for the current rate.

**Legal.** Permit records are public government data. Nothing here is a substitute for an official records search, a title report, or a municipal building department inquiry, and none of it is legal, insurance, or investment advice.

#### Empty input and spending limits

An empty input runs the three-address Austin sample. If you select another city, supply addresses in that city; missing addresses are rejected instead of using the Austin sample. The run stops when its spending limit is reached, and SUMMARY counts only rows saved in the dataset.

Marin County identity: when the published permit number is blank, `permitId` uses the portal's published `unique_id` or `permit_tracking_id`. This identifies the source record; it does not assert that an unnumbered application has been issued.

## Help with your run

Open the **Issues** tab on this Actor with the run URL, the input field that caused trouble and what you expected to receive. Remove API keys and private information before posting.

[Explore more Mayd-It Actors](https://apify.com/maydit)

## Understand your run

Open **Run report** in the run’s Output tab for saved-record counts, reported source issues and limits, and suggested next steps. Use **CSV export** to take the saved results into a spreadsheet. The existing JSON results and detailed machine-readable summary remain available.

The report reflects this run’s recorded coverage. A successful run or a saved result does not establish that every possible source record was collected.
