NYC Building Permit Feed, unofficial avatar

NYC Building Permit Feed, unofficial

Pricing

from $3.00 / 1,000 permit records

Go to Apify Store
NYC Building Permit Feed, unofficial

NYC Building Permit Feed, unofficial

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.

Pricing

from $3.00 / 1,000 permit records

Rating

0.0

(0)

Developer

Ample Labs

Ample Labs

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Share

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 (DOB NOW: Build – Approved Permits) and 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 idSourceNYC Open Data API
nyc_dob_nowNYC DOB NOW approved permitshttps://data.cityofnewyork.us/resource/rbx6-tga4.json
nyc_bisNYC legacy BIS permit issuancehttps://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 NOWBIS
Dataset nameDOB NOW: Build – Approved PermitsDOB Permit Issuance
Dataset idrbx6-tga4ipu4-2q9a
NYC Open Data URLhttps://data.cityofnewyork.us/Housing-Development/DOB-NOW-Build-Approved-Permits/rbx6-tga4https://data.cityofnewyork.us/Housing-Development/DOB-Permit-Issuance/ipu4-2q9a
Item fieldssource_name, dataset, source_urlsame

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

InputDefaultWhat it does
datasetsbothWhich 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.
dateFromunsetExplicit 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.
dateTounsetOne-off window. The saved watermark is not moved.
maxBackfillDays1How far an explicit dateFrom / dateTo range may look back, counting from dateTo. It does not extend the default first-run lookback.
boroughsemptyEmpty means all. Compared with upper(borough).
zipCodesemptyEmpty means all.
permitTypeContainsemptyCase-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.
minEstimatedCostunsetDollars. DOB NOW is filtered after download because the cost column is text. BIS has no cost; those rows are dropped unless includeUnknownCost is true.
includeUnknownCostfalseKeep rows with no parseable cost when a minimum is set.
includeFreeTextfalseAdd the DOB NOW description. Off unless you opt in. See Privacy.
includeBusinessNamesfalseAdd business-name columns inside source.
maxResults5000Stop after this many charged permits in the run. The status says to run again. State is already saved.
maxPermitAgeDays90Skip permits issued more than this many days before the run. They are not charged. The watermark still moves past a batch of old permits.
stateKeydefaultNames the saved cursor. Use one key per schedule or filter setup. 1–28 characters: lowercase letters, digits, and hyphens.
resetStatefalseForget 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.

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.

{
"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. 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 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.

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. 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.