US Commercial Fire Permit & Inspection Leads Scraper
Pricing
from $6.00 / 1,000 results
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
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.
| Jurisdiction | Sources | Record types | Data richness |
|---|---|---|---|
| New York City, NY | NYC DOB NOW: Build – Approved Permits | permit | Fire-specific work-type filter (Sprinklers, Standpipe). FDNY fire-alarm permits are not on open data, so alarm coverage here is partial. |
| Chicago, IL | Chicago Building Permits | permit | Keyword-matched from free-text work descriptions across the full permit table. |
| San Francisco, CA | SF DBI Building Permits, SFFD Fire Permits, SFFD Fire Inspections, SFFD Fire Violations | permit, inspection, violation | The 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, WA | Seattle Trade Permits | permit | Cleanest source in the registry — an explicit "Fire Sprinkler and Suppression" permit type, not keyword-matched. |
| Austin, TX | Austin Issued Construction Permits | permit | Explicit work_class values (e.g. "Fireline") plus keyword-matched descriptions. |
| Los Angeles, CA | LA Building Permits, LA Electrical Permits | permit | Two complementary sources — building permits (heavily residential; the residential filter matters) and electrical permits (the better source for commercial fire-alarm work). |
| Mesa, AZ | Mesa Building Permits | permit | Explicit commercial/residential permit-type field; deliberately excludes bare "FIRE" keyword matching to avoid "National Fire Protection Association" boilerplate false positives. |
| New Orleans, LA | New Orleans Permits | permit | Explicit "Hoods" permit type for commercial kitchen suppression, plus keyword matching. |
| Washington, DC | DC Building Permits (current + prior year) | permit | ArcGIS feature layers, one per calendar year — see Architecture.md for the annual maintenance this requires. |
| Charlotte / Mecklenburg County, NC | Mecklenburg County Accela Permits; Mecklenburg County Building Permits (legacy) | permit | County-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, MA | Boston Approved Building Permits | permit | CKAN datastore (SQL API), daily refresh. Rich free-text comments plus a dedicated "Electrical Fire Alarms" permit type. |
| Philadelphia, PA | Philadelphia L&I Building & Trade Permits | permit | Carto SQL API. Dedicated "Fire Suppression" permit types (current + legacy BP_FIRESUP) plus keyword-matched scope-of-work; explicit commercial/residential field. |
| Nashville, TN | Nashville Building Permits Issued | permit | ArcGIS 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, OH | Columbus Building Permits | permit | Dedicated "Fire Alarm"/"Fire Suppression" permit categories plus keyword-matched value descriptions; closed 9-value commercial/residential field. |
| Miami, FL | Miami Building Permits (Since 2014) | permit | Keyword-matched scope-of-work; native lat/lon; clean binary Residential/Commercial flag. |
| Portland, OR | Portland BDS All Permits | permit | Bureau of Development Services "All Permits" layer, keyword-matched descriptions; no zip/contractor/coordinate fields. |
| Baltimore, MD | Open Baltimore Building Permits | permit | Keyword-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, MO | Kansas City, MO Issued Building Permits | permit | Explicit work-class values like "Electrical Fire Alarm Commercial" plus keyword-matched permit types. |
| Minneapolis, MN | Minneapolis CCS Permits | permit | Keyword-matched comments; native lat/lon; occupancy-code commercial/residential field (~20% of rows blank). |
| Louisville, KY | Louisville Active Construction Permits | permit | Dedicated "Fire Detection"/"Fire Suppression System" permit types; the fullest field set of the phase-2 sweep. |
| Detroit, MI | Detroit BSEED Trades Permits; Detroit BSEED Building Permits | permit | Detroit 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, NC | Raleigh Building Permits | permit | Keyword-matched descriptions; contractor company, plus the contractor's phone and email with includeContactDetails; explicit Residential/Non-Residential field. |
| Fort Worth, TX | Fort Worth Permits (CIVIC) | permit | On-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, TX | San Antonio Permits Issued | permit | CKAN datastore updated ~monthly; free-text project names catch fire work the permit-type taxonomy doesn't. |
| Memphis, TN | Memphis DPD Building Permits | permit | Keyword-matched descriptions; clean RES/COM commercial/residential field. |
| Tucson, AZ | Tucson Commercial Building Permits | permit | Commercial-only permit layer (Tyler EnerGov published through a standard ArcGIS MapServer). |
| Sacramento, CA | Sacramento Issued Building Permits — current-year + archive layers, each split into a dedicated County Fire permit-type source and a keyword source | permit | Dedicated '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, VA | Virginia Beach Building Permits — dedicated Fire permit-type source + keyword source | permit | Dedicated '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, CO | Colorado Springs Fire Department Records (Accela Citizen Access) | permit | Accela 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, NE | Omaha Fire Prevention Records (Accela Citizen Access) | permit | Accela 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, GA | Atlanta Fire Protection Permits (Accela Citizen Access, Building module) | permit | Accela 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, OK | Tulsa Fire Prevention Records (Tyler EnerGov Citizen Self Service) | permit | Tyler 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, TX | Houston Sold Permits (city report) | permit | Pilot. 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):
| Metro | Reason |
|---|---|
| San Diego, CA | Permits published only as static CSV files; no queryable API. |
| Jacksonville, FL | Permits live in a custom Azure-AD-gated app (JAXEPICS); its API refuses anonymous requests and the open-data portal is dead. |
| San Jose, CA | Open data is current but carries no fire-protection-distinguishing permit types — zero matches. |
| Indianapolis, IN | Accela is scriptable but has no fire permit taxonomy, no description column in results, and a silent 150-row cap. |
| Oklahoma City, OK | Accela is anonymous here but publishes no fire module; the data portal remains bot-blocked. |
| El Paso, TX | The Accela fire module is login-gated; the anonymous building module carries no fire permit types. |
| Las Vegas, NV | Permit 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, WI | Permit dataset is live but still has 4 coarse type buckets and no work description — fire work indistinguishable (re-verified 2026-08-10). |
| Albuquerque, NM | Permit layer stopped updating 2025-01-16; no current feed. |
| Fresno, CA | Anonymous Accela access is disabled agency-wide; the replacement portal needs JavaScript. |
| Long Beach, CA | No building-permit dataset on either of its open-data portals. |
| Oakland, CA | Accela is scriptable but the fire module has no date search and the building module no fire types. |
| Bakersfield, CA | Runs Click2Gov, which offers no date-range search at all. |
| Tampa, FL | Current layers contain no fire-protection-distinguishable permits. |
| Arlington, TX | Structured type fields have zero fire-protection values; free-text hits were legal-notice boilerplate. |
| Dallas, TX | The Socrata dataset is frozen at 2019-12-31, the newest ArcGIS layer ends Nov 2024, and the Accela portal has no fire module. |
| Phoenix, AZ | The CKAN portal has no record-level permit data — its only permit dataset is aggregate annual housing-unit counts, stale since 2023. |
| Denver, CO | Permit 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):
| Field | Type | Default | Description |
|---|---|---|---|
cities | string[] | [] | Cities as "City, ST" (e.g. "Seattle, WA"). |
counties | string[] | [] | Counties as "County, ST" (e.g. "Mecklenburg, NC"). |
states | string[] | [] | State codes or names (e.g. "CA", "Texas"). Expands to every supported jurisdiction in the state. |
zipCodes | string[] | [] | 5-digit ZIP codes; matched to supported jurisdictions by prefix. |
permitTypes | string[] (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. |
keywords | string[] | [] | Case-insensitive keywords the record description must contain (e.g. "NFPA 13"). |
startDate | string (YYYY-MM-DD) | null → lookbackDays before run time | Earliest record date. |
endDate | string (YYYY-MM-DD) | null → today | Latest record date. |
lookbackDays | integer (1–365) | null → 90 | Days 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. |
includeInspections | boolean | true | Include fire inspection records (failed inspections are high-value leads). |
includeViolations | boolean | true | Include fire-code violation records. |
includeExpiredOrExpiring | boolean | true | Include records whose permits/certifications are expired or expiring soon. |
includeResidential | boolean | false | Include single-family residential records (off by default — this is a commercial lead product). |
onlyNewRecords | boolean | false | Return only records new or changed since your last run (per-account state). Run daily for a fresh-leads feed. |
maxResults | integer (1–100000) | required, prefill 500 | Hard cap on returned leads — you pay per result. Every requested source is read; when the cap binds, sources share it equally. |
minimumLeadScore | integer (0–100) | 0 | Only return leads scoring at least this. 60+ = strong signals only. |
includeRawData | boolean | false | Attach the original source record under rawData. |
includeContactDetails | boolean | false | Fill 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. |
includeSourceMetadata | boolean | true | Include merged-source provenance details (mergedSources). The primary source is always included. |
socrataAppToken | string (secret) | null | Optional token from any Socrata portal — raises rate limits for heavy use. |
6. Output reference
Every dataset item is a PermitLead:
| Field | Type | Meaning |
|---|---|---|
recordId | string | Stable 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). |
businessName | string | null | Business associated with the permit/inspection, where the source publishes one. |
projectName | string | null | Named 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. |
fireSystemType | enum | Normalized fire-protection category (see permitTypes above) — classifier output. |
workType | "new_installation" | "modification" | "replacement" | "repair" | "inspection" | "unknown" | What kind of work this record represents. |
permitNumber | string | null | Official permit/case number. |
permitStatus | string | null | Source's status string (e.g. Issued, Closed, Expired). |
saleKind | "plan-review-fee" | "permit" | null | Houston 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 / expirationDate | string | 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" | null | What 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. |
inspectionDate | string | null | For inspection/violation records. |
inspectionStatus | string | null | e.g. Failed, Passed, Reinspect. |
violations | string[] | Violation description(s), for violation records. |
description | string | null | Concatenated source description fields — the raw text the classifier read. |
projectValue | number | null | Reported/estimated project cost, where published. |
propertyType | string | null | Normalized 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. |
leadScore | integer (0–100) | See below. |
leadSignals | string[] | 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). |
mergedSources | SourceRef[] (optional) | Present when a record was deduplicated across multiple sources; the additional sources that contributed data. Omitted entirely when includeSourceMetadata: false. |
rawData | object (optional) | The original source row, only when includeRawData: true. |
scrapedAt | string (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.
| Signal | Points | Fires when |
|---|---|---|
FAILED_INSPECTION | +25 | Inspection status indicates a failed/deficient/not-approved result. |
NEW_FIRE_SPRINKLER_PERMIT | +20 | New-installation permit classified as fire_sprinkler. |
FIRE_CODE_VIOLATION | +20 | A violation record, or fireSystemType: fire_code_violation. |
NEW_FIRE_ALARM_PERMIT | +15 | New-installation permit classified as fire_alarm. |
FIRE_PUMP_INSTALLATION | +15 | fireSystemType: fire_pump. |
KITCHEN_SUPPRESSION_SYSTEM | +15 | fireSystemType: kitchen_suppression. |
REINSPECTION_REQUIRED | +15 | Inspection status indicates a reinspection is required. |
EXPIRING_CERTIFICATION | +15 | expirationDate falls within the next 90 days. |
SYSTEM_REPLACEMENT | +12 | workType: replacement. |
RECENTLY_ISSUED | +12 | issuedDate within the last 30 days. |
LARGE_PROJECT_VALUE | +12 | projectValue >= $100,000. |
RECENTLY_SUBMITTED | +8 | No issuedDate yet, but applicationDate within the last 30 days. |
NO_CONTRACTOR_LISTED | +8 | Permit record with no contractor name or company — an unsold job. |
RESTAURANT | +8 | propertyType: restaurant. |
COMMERCIAL_PROPERTY | +5 | Classifier determined the property is not residential. |
OLD_RECORD | −20 | Most recent activity date is more than 365 days old. |
WORK_COMPLETED | −12 | permitStatus indicates completed/final/closed/expired/withdrawn/cancelled. |
RESIDENTIAL | −25 | Classifier determined the property is residential. |
MISSING_ADDRESS | −25 | No street address — the record is real, but nobody can turn up at the building. |
SPARSE_RECORD | −15 | No 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 ApifyClientclient = 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 requestedcities/counties/states/zipCodesresolved to a supported jurisdiction — see section 2 for the exact list and unsupported reasons. You are not charged when this happens. - A jurisdiction shows
failedin 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), orminimumLeadScoreis filtering everything out (try lowering it or setting it to0). - Socrata throttling / 429 errors under heavy use. Set
socrataAppTokento 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.