# Los Angeles Building Permits Scraper (`usta/los-angeles-building-permits-scraper`) Actor

Building permits issued in the City of Los Angeles, read live from the LADBS open-data feed. Filter by date window, permit type, ZIP or minimum valuation. One row per permit, with the record link.

- **URL**: https://apify.com/usta/los-angeles-building-permits-scraper.md
- **Developed by:** [US Tech Automations](https://apify.com/usta) (community)
- **Categories:** Real estate, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.00 / 1,000 result rows

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## Los Angeles Solar & ADU Permit Leads

Solar and ADU installer sales reps get one row per Los Angeles building permit flagged for solar, EV charging or an ADU, filtered and ready to call.

### Introduction

Pick how far back to look. Get back one row per building permit the City of
Los Angeles issued in that window, with a link to the city's own public record
on every single row.

This covers the City of Los Angeles only. Not Los Angeles County, not Long
Beach, Santa Monica, Pasadena, Burbank or Glendale. Those cities run their own
permit counters and are not in this feed.

Each run reads the Los Angeles Department of Building and Safety open-data feed
live, at the moment the run starts. Nothing is served from a stored copy. The
source is dataset `pi9x-tg5x` on `data.lacity.org`, "Building and Safety -
Building Permits Issued from 2020 to Present". On 2026-09-14 that dataset held
412,133 permits, issued between 2020-01-01 and 2026-09-12.

What this Actor adds on top of the free portal is a rolling date window instead
of hand-written dates, permit type, ZIP and minimum-valuation filters in one
place, numbers parsed as numbers rather than text, a row cap that also caps
what a run can cost, and a record link on every row so any figure can be
checked at the source.

Every row carries these fields:

| Field | What it holds |
| --- | --- |
| `permit_number` | The city's own permit number |
| `permit_type` | The city's permit type wording, unchanged |
| `permit_subtype` | Building use class, such as `1 or 2 Family Dwelling` |
| `use_description` | What the building is used for, such as `Apartment` |
| `status` | The city's status wording, usually `Issued` |
| `issue_date` | Date the permit was issued, as YYYY-MM-DD |
| `submitted_date` | Date the application was filed, where published |
| `address` | Street address as the city published it |
| `zip` | 5-digit postal code |
| `council_district` | Los Angeles council district number |
| `valuation_usd` | Declared job value in US dollars, as a number |
| `square_feet` | Floor area, where the city publishes one |
| `construction_type` | Building code construction type, such as `Type V-B Construction` |
| `height_feet` | Building height in feet, where published |
| `dwelling_units_changed` | Homes added or removed, negative when removed |
| `adu_changed` | Accessory dwelling units added or removed |
| `junior_adu_changed` | Junior accessory dwelling units added or removed |
| `ev_charging` | True when the permit is flagged for electric-vehicle charging |
| `solar` | True when the permit is flagged as solar |
| `issuing_office` | How the permit was issued, such as `Express Permit` |
| `work_description` | The city's description of the work |
| `latitude`, `longitude` | Point location, where the city publishes one |
| `attribution` | The credit line the City of Los Angeles asks for |
| `source_dataset` | The portal and dataset the row was read from |
| `source_url` | Link to that exact record on the city's public API |
| `fetched_at` | UTC time the row was read, as an ISO timestamp |

A field the city does not publish arrives as `null`. It is never guessed and
never filled in from somewhere else.

### Tutorial

1. Set **Days back**, from 1 to 90. The window runs from that day through
   today, and both ends are included.
2. Optionally pick a **Permit type** from the dropdown. Leave it on
   "Every permit type" to keep them all.
3. Optionally type a 5-digit **ZIP code**, such as `90066`. Leave it empty for
   the whole city.
4. Optionally set a **Minimum valuation** in dollars, to skip small jobs.
5. Set **Maximum rows**. This is a hard ceiling on rows returned, so it is also
   a hard ceiling on what the run can cost. It starts at 100.
6. Start the run.
7. Open **Permits** to browse rows, or **Download permits as CSV** to take them
   away. **Run status** holds the row count, the oldest and newest issue date
   actually returned, the source, the credit line, and any warnings.

To sweep a long period without one enormous run, run week by week and raise
**Maximum rows** only as far as needed.

### Pricing

**$0.003 per row, charged from the very first row. There is no start fee.**

Billing is per result. One row pushed is one charged result. A run returning 40
rows is charged for 40 rows, which is $0.12. A run returning 100 rows is
charged $0.30. A run that returns no rows costs nothing, because nothing was
charged for.

Every row counts. There is no allowance of uncharged rows at the start of a
run, so budget on the full count, not on a discount.

A run that is refused for bad input, and a run that cannot reach the Los
Angeles portal, both push zero rows and so cost nothing.

**Maximum rows** is the spend control. At the default of 100 rows, a run cannot
cost more than $0.30 no matter how many permits the city issued.

### Input and output examples

Input:

```json
{
  "daysBack": 7,
  "permitType": "Bldg-New",
  "zip": "",
  "minValuation": 50000,
  "maxItems": 100
}
```

One output row, exactly as pushed:

```json
{
  "permit_number": "26010-20000-03188",
  "permit_type": "Bldg-New",
  "permit_subtype": "1 or 2 Family Dwelling",
  "use_description": "Accessory Dwelling Unit",
  "status": "Issued",
  "issue_date": "2026-09-12",
  "submitted_date": "2026-07-13",
  "address": "13460 W BURBANK BLVD",
  "zip": "91352",
  "council_district": "6",
  "valuation_usd": 220000.0,
  "square_feet": 1200.0,
  "construction_type": "Type V-B Construction",
  "height_feet": 14.5,
  "dwelling_units_changed": null,
  "adu_changed": 1,
  "junior_adu_changed": null,
  "ev_charging": false,
  "solar": false,
  "issuing_office": "Express Permit",
  "work_description": "NEW DETACHED ADU",
  "latitude": 34.2265,
  "longitude": -118.3891,
  "attribution": "Published by the City of Los Angeles Department of Building and Safety on data.lacity.org, dataset pi9x-tg5x. Use is subject to the data.lacity.org Terms of Use, https://data.lacity.org/terms-of-use.",
  "source_dataset": "data.lacity.org/resource/pi9x-tg5x",
  "source_url": "https://data.lacity.org/resource/pi9x-tg5x.json?permit_nbr=26010-20000-03188",
  "fetched_at": "2026-09-15T02:53:28Z"
}
```

`sample-output.json` in the package holds three more rows taken from a real
run.

The run also writes a short status record:

```json
{
  "status": "OK",
  "rows": 3,
  "days_back": 14,
  "permit_type": "Bldg-New",
  "zip": null,
  "min_valuation": 50000,
  "oldest_issue_date": "2026-09-12",
  "newest_issue_date": "2026-09-12",
  "source": "data.lacity.org/resource/pi9x-tg5x",
  "terms": "https://data.lacity.org/terms-of-use",
  "warnings": []
}
```

`status` is `OK` when the portal answered, `INVALID_INPUT` when the request
could not be honoured, and `UNKNOWN` when the portal did not answer. The last
two push zero rows.

### Recommendations

**Finding new construction leads.** Set **Permit type** to `Bldg-New` and a
**Minimum valuation** that matches the job size worth chasing. Run it daily
with **Days back** at 3, so a day the city publishes late is still picked up.

**Finding accessory dwelling unit work.** Run with no permit type filter and
sort the rows on `adu_changed` and `junior_adu_changed`. A positive number
means the permit adds that many units. `use_description` of
`Accessory Dwelling Unit` is the other strong signal.

**Watching one neighbourhood.** Set **ZIP code** and leave the other filters
empty. One ZIP over 30 days is usually a few hundred rows.

**Keeping the cost flat.** Leave **Maximum rows** low and run more often. A
daily run at 100 rows costs at most $0.30 a day.

**Checking a figure.** Open the `source_url` on any row. It returns that one
permit from the city's own API, so no number here has to be taken on trust.

### FAQ

**Is the contractor's name included?** No. The public dataset behind this Actor
publishes no contractor, applicant, owner or architect name. There is no such
column in it. The older Los Angeles permit dataset that did carry a
contractor's business name, `yv23-pmwf`, now requires a portal login and
answers HTTP 403 to the public, so it is not used here. Nothing in the output
is a guessed name.

**How fresh is the data?** The city usually publishes a day's permits one to
three days later. On 2026-09-14 the newest issue date in the dataset was
2026-09-12. A **Days back** of 1 or 2 can therefore come back empty. Use 3 or
more for a daily run.

**How far back can it go?** Ninety days is the limit on **Days back**. The
dataset itself starts at 2020-01-01, but this Actor is built for the recent
window, not for bulk history.

**Why is a run empty?** Either the filters matched nothing, or the window is
newer than the city's publishing lag. An empty run reports `OK` with a note
saying so, and costs nothing.

**Does it cover Los Angeles County?** No. City of Los Angeles permits only.

**What happens if the city portal is down?** The run reports `UNKNOWN` with the
exact error text and pushes zero rows. No cached or invented row is ever put in
its place.

**Can the same permit appear twice in one run?** No. Rows are de-duplicated on
permit number and issue date before anything is pushed.

### Limitations

Read this part before buying.

- **No contractor, applicant or owner names.** The public feed does not publish
  them, so they are not here. If a name on the permit is the thing needed, this
  Actor will not supply it.
- **City of Los Angeles only.** Not the county, and not the surrounding
  independent cities.
- **A publishing lag of one to three days.** A permit issued this morning is
  usually not readable today.
- **The city's own gaps carry through.** `square_feet`, `height_feet`,
  `dwelling_units_changed` and the coordinates are blank on many permits
  because the city published them blank. They are left as `null` rather than
  estimated.
- **`valuation_usd` is the value declared on the application.** It is not an
  appraisal and not a contract price.
- **Setting a minimum valuation drops permits with no declared value**, because
  a blank cannot be shown to clear a floor. The run says so in its warnings.
- **Ninety days back is the hard limit**, and 50,000 rows is the hard row cap.
- **No person is on call for this Actor.** There is no staffed support desk
  behind it.

### Source and credit

Data comes from the City of Los Angeles open-data portal, dataset `pi9x-tg5x`,
published by the Department of Building and Safety. Use of that portal is
subject to its Terms of Use at `https://data.lacity.org/terms-of-use`, which
apply to "application developers who obtain data through this web portal". That
credit line is copied onto every row in the `attribution` field so it travels
with the data.

### Refunds and support

Refunds: if a run returns zero rows for a valid input, email operations@ustechautomations.com within 7 days and we refund that run. Support: same address, replies within 2 business days.

# Actor input Schema

## `daysBack` (type: `integer`):

How many days back from today to look, 1 to 90. The window runs from that day through today, and both ends are included. The city usually adds a day's permits one to three days later, so a window of 1 or 2 days can come back empty.

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

Keep only one permit type. Leave it empty for every type.

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

Keep only permits in one 5-digit Los Angeles ZIP code, such as 90066. Leave it empty for the whole city.

## `minValuation` (type: `integer`):

Keep only permits whose declared job value is at least this much. Leave it empty to keep every permit. Permits the city published with no declared value are left out whenever this is set.

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

Hard ceiling on rows returned, and therefore on what the run can cost. Every row returned is charged, from the first one.

## Actor input object example

```json
{
  "daysBack": 7,
  "permitType": "",
  "zip": "",
  "maxItems": 100
}
```

# Actor output Schema

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

No description

## `csv` (type: `string`):

No description

## `report` (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 = {
    "daysBack": 7,
    "permitType": "",
    "zip": "",
    "maxItems": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("usta/los-angeles-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 = {
    "daysBack": 7,
    "permitType": "",
    "zip": "",
    "maxItems": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("usta/los-angeles-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 '{
  "daysBack": 7,
  "permitType": "",
  "zip": "",
  "maxItems": 100
}' |
apify call usta/los-angeles-building-permits-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,usta/los-angeles-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/wVHH6Onlq8e885Xfi/builds/UOWxsFlxbhl1uXCQ0/openapi.json
