US Commercial Fire Permit & Inspection Leads Scraper avatar

US Commercial Fire Permit & Inspection Leads Scraper

Pricing

from $6.00 / 1,000 results

Go to Apify Store
US Commercial Fire Permit & Inspection Leads Scraper

US Commercial Fire Permit & Inspection Leads Scraper

Commercial fire-protection permits, failed inspections and code violations from official city sources — open-data APIs (Socrata, ArcGIS, CKAN, Carto) and permit portals (Accela, Tyler EnerGov) — normalized, deduplicated, scored 0-100, ready for fire-protection sales teams.

Pricing

from $6.00 / 1,000 results

Rating

0.0

(0)

Developer

Scrapelab Max

Scrapelab Max

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

Share

Turn official government permit records into a ready-to-call sales pipeline for fire-protection contractors, alarm/sprinkler installers, and fire-safety compliance vendors.

1. What you get

This actor is a commercial fire-protection lead-intelligence feed, not a generic permit scraper. Every run pulls new sprinkler and fire-alarm permits, fire-pump and standpipe work, kitchen-suppression installs, failed fire inspections, reinspection orders, fire-code violations, and expiring certifications straight from official city/county sources — open-data APIs (Socrata, ArcGIS, CKAN, Carto) and permit portals (Accela Citizen Access, Tyler EnerGov). Raw municipal records are normalized into one consistent schema, deduplicated across overlapping datasets, and scored 0–100 with human-readable leadSignals explaining exactly why a record is (or isn't) worth a sales call — a brand-new sprinkler permit with no contractor listed and a $250k project value scores very differently than a two-year-old closed permit. Every record carries a source.url back to the official government dataset so a buyer can verify it independently before spending a call.

2. Supported jurisdictions

Coverage is 33 metro areas, chosen by verified data availability (clean Socrata/ArcGIS/CKAN/Carto open-data, a scriptable Accela Citizen Access or Tyler EnerGov CSS portal, or a city report that answers plain HTTP, with real fire-protection records), not by population or geography alone.

JurisdictionSourcesRecord typesData richness
New York City, NYNYC DOB NOW: Build – Approved PermitspermitFire-specific work-type filter (Sprinklers, Standpipe). FDNY fire-alarm permits are not on open data, so alarm coverage here is partial.
Chicago, ILChicago Building PermitspermitKeyword-matched from free-text work descriptions across the full permit table.
San Francisco, CASF DBI Building Permits, SFFD Fire Permits, SFFD Fire Inspections, SFFD Fire Violationspermit, inspection, violationThe richest jurisdiction in the registry — permits, dedicated fire-department operational permits, fire inspections (incl. NFPA-25 sprinkler inspections), and fire violations, all four sources. High inspection volume; use minimumLeadScore to throttle.
Seattle, WASeattle Trade PermitspermitCleanest source in the registry — an explicit "Fire Sprinkler and Suppression" permit type, not keyword-matched.
Austin, TXAustin Issued Construction PermitspermitExplicit work_class values (e.g. "Fireline") plus keyword-matched descriptions.
Los Angeles, CALA Building Permits, LA Electrical PermitspermitTwo complementary sources — building permits (heavily residential; the residential filter matters) and electrical permits (the better source for commercial fire-alarm work).
Mesa, AZMesa Building PermitspermitExplicit commercial/residential permit-type field; deliberately excludes bare "FIRE" keyword matching to avoid "National Fire Protection Association" boilerplate false positives.
New Orleans, LANew Orleans PermitspermitExplicit "Hoods" permit type for commercial kitchen suppression, plus keyword matching.
Washington, DCDC Building Permits (current + prior year)permitArcGIS feature layers, one per calendar year — see Architecture.md for the annual maintenance this requires.
Charlotte / Mecklenburg County, NCMecklenburg County Accela Permits; Mecklenburg County Building Permits (legacy)permitCounty-wide — covers Charlotte and every other Mecklenburg municipality; address.city names the town. The county moved new projects to Accela in January 2025, and that layer carries the trade permits: fire-alarm work here is filed as a commercial electrical permit. Status, zip, coordinates and owner are published; contractor is not. The county's server occasionally returns a description with text from a neighbouring row (about 2% of rows), so check source.recordUrl where the wording matters. The legacy feed is still read for projects begun before the move.
Boston, MABoston Approved Building PermitspermitCKAN datastore (SQL API), daily refresh. Rich free-text comments plus a dedicated "Electrical Fire Alarms" permit type.
Philadelphia, PAPhiladelphia L&I Building & Trade PermitspermitCarto SQL API. Dedicated "Fire Suppression" permit types (current + legacy BP_FIRESUP) plus keyword-matched scope-of-work; explicit commercial/residential field.
Nashville, TNNashville Building Permits IssuedpermitArcGIS Hub layer with a rolling ~3-year window. Keyword-matched from the Purpose free text; fire-damage rehab permits deliberately excluded; no status field.
Columbus, OHColumbus Building PermitspermitDedicated "Fire Alarm"/"Fire Suppression" permit categories plus keyword-matched value descriptions; closed 9-value commercial/residential field.
Miami, FLMiami Building Permits (Since 2014)permitKeyword-matched scope-of-work; native lat/lon; clean binary Residential/Commercial flag.
Portland, ORPortland BDS All PermitspermitBureau of Development Services "All Permits" layer, keyword-matched descriptions; no zip/contractor/coordinate fields.
Baltimore, MDOpen Baltimore Building PermitspermitKeyword-matched descriptions; combined address string; no coordinate attributes. The city files apartment buildings under the use Dwelling: Multi-Family; those rows are treated as commercial and arrive in default runs. One- and two-family homes are held back unless includeResidential is on.
Kansas City, MOKansas City, MO Issued Building PermitspermitExplicit work-class values like "Electrical Fire Alarm Commercial" plus keyword-matched permit types.
Minneapolis, MNMinneapolis CCS PermitspermitKeyword-matched comments; native lat/lon; occupancy-code commercial/residential field (~20% of rows blank).
Louisville, KYLouisville Active Construction PermitspermitDedicated "Fire Detection"/"Fire Suppression System" permit types; the fullest field set of the phase-2 sweep.
Detroit, MIDetroit BSEED Trades Permits; Detroit BSEED Building PermitspermitDetroit files fire work as trade permits: a fire alarm is its own permit type, and sprinkler, standpipe, fire-pump and hood-suppression work is filed as a mechanical permit. Owner and the filing company are published; status, contractor and project value are not. Building permits whose description names fire work are read as well.
Raleigh, NCRaleigh Building PermitspermitKeyword-matched descriptions; contractor company, plus the contractor's phone and email with includeContactDetails; explicit Residential/Non-Residential field.
Fort Worth, TXFort Worth Permits (CIVIC)permitOn-prem MapServer without an OBJECTID field (paginated via a configured sort column); the filing date stands in for the issued date — the layer publishes no true issued-date field.
San Antonio, TXSan Antonio Permits IssuedpermitCKAN datastore updated ~monthly; free-text project names catch fire work the permit-type taxonomy doesn't.
Memphis, TNMemphis DPD Building PermitspermitKeyword-matched descriptions; clean RES/COM commercial/residential field.
Tucson, AZTucson Commercial Building PermitspermitCommercial-only permit layer (Tyler EnerGov published through a standard ArcGIS MapServer).
Sacramento, CASacramento Issued Building Permits — current-year + archive layers, each split into a dedicated County Fire permit-type source and a keyword sourcepermitDedicated 'County Fire' permit type (many with terse descriptions a keyword search would miss) plus keyword-matched work descriptions. Dates are MM/DD/YYYY strings — served by the hybrid string-date support; two layers require January boundary maintenance (see Architecture.md).
Virginia Beach, VAVirginia Beach Building Permits — dedicated Fire permit-type source + keyword sourcepermitDedicated 'Fire' permit type (NFPA 13/72 scope text, ~860/yr) plus keyword-matched descriptions with server-side exclusion of 'NON-SPRINKLERED' boilerplate. Dates are YYYY/MM/DD strings — served by the hybrid string-date support. No contractor or valuation fields.
Colorado Springs, COColorado Springs Fire Department Records (Accela Citizen Access)permitAccela Citizen Access fire module, read over plain HTTP — 10 construction fire-system record types (sprinkler, alarm, pumps, standpipe, suppression, responder radio) with full work descriptions. Addresses aren't in the CSV export, so they're recovered in bulk from the results grid; each of the ten fire record types is searched separately so the grid pages fetched are all fire records rather than ~15% of them, which is what makes a large maxResults affordable. Records the walk still can't reach are withheld rather than sold without an address, and the run summary says how much of the window was covered and why it stopped.
Omaha, NEOmaha Fire Prevention Records (Accela Citizen Access)permitAccela Citizen Access fire module, read over plain HTTP — fire alarm applications, protection equipment, standpipe and extinguisher-system records. Same bulk grid-based address recovery as Colorado Springs; typically cheaper per lead here since Omaha's weekly volume is small.
Atlanta, GAAtlanta Fire Protection Permits (Accela Citizen Access, Building module)permitAccela Citizen Access, read over plain HTTP — Atlanta files fire-system permits as record types inside its Building module, and three are searched: Commercial - Fire Sprinkler, Residential - Fire Sprinkler and Commercial - Fire Underground, with work descriptions and permit names. Dates are filing dates, so leads arrive before issuance. "Residential" is the city's own type name and mostly covers multi-unit buildings: default runs include those rows, and the type name never triggers includeResidential. The usual text check still applies, so a row whose own description says townhouse or single-family is held back unless includeResidential is on. Known portal gaps: on about 9% of rows the portal prints the placeholder ZIP 00000, which is delivered as a null zip; about 3% have no ZIP at all and keep the whole address in street; and in-progress applications can have a blank status or description.
Tulsa, OKTulsa Fire Prevention Records (Tyler EnerGov Citizen Self Service)permitTyler EnerGov CSS, a stateless multi-tenant JSON REST API read anonymously — no cookies, viewstate, or sessions. A tenant-header chain pinned from the portal's tenant list scopes every request to Tulsa, and coverage comes from one type-narrowed search per fire permit-type/work-class pair, with exact counts read off the API's own total. Unlike the Accela adapter, this one never withholds a record for a missing address.
Houston, TXHouston Sold Permits (city report)permitPilot. Reads the city's Sold Permits report for a pinned list of 20 ZIPs, for up to 3 days per run: the day before the run in Houston's time and the two days before it, as far back as your window reaches. A 90-day run reads 3 days of it and says so in the log. Fire alarm and sprinkler permits and their plan review fees, with the occupant's name and the street address; saleKind says which of the two a lead is. With onlyNewRecords on, a project is delivered once, when it is first sold, which is often its plan review fee; with it off, a project is delivered by every run whose days include a day it was sold on, as every source re-delivers its window. No contractor, status or link. The ZIPs: 77002, 77003, 77006, 77007, 77008, 77017, 77018, 77019, 77022, 77024, 77027, 77042, 77055, 77056, 77072, 77079, 77082, 77084, 77092, 77098. A ZIP you ask for outside them is listed under zipsNotWalked in the coverage report and is not read. A ZIP you ask for selects the city, as for every source: the run reads all 20 ZIPs and delivers leads from all of them, not only from the ZIP you named.

Coverage is growing. Requested locations outside this list are reported as unsupported with a reason in the run's coverage report — you are never charged for them.

Known-unsupported metros and why (verified 2026-08-08, re-checked entries 2026-08-10):

MetroReason
San Diego, CAPermits published only as static CSV files; no queryable API.
Jacksonville, FLPermits live in a custom Azure-AD-gated app (JAXEPICS); its API refuses anonymous requests and the open-data portal is dead.
San Jose, CAOpen data is current but carries no fire-protection-distinguishing permit types — zero matches.
Indianapolis, INAccela is scriptable but has no fire permit taxonomy, no description column in results, and a silent 150-row cap.
Oklahoma City, OKAccela is anonymous here but publishes no fire module; the data portal remains bot-blocked.
El Paso, TXThe Accela fire module is login-gated; the anonymous building module carries no fire permit types.
Las Vegas, NVPermit table is current but has only generic trade categories and no fire-distinguishing text across 436k rows; fire permits live in an unpublished LVFR system (re-verified 2026-08-10).
Milwaukee, WIPermit dataset is live but still has 4 coarse type buckets and no work description — fire work indistinguishable (re-verified 2026-08-10).
Albuquerque, NMPermit layer stopped updating 2025-01-16; no current feed.
Fresno, CAAnonymous Accela access is disabled agency-wide; the replacement portal needs JavaScript.
Long Beach, CANo building-permit dataset on either of its open-data portals.
Oakland, CAAccela is scriptable but the fire module has no date search and the building module no fire types.
Bakersfield, CARuns Click2Gov, which offers no date-range search at all.
Tampa, FLCurrent layers contain no fire-protection-distinguishable permits.
Arlington, TXStructured type fields have zero fire-protection values; free-text hits were legal-notice boilerplate.
Dallas, TXThe Socrata dataset is frozen at 2019-12-31, the newest ArcGIS layer ends Nov 2024, and the Accela portal has no fire module.
Phoenix, AZThe CKAN portal has no record-level permit data — its only permit dataset is aggregate annual housing-unit counts, stale since 2023.
Denver, COPermit layers are current but expose no free-text description, so fire work is indistinguishable; fire-department permits are unpublished.

3. Quick start

Minimal input to pull fresh fire-alarm and fire-sprinkler leads across three cities:

{
"cities": ["Seattle, WA", "San Francisco, CA", "New York, NY"],
"permitTypes": ["fire_alarm", "fire_sprinkler"],
"startDate": "2026-07-01",
"maxResults": 5000
}

Run this and the actor resolves each city to its registered source(s), fetches permits issued on or after 2026-07-01, keeps only fire-alarm and fire-sprinkler records, deduplicates them, scores each 0–100, caps at 5,000, sorts best-first, validates every record against the output schema, and pushes the result to the run's dataset — plus a COVERAGE_REPORT key in the run's key-value store summarizing what succeeded, what failed, and what was unsupported.

Pricing and the per-run charge limit. You pay a flat price per lead delivered (see the pricing tab) plus Apify's nominal actor-start fee (a fraction of a cent per run); unsupported jurisdictions, failed sources and suppressed duplicates are never charged. Every run also has a maximum charge, set by Apify from your plan and adjustable per run (maxTotalChargeUsd). The actor reads that limit before it fetches anything and stops at whichever is lower: maxResults or the number of leads your limit can pay for — so it never fetches leads it can't deliver, and every lead in the dataset is one you were charged for. When the limit was the binding constraint, the coverage report says so (chargeLimit.reached: true, chargeLimit.leadsWithinLimit); leads that weren't delivered are not marked as seen, so a follow-up run with a higher limit or onlyNewRecords: true picks them up. For Houston a follow-up run reads the last 3 days only.

How the cap is shared. Every source you asked for is read, whatever the cap. If more leads match than maxResults (or your charge limit) allows, the sources that have leads share the cap equally and each gives its best-scored leads; a source with fewer leads than its share gives them all and the rest is passed on. With onlyNewRecords on, leads you already received do not count against the cap. The coverage report shows, for each source, how many leads it found (emittedCount), how many you had already received (alreadySeenCount) and how many were delivered (deliveredCount).

What a source can ever fill. Each source's row in the coverage report has a publishes block: status, applicationDate, issuedDate, expirationDate, projectValue, businessName, owner, applicant, contractor, contractorLicense, contactDetails and recordUrl, each true or false. true means the source has a column for the field, though a record may leave it empty. false means no record of that source will ever hold it. So a lead with no contractor from a source whose contractor is false is not a job without a contractor yet: the city does not name one.

More example inputs

Daily fresh leads for one state. Schedule this daily; only records not seen on a previous run (or whose status changed) come back, so you pay only for what is new.

{
"states": ["WA"],
"permitTypes": ["fire_alarm", "fire_sprinkler"],
"onlyNewRecords": true,
"maxResults": 1000
}

One permit-portal city. Tulsa is served by a Tyler EnerGov portal rather than an open-data API — same input shape, same output schema. Portal cities take longer per lead than open-data cities (roughly two minutes for 50 leads), so keep maxResults modest on a first run.

{
"cities": ["Tulsa, OK"],
"startDate": "2026-08-01",
"maxResults": 100
}

Best-scored leads only, permits and inspections. San Francisco publishes fire inspections and violations as well as permits; minimumLeadScore keeps the feed to records worth a call.

{
"cities": ["San Francisco, CA"],
"includeInspections": true,
"includeViolations": true,
"minimumLeadScore": 60,
"maxResults": 250
}

4. Example output

One real record, captured live from the Seattle Trade Permits source (public government data — nothing redacted):

{
"recordId": "seattle-trade-permits:7005022-FS",
"jurisdiction": { "city": "Seattle", "county": "King", "state": "WA" },
"businessName": null,
"projectName": null,
"address": {
"street": "705 NE NORTHLAKE WAY",
"city": "Seattle",
"state": "WA",
"zip": null,
"latitude": null,
"longitude": null
},
"recordType": "permit",
"fireSystemType": "fire_pump",
"workType": "replacement",
"permitNumber": "7005022-FS",
"permitStatus": "Issued",
"saleKind": null,
"applicationDate": null,
"applicationDateKind": null,
"issuedDate": "2025-11-19",
"expirationDate": null,
"inspectionDate": null,
"inspectionStatus": null,
"violations": [],
"description": "REVISION TO PERMIT 6911078-FS: Add vertical turbine fire pump taking suction from Lake Washington to replace water supply from municipal system for dry-standpipe system serving a boat moorage. | Fire Sprinkler and Suppression",
"projectValue": null,
"propertyType": null,
"owner": { "name": null, "company": null, "phone": null, "email": null },
"applicant": { "name": null, "company": null, "phone": null, "email": null },
"contractor": { "name": null, "company": null, "licenseNumber": null, "phone": null, "email": null },
"leadScore": 80,
"leadSignals": ["FIRE_PUMP_INSTALLATION", "SYSTEM_REPLACEMENT", "NO_CONTRACTOR_LISTED", "COMMERCIAL_PROPERTY"],
"source": {
"sourceId": "seattle-trade-permits",
"jurisdiction": "Seattle, WA",
"provider": "socrata",
"url": "https://data.seattle.gov/Permitting/Trade-Permits/c87v-5hwh",
"recordUrl": "https://services.seattle.gov/portal/customize/LinkToRecord.aspx?altId=7092673-FS",
"recordUrlKind": "page"
},
"scrapedAt": "2026-08-07T23:38:48.781Z"
}

leadScore 80 reflects a fire-pump replacement (high-value system work) with no contractor listed yet (an unsold job) on a commercial property — three positive signals stacked on the 40-point base score. Fields the source didn't populate (businessName, projectValue, zip, etc.) are null, never guessed.

5. Input reference

All fields from .actor/input_schema.json (maxResults is the only required field):

FieldTypeDefaultDescription
citiesstring[][]Cities as "City, ST" (e.g. "Seattle, WA").
countiesstring[][]Counties as "County, ST" (e.g. "Mecklenburg, NC").
statesstring[][]State codes or names (e.g. "CA", "Texas"). Expands to every supported jurisdiction in the state.
zipCodesstring[][]5-digit ZIP codes; matched to supported jurisdictions by prefix.
permitTypesstring[] (enum)[] (= all)Restrict to specific fire-protection categories: fire_alarm, fire_sprinkler, fire_pump, standpipe, kitchen_suppression, special_suppression, inspection, fire_code_violation, certificate_of_occupancy, other_fire_protection.
keywordsstring[][]Case-insensitive keywords the record description must contain (e.g. "NFPA 13").
startDatestring (YYYY-MM-DD)null → lookbackDays before run timeEarliest record date.
endDatestring (YYYY-MM-DD)null → todayLatest record date.
lookbackDaysinteger (1–365)null → 90Days before run time to read from when startDate is not set. A schedule's input is fixed, so this is how a daily run asks for "the last week". startDate, if set, wins.
includeInspectionsbooleantrueInclude fire inspection records (failed inspections are high-value leads).
includeViolationsbooleantrueInclude fire-code violation records.
includeExpiredOrExpiringbooleantrueInclude records whose permits/certifications are expired or expiring soon.
includeResidentialbooleanfalseInclude single-family residential records (off by default — this is a commercial lead product).
onlyNewRecordsbooleanfalseReturn only records new or changed since your last run (per-account state). Run daily for a fresh-leads feed.
maxResultsinteger (1–100000)required, prefill 500Hard cap on returned leads — you pay per result. Every requested source is read; when the cap binds, sources share it equally.
minimumLeadScoreinteger (0–100)0Only return leads scoring at least this. 60+ = strong signals only.
includeRawDatabooleanfalseAttach the original source record under rawData.
includeContactDetailsbooleanfalseFill the phone and email fields with what a permit record itself publishes for its contractor, applicant or owner. Off: both fields are null on every record. Filled only where the city's own record carries them (today Raleigh, Austin and Mesa), copied as published. With includeRawData on, rawData is the portal's row as published and holds whatever contact columns the portal publishes, whatever this input says.
includeSourceMetadatabooleantrueInclude merged-source provenance details (mergedSources). The primary source is always included.
socrataAppTokenstring (secret)nullOptional token from any Socrata portal — raises rate limits for heavy use.

6. Output reference

Every dataset item is a PermitLead:

FieldTypeMeaning
recordIdstringStable ID, namespaced by source (<sourceId>:<permitNumber-or-hash>).
jurisdiction{ city, county, state }The registry jurisdiction the record belongs to (nullable city/county, 2-letter state).
businessNamestring | nullBusiness associated with the permit/inspection, where the source publishes one.
projectNamestring | nullNamed project, where published.
address{ street, city, state, zip, latitude, longitude }All nullable except where the source provides them.
recordType"permit" | "inspection" | "violation"What kind of government record this is.
fireSystemTypeenumNormalized fire-protection category (see permitTypes above) — classifier output.
workType"new_installation" | "modification" | "replacement" | "repair" | "inspection" | "unknown"What kind of work this record represents.
permitNumberstring | nullOfficial permit/case number.
permitStatusstring | nullSource's status string (e.g. Issued, Closed, Expired).
saleKind"plan-review-fee" | "permit" | nullHouston only: what the city sold. plan-review-fee when only the fee that comes before a permit was sold on the days the run read, permit when a permit was. null on every other source. With onlyNewRecords on a project is delivered once, so this is what was first seen, not the current state.
applicationDate / issuedDate / expirationDatestring | null (YYYY-MM-DD)Permit lifecycle dates, where published. applicationDate is the portal's filing date. On four sources the portal does not document its date as a filing date, and it is delivered as published: Portland (CREATEDATE, the day the record was created), Chicago (application_start_date, "date when City began reviewing"), Colorado Springs (the results grid's Date) and Omaha (the results grid's Date). Houston is a fifth: the day the city sold the permit or its plan review fee. The report prints no date, so this is the day that was searched, and for a project sold on several of the days a run read it is the oldest of them. It is not a filing date and not a verified issue date, and Houston's issuedDate is always null. An approval date is never delivered as applicationDate, with that one exception: for a Houston permit the sale day may be the day of issue.
applicationDateKind"filed" | "sold" | "other" | nullWhat kind of date applicationDate is. filed: the portal's filing date. sold: the day the city sold the permit or its plan review fee (Houston). other: a date the portal does not document as a filing date (Portland, Chicago, Colorado Springs, Omaha). null when applicationDate is.
inspectionDatestring | nullFor inspection/violation records.
inspectionStatusstring | nulle.g. Failed, Passed, Reinspect.
violationsstring[]Violation description(s), for violation records.
descriptionstring | nullConcatenated source description fields — the raw text the classifier read.
projectValuenumber | nullReported/estimated project cost, where published.
propertyTypestring | nullNormalized property category (e.g. restaurant, office, warehouse) — classifier output; null when the source gives no usable signal.
owner{ name, company, phone, email }The property owner. All nullable. phone and email: see contractor.
applicant{ name, company, phone, email }Who filed, where the portal names them apart from the owner and the contractor. All nullable.
contractor{ name, company, licenseNumber, phone, email }All nullable — a permit with no contractor listed is itself a lead signal (unsold job). licenseNumber is filled where the permit row carries it (New York, Mesa). phone and email are that party's own, copied as the permit record publishes them, and only when includeContactDetails is on; a value that is not a phone or an email is null. When several rows of one permit name different parties, a phone, email or licence number is never moved beside another party's name.
leadScoreinteger (0–100)See below.
leadSignalsstring[]Every scoring rule that fired, in evaluation order.
source{ sourceId, jurisdiction, provider, url, recordUrl, recordUrlKind }Provenance. url is the dataset's or portal's home page, the same for every lead of a source. recordUrl opens this one record on the government's own site, and is null where no such link has been verified — a missing link rather than a guessed one. recordUrlKind says what it opens: page (the city's own page for the record), rest (the ArcGIS view of its row: plain HTML, a raw listing of attributes) or data (the open-data API's answer for its row, in JSON).
mergedSourcesSourceRef[] (optional)Present when a record was deduplicated across multiple sources; the additional sources that contributed data. Omitted entirely when includeSourceMetadata: false.
rawDataobject (optional)The original source row, only when includeRawData: true.
scrapedAtstring (ISO timestamp)When this run fetched the record.

Lead scoring

Every lead starts at a base score of 40 and every matching rule below adds or subtracts points; the total is clamped to 0–100. leadSignals lists every rule that fired.

SignalPointsFires when
FAILED_INSPECTION+25Inspection status indicates a failed/deficient/not-approved result.
NEW_FIRE_SPRINKLER_PERMIT+20New-installation permit classified as fire_sprinkler.
FIRE_CODE_VIOLATION+20A violation record, or fireSystemType: fire_code_violation.
NEW_FIRE_ALARM_PERMIT+15New-installation permit classified as fire_alarm.
FIRE_PUMP_INSTALLATION+15fireSystemType: fire_pump.
KITCHEN_SUPPRESSION_SYSTEM+15fireSystemType: kitchen_suppression.
REINSPECTION_REQUIRED+15Inspection status indicates a reinspection is required.
EXPIRING_CERTIFICATION+15expirationDate falls within the next 90 days.
SYSTEM_REPLACEMENT+12workType: replacement.
RECENTLY_ISSUED+12issuedDate within the last 30 days.
LARGE_PROJECT_VALUE+12projectValue >= $100,000.
RECENTLY_SUBMITTED+8No issuedDate yet, but applicationDate within the last 30 days.
NO_CONTRACTOR_LISTED+8Permit record with no contractor name or company — an unsold job.
RESTAURANT+8propertyType: restaurant.
COMMERCIAL_PROPERTY+5Classifier determined the property is not residential.
OLD_RECORD−20Most recent activity date is more than 365 days old.
WORK_COMPLETED−12permitStatus indicates completed/final/closed/expired/withdrawn/cancelled.
RESIDENTIAL−25Classifier determined the property is residential.
MISSING_ADDRESS−25No street address — the record is real, but nobody can turn up at the building.
SPARSE_RECORD−15No street address, no business name, and no description — almost nothing to act on.
STATUS_CHANGED(no points)Added by change detection when a record's status/dates changed since the last run — not a scoring rule, an informational signal.

7. Daily fresh-leads workflow

Set onlyNewRecords: true and schedule the actor to run daily (Apify Console → Actor → Schedules, or the Scheduler API). Each account's runs share one named key-value store (fire-permit-leads-state) holding a fingerprint per record — permit number where available, otherwise a normalized-address + fire-type + date fingerprint. This state is per Apify account, so your daily schedule and another customer's daily schedule never see each other's history. On each run, records not seen before are returned as new leads; records whose status, dates, or violations changed since last seen are returned with a STATUS_CHANGED signal added; unchanged records are suppressed entirely. Fingerprints older than 400 days are pruned automatically so the state store doesn't grow unbounded.

For a daily schedule, also set lookbackDays (7 to 14 is a sensible range). Without it each run re-reads the last 90 days from every source to find the handful of records that are new. A shorter window is faster and lighter on the cities' portals; the trade-off is that a status change on a record older than the window is not seen. Leave it unset for your first run so the full 90 days are delivered once.

Houston is read for 3 days per run at most, whatever lookbackDays says: the day before the run, in Houston's time, and the two days before it. lookbackDays: 1 reads one day. A daily run therefore reads again the two days before yesterday, and picks up what a failed or skipped run missed. The coverage report lists the days read (daysWalked) and every ZIP and day that was not read in full, with the reason (zipDaysNotRead). A gap older than 3 days is read by a run with startDate and endDate both set to that day. If the city's server fails three times in a row, or refuses a request, the run stops asking it and says so.

Run Houston one run at a time. Every request is 1.36 MB from a city server whose tolerance is not known, and the adapter sends one request at a time for that reason. Two runs at once double the load, and nothing in the actor prevents it: do not start a second run that includes Houston while one is still running.

8. API usage examples

All three examples use the quick-start input from section 3.

JavaScript (apify-client)

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const { items } = await client.actor('<ACTOR_ID>').call({
cities: ['Seattle, WA', 'San Francisco, CA', 'New York, NY'],
permitTypes: ['fire_alarm', 'fire_sprinkler'],
startDate: '2026-07-01',
maxResults: 5000,
}).then((run) => client.dataset(run.defaultDatasetId).listItems());
console.log(items.length, 'leads');

Python (apify-client)

from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("<ACTOR_ID>").call(run_input={
"cities": ["Seattle, WA", "San Francisco, CA", "New York, NY"],
"permitTypes": ["fire_alarm", "fire_sprinkler"],
"startDate": "2026-07-01",
"maxResults": 5000,
})
items = list(client.dataset(run["defaultDatasetId"]).iterate_items())
print(len(items), "leads")

curl

curl "https://api.apify.com/v2/acts/<ACTOR_ID>/run-sync-get-dataset-items?token=<TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"cities": ["Seattle, WA", "San Francisco, CA", "New York, NY"],
"permitTypes": ["fire_alarm", "fire_sprinkler"],
"startDate": "2026-07-01",
"maxResults": 5000
}'

9. Troubleshooting

  • Run failed with NoJurisdictionsMatched. None of your requested cities/counties/ states/zipCodes resolved to a supported jurisdiction — see section 2 for the exact list and unsupported reasons. You are not charged when this happens.
  • A jurisdiction shows failed in the coverage report. That source had an outage, or its upstream schema drifted (a probe canary detects sources that suddenly return zero records and refuses to report a false empty success — it's reported as failed instead). The run still returns results from every other requested jurisdiction. If a jurisdiction stays failed across multiple runs, please file an issue.
  • Zero results. Usually means the date range is too narrow (try widening startDate), or minimumLeadScore is filtering everything out (try lowering it or setting it to 0).
  • Socrata throttling / 429 errors under heavy use. Set socrataAppToken to a free app token from any Socrata portal (e.g. data.seattle.gov, data.cityofnewyork.us) — this raises your per-host rate limit.

10. Data sourcing & responsible use

This actor collects only from public government open-data platforms and citizen-access portals (Socrata, ArcGIS Feature Services, CKAN, Carto SQL, Accela Citizen Access, and Tyler EnerGov CSS) that municipalities publish, or leave anonymously browsable, for exactly this kind of reuse — every request is plain HTTP, never browser automation. It never bypasses authentication, solves a CAPTCHA, or accesses non-public records. Requests are rate-limited and retried politely per host — no aggressive parallel hammering of city infrastructure. Every record keeps a source.url pointing back to the official dataset so a buyer can independently verify it. When a field isn't published by the source, the actor emits null — it never fabricates a contact name, phone number, or any other detail not present in the government data. The phone and email fields are filled only when includeContactDetails is on, only where the permit record itself publishes one for that party, and exactly as published; nothing is looked up anywhere else. With includeRawData on, rawData is the portal's row as published: it holds whatever contact columns the portal publishes, whatever includeContactDetails says. A contractor can be a sole trader, so the calling and commercial-email rules that apply to you apply to these fields.