# NYC Building Permit Feed, unofficial (`amplelabs/nyc-building-permit-feed`) Actor

Unofficial NYC building permit feed from NYC Open Data. Filter by borough, ZIP, work type and cost. Scheduled runs send permits published since the last run.

- **URL**: https://apify.com/amplelabs/nyc-building-permit-feed.md
- **Developed by:** [Ample Labs](https://apify.com/amplelabs) (community)
- **Categories:** Real estate, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 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.

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

## NYC Building Permit Feed, unofficial

This Actor is an independent product of Ample Labs. It is not affiliated with, endorsed by, or sponsored by the City of New York, NYC Open Data, the NYC Department of Buildings, or Tyler Technologies, Inc. (Socrata). Data is provided as is, without warranty of any kind, and may be incomplete or out of date. No official logos or seals are used.

Source: NYC Department of Buildings (DOB) via NYC Open Data, datasets [`rbx6-tga4`](https://data.cityofnewyork.us/d/rbx6-tga4) (DOB NOW: Build – Approved Permits) and [`ipu4-2q9a`](https://data.cityofnewyork.us/d/ipu4-2q9a) (DOB Permit Issuance). Version: each item records when the run fetched it (`retrieved_at`) and when the source was last updated (`source_rows_updated_at`, `source_row_updated_at`). Modifications: Ample Labs keeps allowlisted columns only, renames and normalizes core fields, drops rows by your filters, the age cutoff and the exclusion list, and leaves free text out unless you turn on `includeFreeText`. The full sentence is on every item as `modifications` and `source_attribution`.

### What you get

Building permits that NYC has just published, as one dataset row per permit. Schedule the Actor and each run sends the permits published since your last run. Permits you already received under the same `stateKey` are not sent again, apart from a free copy after a crashed run (see Output in the full README).

Use it to watch new permits for work such as general construction, plumbing, foundations, mechanical systems and sidewalk sheds in the boroughs or ZIP codes you cover.

### Inputs

- `datasets`: `nyc_dob_now`, `nyc_bis`, or both (default both).
- `boroughs`: any of MANHATTAN, BRONX, BROOKLYN, QUEENS, STATEN ISLAND. Empty means all.
- `zipCodes`: five-digit ZIP codes. Empty means all.
- `permitTypeContains`: text matched against permit type or work type.
- `minEstimatedCost`: minimum estimated cost in dollars. BIS rows have no cost and are dropped unless `includeUnknownCost` is on.
- `maxPermitAgeDays`: default 90. Older permits are skipped.
- `maxResults`: default 5,000 permits per run.

### Output

One item per permit, with permit number, work types, status, issue, filing and expiration dates, estimated cost, address, borough, ZIP, BIN, BBL and coordinates. The field list is in DATA_DICTIONARY.md.

Example item (DOB NOW, from a test run on 2026-10-31; trimmed):

```
permit_number: B00554180-I1-SH
work_types: ["Sidewalk Shed"]
filing_reason: Renewal Permit with Changes
status: Permit Issued
issued_date: 2026-10-29
expiration_date: 2027-01-27
estimated_cost_usd: 5000
house_number: 1301
street: SURF AVENUE
borough: BROOKLYN
zip_code: 11224
bin: 3189607
bbl: 3070640016
```

### Runs with no new permits

If a run delivers no permits, the dataset holds one status row with `eventType` "no-new-permits" instead of permit records. This happens when nothing new that matches your settings was published since the last run. It also happens when a run stops before sending any permit: at the spending limit, near the run timeout, when NYC Open Data blocks access, or when every dataset is unavailable. `stopReason` says which. A run that ends in an unhandled error writes no row.

The row is free. It is not a permit and it is not charged. It does not move your saved position. To keep only permits, drop rows where `eventType` is "no-new-permits".

Example status row (illustrative values, not from a live run):

```
{
  "eventType": "no-new-permits",
  "datasets": ["nyc_dob_now"],
  "daysScanned": [],
  "watermarks": { "nyc_dob_now": "2026-10-30T18:43:14.100Z" },
  "stopReason": "completed",
  "runAt": "2026-10-31T23:00:00.000Z"
}
```

### Privacy defaults

Free-text fields are opt-in and off by default (`includeFreeText` false). Business-name fields are opt-in and off by default (`includeBusinessNames` false). Owner names, applicant names, phone numbers and license numbers are never requested. Street address is included by default because it is part of the public permit record.

Outreach rules: if you use this data for commercial email, US CAN-SPAM requires you to identify yourself as the sender, include a valid physical postal address and an opt-out, and honour opt-outs within 10 business days. Under UK PECR, unsolicited marketing email to sole traders and partnerships (individual subscribers) needs their consent or a soft opt-in; corporate subscribers must be told who you are and given an opt-out. Do not use this data for unsolicited marketing to individuals.

The 10-business-day opt-out is 15 U.S.C. 7704(a)(4).

Some businesses in this data are sole traders, so a business name or address can identify a person. Addresses are included by default because they are part of the public permit record; free-text fields are off by default. Do not use this data to contact or profile individuals.

### Pricing

Prices are per event:

- $0.01 per run start (`apify-actor-start`). Apify charges one start event per GB of run memory, minimum one. The default is 256 MB and the maximum is 512 MB, so a run pays one start event.
- $0.003 per permit delivered (`permit-record`), charged after the permit is written to your dataset. A permit with several work types is charged once. Free updates, crash replays and the status row cost nothing.
- $0.003 per extra day scanned (`day-scanned`): each backfill day after the first, or each catch-up batch after the first batch of a dataset in one run. A daily schedule avoids most of these.
- $0.003 per `bulk-scan` block: each full 2,500 rows downloaded beyond the first 7,500 in a run. Reload batches and days already charged as `day-scanned` do not count.

Rows dropped by your filters are not charged. Example (estimate): a daily run on one new batch that delivers 50 permits costs $0.01 + 50 × $0.003 = $0.16.

### Support

Report problems in the Issues tab on this Actor's page.

Questions and takedown requests: support@haveample.com

### 1. What it does

The Actor reads two public datasets:

| Input id | Source | NYC Open Data API |
|---|---|---|
| `nyc_dob_now` | NYC DOB NOW approved permits | `https://data.cityofnewyork.us/resource/rbx6-tga4.json` |
| `nyc_bis` | NYC legacy BIS permit issuance | `https://data.cityofnewyork.us/resource/ipu4-2q9a.json` |

There is no app token. Requests are unauthenticated and stay at or under one per second, one at a time.

NYC is incremental. A run snapshots the newest `:created_at`, lists batches after the saved watermark, and commits that watermark after each finished batch. A batch that arrives after the snapshot waits for the next run. DOB NOW batches land at about 18:41–18:56 UTC, so a schedule at **19:15 UTC** or later gets the same day's NYC data.

### 2. Source and attribution

NYC Administrative Code § 23-502(d), from https://codelibrary.amlegal.com/codes/newyorkcity/latest/NYCadmin/0-0-0-204577:

> Such public data sets shall be made available without any restrictions on their use provided that the department may require a third party providing to the public any public data set, or application utilizing such data set, to explicitly identify the source and version of the public data set, and a description of any modifications made to such public data set.

Each item carries that identification.

| | DOB NOW | BIS |
|---|---|---|
| Dataset name | DOB NOW: Build – Approved Permits | DOB Permit Issuance |
| Dataset id | `rbx6-tga4` | `ipu4-2q9a` |
| NYC Open Data URL | https://data.cityofnewyork.us/Housing-Development/DOB-NOW-Build-Approved-Permits/rbx6-tga4 | https://data.cityofnewyork.us/Housing-Development/DOB-Permit-Issuance/ipu4-2q9a |
| Item fields | `source_name`, `dataset`, `source_url` | same |

Version on each item:

- `retrieved_at` is when this run fetched the row.
- `source_rows_updated_at` is `max(:updated_at)` from `https://data.cityofnewyork.us/resource/<id>.json`. That is the newest source-row update time the API returns. If that request fails, the field is null and the run continues, unless HTTP 403 has already happened on the previous request.
- `source_row_updated_at` is the row's `:updated_at` when the row has it. Otherwise null.

Modifications, copied onto every item as `modifications` and repeated in `source_attribution`:

Allowlisted columns only. Core fields are renamed and normalized. Rows are dropped by the issue-date, borough, ZIP, and permit-type filters, by the age cutoff, by the cost filter, and by the exclusion list. Free text is off unless includeFreeText is true, and when it is on, email addresses and phone numbers are redacted.

`source_attribution` is one sentence with the dataset name, id, URL, retrieval time, update times, and those modifications.

This is not a contact list. Do not use it to call, text, or email anyone. A permit is a public filing, not permission to contact an owner, applicant, or contractor.

### 3. Inputs

| Input | Default | What it does |
|---|---|---|
| datasets | both | Which datasets to read. At least one of `nyc_dob_now` or `nyc_bis`. An unknown value is rejected with HTTP 400 only by Apify's input-schema check when a run is started through the API. The Console blocks it in the form. Anywhere else the run fails with an `unknown dataset` error. |
| dateFrom | unset | Explicit first-run or `resetState` issue date. Ignored once a watermark exists, unless you also set `dateTo`. A default first run leaves this empty. See below. |
| dateTo | unset | One-off window. The saved watermark is not moved. |
| maxBackfillDays | 1 | How far an explicit `dateFrom` / `dateTo` range may look back, counting from `dateTo`. It does not extend the default first-run lookback. |
| boroughs | empty | Empty means all. Compared with `upper(borough)`. |
| zipCodes | empty | Empty means all. |
| permitTypeContains | empty | Case-insensitive substring on permit type or work type. A DOB NOW permit matches if any of its work types match, and the item still includes the other work types. |
| minEstimatedCost | unset | Dollars. DOB NOW is filtered after download because the cost column is text. BIS has no cost; those rows are dropped unless `includeUnknownCost` is true. |
| includeUnknownCost | false | Keep rows with no parseable cost when a minimum is set. |
| includeFreeText | false | Add the DOB NOW description. Off unless you opt in. See Privacy. |
| includeBusinessNames | false | Add business-name columns inside `source`. |
| maxResults | 5000 | Stop after this many charged permits in the run. The status says to run again. State is already saved. |
| maxPermitAgeDays | 90 | Skip permits issued more than this many days before the run. They are not charged. The watermark still moves past a batch of old permits. |
| stateKey | `default` | Names the saved cursor. Use one key per schedule or filter setup. 1–28 characters: lowercase letters, digits, and hyphens. |
| resetState | false | Forget the saved cursor and seen keys for this stateKey. |

The Console form prefills `datasets` to `nyc_dob_now` and `maxResults` to 10. Those prefills are what the Apify Store check sends. They are not the defaults. A call that omits a field still uses the default in the table: both datasets and 5,000 permits. `resetState` is not prefilled, so it stays off. `stateKey` omitted or set to `default` uses the store `multi-city-permit-feed-state-<stateKey>`. A saved watermark makes the next run incremental, so it does not re-scan older issue dates and does not charge those permits again. DOB NOW publishes on weekdays. A repeat incremental run on a weekend can therefore add no permits. That run pushes the free `no-new-permits` status row, so the dataset is not empty.

Street address is included by default. Free text stays off unless `includeFreeText` is true.

A default first run has no `dateFrom` and no saved cursor. It scans issue dates from today minus 2 days through the last published issue date, in the city's local calendar. That span is at most three days. On Saturday that window includes Thursday and Friday when those dates are published. On Sunday it includes Friday. On Monday, if today minus 2 days is after the last published date, the window is that last published weekday. Late permits that sit in the newest batch but were issued before the window are not delivered on the first run. The watermark saved at the end is the `max(:created_at)` snapshotted when the run started, and it is written only after that window finishes. If the run stops early because of the timeout, the permit cap, or the budget, the next run resumes the same dates and does not move the window. The watermark is set only when the range completes. When `maxBackfillDays` is greater than 1 and shorter than the span, the most recent `maxBackfillDays` are kept. The default of 1 does not shrink this lookback, and a larger value does not extend it past today minus 2 days. An explicit `dateFrom` would be a fixed calendar date, so the prefill does not set one. `maxBackfillDays` is not prefilled, because it does not extend this window.

Dates are the calendar dates printed by the city. They are not converted between time zones. `date(W)` for a reload is computed in America/New_York.

Server-side filters: issue dates, boroughs, ZIP codes, and `permitTypeContains` (both hyphen and en-dash forms). Client-side: DOB NOW cost, BIS cost (always unknown), BIS age, and the dash-normalization check.

Rows that fail a filter are not charged.

### 4. Output

The field list is in [DATA_DICTIONARY.md](DATA_DICTIONARY.md).

One item is one permit. A DOB NOW permit that was filed under several work types is one item: `work_types` is the sorted list, and it is one permit result. `source` holds the allowlisted columns for that dataset. Excluded columns are never requested. Free-text columns are requested only when `includeFreeText` is true, and business-name columns only when `includeBusinessNames` is true.

`record_version` is 1 the first time a permit is delivered. If NYC later republishes that permit with more work types and the earlier row is still in the dataset, the next copy is a free update: `record_version` 2 and `changed_fields`. That has not been seen in the historical extract. It is a guard. A permit already in the delivered set is not sent again when a later batch republishes it and the earlier row is gone, including when only the expiration date changed.

`redelivered: true` means the previous run crashed after saving the key and before the push was acknowledged. The copy is free. If the crash was after a successful push, you can see the same permit twice. The second copy is free. A record is never charged twice.

An unhandled error writes no status row. A run that ends for `failed-graceful`, `access-blocked`, `spending-limit`, or `timeout`, or that completes with no new permits, writes one status row after state is saved. `eventType` `no-new-permits` means no permits were delivered in this run, and `stopReason` gives the reason. The row names the datasets checked (`datasets`), the saved watermark after the run for each of them (`watermarks`, or null when that dataset has no watermark), the issue dates finished in this run (`daysScanned`, possibly empty), and when the row was written (`runAt`). It is not a permit and it is not a `permit-record` charge. It does not move the watermark and it is not added to the delivered set. It has no personal data and no text from the source. Filter it out by dropping rows whose `eventType` is `no-new-permits`. A run that delivered at least one permit does not push this row. A free update or a redelivery counts as a delivery, so that run does not push it either.

```json
{
  "eventType": "no-new-permits",
  "datasets": ["nyc_dob_now"],
  "daysScanned": [],
  "watermarks": { "nyc_dob_now": "2026-10-30T18:43:14.100Z" },
  "stopReason": "completed",
  "runAt": "2026-10-31T23:00:00.000Z"
}
```

### 5. Incremental delivery

State is stored in a named key-value store, `multi-city-permit-feed-state-<stateKey>`, under the record `STATE`. `stateKey` defaults to `default`. Give each schedule, and each different filter setup, its own key. The run's default key-value store is empty on the next run, so the cursor is not kept there. Per-run diagnostics are written to the default store as `OUTPUT`.

Changing filters or `includeFreeText` on an existing stateKey does not go back through days that were already charged. Those days stay done, including a day that was free. Use a new stateKey when a filter change should deliver permits the old key already counted. `resetState` forgets that key's cursor; turn it off again after the run.

State remembers the last fully committed `:created_at` (`W`), the keys already pushed in an unfinished batch, a short `recent` set used when a dataset is reloaded, which catch-up days were already charged, and up to 200 keys lost to a crash.

For each new batch:

- Batches of 3,780 rows or fewer are paged.
- Larger batches are checked. A batch is a reload when it is at least half of the rows published up to that timestamp, or when at least half of a 200-row sample already existed under an earlier `:created_at`. The size, reappearance, and prior-delivery queries are not bounded by `W`.
- A normal batch is paged by the city's own key. The last key on a full page is held until the next page, so a permit is not split across a push. Pushes are at most 25 permits. Keys are saved before the push, then charged.
- A reload delivers only permits issued on or after the New York calendar date of your previous watermark, minus one day, that were not already delivered. They are charged once. The rest of that batch is not sent and is not charged as `day-scanned` or `bulk-scan`. If NYC re-publishes a whole dataset, we deliver only permits issued since your last run (from the day before), once. A permit that first appears inside such a reload with an older issue date is not delivered.
- `W` moves to that batch even when every row was older than `maxPermitAgeDays`.

An unfinished batch resumes on the next run. Keys already pushed are not sent again. A batch published after the opening snapshot is not read in this run.

If more than 200 records are lost to crashes before the next run, the oldest are dropped and listed in the log; they are never charged.

NYC sometimes publishes thousands of old permits at once. By default, permits issued more than 90 days before the run are skipped and not charged. Set `maxPermitAgeDays` higher to receive them. Wide settings may add `bulk-scan` charges ($0.003 per 2,500 rows scanned beyond 7,500).

The first batch of a dataset in a run is included in the start fee. Each later batch, and each later backfill day, is a `day-scanned` charge.

### 6. Pricing

The launch price is in [pricing.json](pricing.json). `apify push` does not read that file, and `actor.json` does not reference it. Nothing in this repository turns pricing on.

- $0.01 per `apify-actor-start` event (Apify charges one event per GB of run memory, minimum one; this Actor's 256 MB default and 512 MB maximum mean one event per run).
- $0.003 for each permit pushed (`permit-record`)
- $0.003 for each extra day scanned (`day-scanned`): a backfill day after the first, or a catch-up batch after the first batch of that dataset in the run
- $0.003 `bulk-scan` per full 2,500 rows downloaded beyond the first 7,500 in a run, not counting reload batches and not counting days already charged as `day-scanned`

Run daily to avoid catch-up charges.

The Console pricing must not include the synthetic `apify-default-dataset-item` event. If it is present, Apify charges it for every row in the default dataset, including the status row.

Set the platform floor `minimalMaxTotalChargeUsd` to $0.02 so a run can pay for the start, one day, and one permit. That floor is in `pricing.json` only.

Before each push the run reserves the permit price, a pending day charge, and a pending bulk-scan block. Before an extra day it requires room for that day and at least one permit. Before a page that would finish a bulk-scan block it requires room for that block and two permit charges. If the budget cannot cover the work, the run stops with state saved. If a charge returns fewer events than requested, the run stops.

Default memory is 256 MB. Maximum memory is 512 MB. The default run timeout is 7,200 seconds. The code reads `ACTOR_TIMEOUT_AT` and stops at least 300 seconds before it, with state saved.

Every `day-scanned` and `bulk-scan` charge is written to the log, the run status, and `OUTPUT.charges`. The final status looks like:

`Done: 412 permits ($1.236), 3 day-scanned ($0.009) [nyc_dob_now 2026-10-11 2,480/0; 2026-10-12 2,512/0; nyc_bis 2026-10-12 17/1], 0 bulk-scan, start $0.01`

`OUTPUT` also records per-portal request and 429 counts, and per-issue-date scanned and emitted counts, so a live run can measure throttling and late arrivals.

### 7. Fair-access design

The Actor calls the NYC Open Data API directly. It does not use a proxy and it does not rotate addresses. The only outbound requests are `https://data.cityofnewyork.us/resource/<id>.json`.

- At most one request per second, and one request in flight, for the whole run.
- Page size 1,000. Order is the city's key. `$order` never uses `:id`.
- 429 or 503: wait for `Retry-After` when it is present, at most 300 seconds. Otherwise wait 10, 20, 40, 80, then 160 seconds, plus up to 20 percent jitter. Five retries. After any 429 the interval becomes 2 seconds for the rest of the run.
- Other 5xx responses and network timeouts (60 seconds): the same waits, three retries.
- 400: no retry. The query is logged and that dataset is skipped.
- 403: no retry. Two consecutive 403 responses stop the whole run. The status says NYC Open Data returned HTTP 403.
- 404 or 410: no retry. That dataset is skipped.
- When retries run out, the dataset stops for this run, the cursor stays on the last finished batch, and the log says `<dataset> unavailable, will resume next run`. The other dataset continues. If every dataset fails, the run ends without throwing so a schedule can try again.

No input raises the rate. Per-dataset kill switches live in `src/config.ts`. A disabled dataset is skipped until the next build. Prices and the 90-day age cutoff are in that same file.

### 8. Privacy

Free text is off by default. `job_description` is not requested unless `includeFreeText` is true. When it is on, email addresses and phone numbers are redacted. People's names written in the text can remain. The redaction is pattern matching, not name detection. A known leftover is a sentence such as "CONTACT REFRIGERATION SUPERVISOR JOHN DOE" after the phone number is removed. That is why the default is off.

Business-name fields can contain a person's name, for example sole proprietors or companies named after their owners. They stay off unless `includeBusinessNames` is true.

Owner names, applicant names, phone numbers, and license numbers are on the exclude list. They are not requested and they are not copied into `source`.

Ample Labs does not store your run data. Your results and state stay in your own Apify storage. Apify gives Actor developers aggregated analytics (runs, revenue, success rate), and if you share a run with us for debugging, we can see that run's input, log and results. What else Apify shows a developer by default is not verified here.

### 9. Lawful use

Do not use this feed for unsolicited mass messaging. The [Apify Acceptable Use Policy](https://docs.apify.com/legal/acceptable-use-policy) prohibits that.

If you email anyone using information from this feed, the CAN-SPAM Act, 15 U.S.C. 7704(a)(5), requires a clear identification that the message is an advertisement or solicitation, a working opt-out, and a valid physical postal address of the sender, unless you already have their prior affirmative consent for the identification requirement.

Permit records are taken from state and local government records. The California Consumer Privacy Act, Civil Code §1798.140, excludes publicly available information, including information lawfully made available from federal, state, or local government records. That exclusion does not by itself make every later use lawful.

This is not legal advice.

### 10. Removal requests

Street address is part of the default output. To have a permit left out of future runs, email support@haveample.com from the address you want us to reply to, with the dataset (`rbx6-tga4` or `ipu4-2q9a`), the permit number, record key, BIN, or BBL, and the site address. Do not include a personal name, government ID, or other documents. We may reply to confirm the request before acting on it.

A permit number or record key removes that one row. A BIN or BBL removes every future row at that building or lot. A building-wide entry is added only when the requester asked for the whole building and passed the identity check for that building. The entry records the support ticket id and the date. It does not record a name or an email address.

A finished takedown stops future deliveries of that row. It does not delete rows a customer already received. Those rows stay in that customer's own Apify account.

Maintainers edit `exclusions.json` and ship a new build. See [EXCLUSIONS.md](EXCLUSIONS.md).

### 11. Support

Report problems in the Issues tab on this Actor's page. Questions and takedown requests: support@haveample.com. For a takedown, include the details listed under Removal requests above.

Version history is in [CHANGELOG.md](CHANGELOG.md). This Actor uses semantic versioning. Apify's actor.json version is `0.2` because the input schema changed in a way that is not backward compatible.

# Changelog

This Actor's version history is a separate document: https://apify.com/amplelabs/nyc-building-permit-feed/changelog.md

# Actor input Schema

## `datasets` (type: `array`):

nyc_dob_now = NYC DOB NOW approved permits (rbx6-tga4); nyc_bis = NYC legacy BIS permit issuance (ipu4-2q9a). The Console form starts with nyc_dob_now only. A call that omits this field still uses both datasets. An unknown value, for example chicago, is rejected with HTTP 400 only by this schema when a run is started through the Apify API. The Console blocks it in the form. Anywhere else the run fails with an unknown dataset error.

## `dateFrom` (type: `string`):

YYYY-MM-DD, interpreted in the city's local date. Used only when there is no saved cursor, or when resetState is true. Default: yesterday. Clamped to at most maxBackfillDays before today.

## `dateTo` (type: `string`):

YYYY-MM-DD. Default: today. If set, the run is a one-off window and does not advance the saved cursor.

## `maxBackfillDays` (type: `integer`):

Days beyond the first are charged day-scanned ($0.003 each).

## `boroughs` (type: `array`):

Empty = all. NYC datasets only.

## `zipCodes` (type: `array`):

Empty = all.

## `permitTypeContains` (type: `array`):

Case-insensitive substring match on normalized permit_type OR work_type, e.g. 'NEW CONSTRUCTION', 'Plumbing', 'NB'.

## `minEstimatedCost` (type: `integer`):

Applies to nyc_dob_now. nyc_bis has no cost field; its records are dropped when this is set unless includeUnknownCost is true.

## `includeUnknownCost` (type: `boolean`):

Keep records with no cost when a minimum is set. BIS has no cost field, so it is dropped when a minimum is set unless this is true.

## `includeFreeText` (type: `boolean`):

Adds the DOB NOW job_description as description. Off by default. Emails and phone numbers are redacted, but people's names written in the text can remain.

## `includeBusinessNames` (type: `boolean`):

Adds the 'optin' business-name fields from field_allowlists.json inside source. Note: a business name can be a person's name (sole proprietors, LLCs named after people).

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

Stop after this many charged permits have been pushed. State is saved and the run can resume. The Console form starts at 10. A call that omits this field still uses 5000.

## `maxPermitAgeDays` (type: `integer`):

Old permits that NYC publishes late in bulk (backlog dumps) are skipped and not charged. Raise this to receive them.

## `resetState` (type: `boolean`):

Forget the saved cursor and seen keys for this stateKey and treat this run as a first run. Turn it off again after that run.

## `stateKey` (type: `string`):

Names the saved cursor. The store is multi-city-permit-feed-state-<stateKey>. Use one key per schedule or filter setup. Changing filters or includeFreeText on an existing key does not re-deliver days already charged; use a new stateKey for that. Lowercase letters, digits, and hyphens, 1 to 28 characters. Default is default.

## Actor input object example

```json
{
  "datasets": [
    "nyc_dob_now"
  ],
  "maxBackfillDays": 1,
  "boroughs": [],
  "zipCodes": [],
  "permitTypeContains": [],
  "includeUnknownCost": false,
  "includeFreeText": false,
  "includeBusinessNames": false,
  "maxResults": 10,
  "maxPermitAgeDays": 90,
  "resetState": false,
  "stateKey": "default"
}
```

# Actor output Schema

## `overview` (type: `string`):

Dataset items. The Output tab uses the overview view.

# 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 = {
    "datasets": [
        "nyc_dob_now"
    ],
    "maxResults": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("amplelabs/nyc-building-permit-feed").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 = {
    "datasets": ["nyc_dob_now"],
    "maxResults": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("amplelabs/nyc-building-permit-feed").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 '{
  "datasets": [
    "nyc_dob_now"
  ],
  "maxResults": 10
}' |
apify call amplelabs/nyc-building-permit-feed --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,amplelabs/nyc-building-permit-feed"
        }
    }
}
```

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/gRyFXBccUdBxhCqkW/builds/P5wnsonSeEo992b1i/openapi.json
