The home page says "same query, same result, any day you want to re-run it against the county's open data." That sentence is only worth anything if the query is published. This is the query.
Two public sources, both free and open, both queried directly with no intermediary:
| Source id | What it is | Endpoint |
|---|---|---|
miamidade-county | Miami-Dade County permit data (ArcGIS FeatureServer). This is the only source that publishes a contractor phone number. | https://services.arcgis.com/8Pc9XBTAsYuxx9Ny/arcgis/rest/services/miamidade_permit_data/FeatureServer/0 |
miami-city | City of Miami "Building Permits Since 2014" (ArcGIS FeatureServer). Publishes no phone number on any record — this is the source of the declared-floor-scope, name-only file. | https://services1.arcgis.com/CvuPhqcTQpZPT9qY/arcgis/rest/services/Building_Permits_Since_2014/FeatureServer/0 |
orlando-city | City of Orlando permits (Socrata dataset ryhf-m453 on data.cityoforlando.net). Configured, pulled daily, and not sold. Corrected 2026-08-19: this cell used to say the dataset publishes no telephone column at all. It does — contractor_phone_number — and on 2026-08-19 it carried a number on 746 of the 1,362 permits issued since 2026-08-01. Our field map had never requested that column, so the pipeline read an empty string on every Orlando row and we recorded our own omission as a fact about the city. The map now requests it. With the phone read, the 2026-08-19 run produced 2 Orlando flooring rows: one commercial, and one whose occupancy that source does not resolve. Neither is residential. Earlier that day it produced 4 — two of them fire-alarm sub-permits filed against someone else's tenant improvement, which the sub-permit gate should have removed but did not: it only read Miami's trailing-letter permit-number convention, and Orlando puts the discipline in front (FIR/ELE/MEC/PLM). Fixed the same day. Orlando is pulled and not sold because it now produces nothing at all: measured 2026-09-17 over the nightly files in out/permits/orlando/, each of the eight runs from 2026-09-09 to 2026-09-16 produced 0 rows, and the last run that produced a row of any kind was 2026-09-08, with one. | https://data.cityoforlando.net/resource/ryhf-m453.json |
The county fields we read, and what we call them:
Miami-Dade County (miamidade-county)
PermitNumber -> permit_id
PermitType -> (classifier input)
DetailDescriptionComments -> (classifier input)
ProposedUseDescription -> (classifier input)
PropertyAddress -> address
City -> city
EstimatedValue -> valuation
ContractorName -> permit_applicant
ContractorPhone -> applicant_phone
ContractorNumber -> applicant_license (added 2026-07-30)
applicant_license_class (decoded from the prefix)
OwnerName -> owner_or_builder
LastInspectionDate -> last_inspection
CoCcDate -> co_date
ResidentialCommercial -> (occupancy input)
City of Miami (miami-city)
PermitNumber -> permit_id
ScopeofWork -> (classifier input)
WorkItems -> (classifier input)
DeliveryAddress -> address
TotalCost -> valuation
CompanyName -> permit_applicant
CompanyAddress -> applicant_mail_address (added 2026-07-30)
CompanyCity -> applicant_mail_city (added 2026-07-30)
CompanyZip -> applicant_mail_zip (added 2026-07-30)
PropertyType -> (occupancy input — see §3, this is a BUILDING class)
Certificatedate -> co_date
ContractorNumber — the Florida DBPR licence number — on the permit itself;
MABER's own permit 2026059950 carries CGC1524060, and 829 of the first 1,000
permits in the 2026-07-22 → 27 window carried a populated value. The adapter already requested
outFields=*, so the licence was arriving on every record and being discarded by a config
omission that the page then described as a property of the county's data. Same class of error on the
City of Miami source: it publishes the applicant's own mailing address, and the rows built from it
shipped as "name only" while that address was thrown away. All four fields are now read.permit_applicant and not contractor. The county field is whoever filed the permit, which on repair and alteration permits is routinely a trade sub rather than the general contractor. Confirmed case: permit 2026056864, a $76,000 "REPAIR DUE TO FIRE", lists an air-conditioning company. The column is named for what it actually is.Everything above is free. A page that publishes the endpoints, the field mapping and the literal regexes has argued most of the way to "so why am I paying you", and until 2026-07-30 this page did not spend one word on the other half of that. So, only frictions I have actually exercised — each one is a command you can run and get the same answer:
supportedQueryFormats: JSON, geoJSON, PBF, and asking for a CSV is rejected:
…/query?where=1%3D1&f=csv returns HTTP 400 (checked 2026-07-30 on both
the county and City of Miami layers). Whatever you do with this data, you are parsing JSON, not
opening a file.maxRecordCount: 1000. A single request for the current 7-day window came back with
exactly 1,000 features and exceededTransferLimit: true — against
1,234 permits scanned in the window of the run behind today's numbers. One
request does not see the week.WHERE clause of
UPPER(DetailDescriptionComments) LIKE '%REMODEL%' is accepted and returns
{"count":4965}. I am not going to tell you the county's API is crippled; it is not.remodel-interior, and the MEP-prefix rules that must run before it or a
"MECH KITCHEN HOOD" permit is sold to you as a kitchen remodel), the phone gate, the $2,500
flooring-specific valuation floor, the corrupt-valuation and government-owner exclusions, project
de-duplication (14 rows at 14 distinct addresses in the
current run), the flooring-firm applicant exclusion (1 dropped this run),
the DBPR licence decode in §5, and the second City of Miami source that supplies the only rows
where the authority itself names floor coverings.Each run pulls permits whose issue date falls in a trailing window — currently 7 days — and every published file is named with the date it was generated. The window start and end of the run behind the current numbers are printed on the home page and in claims.json. Files are never edited after generation; a new run writes a new dated file beside the old one.
A first-match-wins classifier runs over the permit type and description, lowercased. The rule that produces this feed is remodel-interior, and these are its literal patterns:
/remodel/ /renovat/ /kitchen/ /bathroom/ /\bbath\b/ /interior build.?out/ /tenant improvement/ /interior alter/ /\baddition\b/
Order matters, and trade-specific rules run first — so a permit reading "MECH A/C CHANGEOUT" is classified hvac and never reaches this feed even if the description also mentions a bathroom. Each shipped row carries an evidence column stating the exact pattern that matched and the exact county text it matched in, for example:
matched /remodel/ in "MBLD INT REMODEL — CONDO/APT INDIVIDUAL UNIT"
Separately, a row is flagged county_declared_floor_scope = yes when the county's own text matches:
/wood\/laminate\/tile\/vinyl|floor.?covering|\bcarpet\b|\bflooring\b|\btile work\b|\bvinyl plank\b/i
These are the strongest rows we have, because the permitting authority itself states the job involves floor coverings. In practice they come from miami-city, which publishes no phone numbers, so they ship in their own file with contact = name-only.
Occupancy is derived from the same text. "SINGLE FAM RES-CLUST-ZERO LOT-TOWN HOUSE" and "CONDO/APT INDIVIDUAL UNIT" are residential; "MULTI-FAMILY", "UNITS OR MORE", "APARTMENT" are multifamily; retail, office, hotel, restaurant, warehouse and similar are commercial. Anything unmatched is unknown and is counted with commercial, never with residential.
That rule describes only one of the two sources, and this page used to imply it described both.
Miami-Dade marks occupancy at the unit level — its description literally says
"CONDO/APT INDIVIDUAL UNIT". The City of Miami has no equivalent field; what it publishes is
PropertyType, which is a building class. So a floor replacement in one condo unit
inside a commercial-classed tower is labelled commercial, and one inside a tower whose
WorkItems text reads "MULTI-FAMILY (CONDO)" is labelled multifamily — even
though the job is a single dwelling and exactly the work a two-truck residential crew does.
2901 S BAYSHORE DR # 17D, 1901 BRICKELL AV # B1603,
325 S BISCAYNE BLVD # 3117, 3131 NE 7 AV PH5002,
901 BRICKELL KEY BLVD # 704, 2000 N BAYSHORE DR # 1402,
36 NW 6 AV # 803) and none of them is labelled residential. The label is coming
from the building, not the job.address column is in every row, so a unit
number is visible to you on sight. If you work single condo units, read the declared-scope file
first and do not trust the occupancy column on miami-city rows.applicant_phone contains at least 7 digits. Name-only rows are not silently dropped — they go to the declared-scope file with contact = name-only so you know what you are holding./\bfloor(s|ing)?\b|\bhardwood\b|\bterrazzo\b|\bcarpet\b/i the row is
dropped: we do not sell a flooring contractor a job another flooring contractor already has. Be clear
about what that is and is not. It is a regex over a name, it fired on
1 applicant in the current run, and it cannot be upgraded to a
licence check — Florida does not license floor covering as a trade (see §5), so no licence class can
confirm or deny that a filer is a flooring firm. A flooring company trading under a name with no
floor word in it will get through this filter — as EMPIRE TODAY LLC, a national flooring
retailer, did: it reached the downloadable declared-scope file on five consecutive nights, because
“Empire Today” contains no floor word and the regex only looked for one. The regex now
names the national chains whose brand carries none (Empire Today, Lumber Liquidators) and
the exclusion was moved into the publish step, which runs before any file is written; the fourteen
dated files already in the spool were rewritten through it, so no downloadable file contains that
row any more, and a publish-time assertion re-reads every servable file and refuses to publish at
all if a flooring-trade applicant is in one. Deferring this to “the next build”, which is
what this paragraph used to say, was the error; chains whose names do contain a floor word (Floor & Decor,
LL Flooring, 50 Floor) were already caught by the rule above. A chain on neither list can
still slip through, which is why the declared-scope file ships as name-only rows you are
expected to read, not as vetted leads. The count excluded is published in the stats file and
on the home page.applicant_trade_guess exists to warn you that the filer may be a trade sub rather than
the general contractor. Until 2026-07-30 it was a name heuristic only — nine regexes over the
company name (flooring, hvac, electrical, plumbing, roofing, fire-protection, pool, glass-windows,
solar) — and this section said the licence number was absent from the record "by design". It was not
absent. It was on the record and being discarded (see the correction in §1).
The licence number is now read from Miami-Dade's ContractorNumber and ships in two
columns: applicant_license (e.g. CGC1524060) and
applicant_license_class, decoded from the prefix. Because the raw number ships, you can
verify any row against DBPR's own public licensee search without taking our word for the decode.
general-or-unknown rather than blank, because
blank was ambiguous between "we checked and it looks general" and "we have no idea".This page previously called a "DBPR licence-class lookup" the real fix for both the trade guess and for turning the name-only declared-scope rows into callable ones. Both halves were wrong, and the reason is in the statute, not in our code:
CAC Cert Air, CBC Cert
Building, CCC Cert Roofing, CFC Cert Plumbing, CGC Cert
General, CMC Cert Mechanical, CPC Cert Pool, CRC Cert
Residental [DBPR's spelling], CSC Cert Metal, CUC Cert Under,
CVC Cert Solar, PCC Cert Pollutant, SCC Cert Specialty) and
12 registered equivalents (RA RB RC RF RG RM RP RQ RR RS RU RX). Not one is floor
covering. Specialty licensees appear only as the undifferentiated "Cert Specialty" / "Reg Specialty"
with no trade sub-type in the published data. Read for yourself:
www2.myfloridalicense.com/sto/file_download/extracts/cilb_certified.csv and
…/cilb_registered.csv — I streamed both on 2026-07-30 and took the distinct
(prefix, class) pairs. So licence class can rule a filer out as a regulated MEP trade; it
can never rule one in as a flooring contractor. Positive flooring evidence therefore stays
tied to the county's own declared-scope text and to nothing else.ContractorPhone.For the declared-scope rows specifically, the useful fix was never DBPR at all: the City of Miami
record already carried the applicant's mailing address, city and ZIP, and the pipeline was dropping
all three. They now ship as applicant_mail_address / _city /
_zip — populated on 8 of the 8
declared-scope rows in the current run. Those rows are mail-or-door rows with a real address, not
names with nothing.
Every shipped row carries its permit_id, its source and the evidence text. Take any permit ID from the sample page, query the endpoint above for that permit number, and compare the address, valuation, applicant and description against what we shipped. If a row does not match its source, that is a defect and I want to hear about it.
The generated counts themselves live in claims.json, written by the same run that writes the CSVs. The home page reads its numbers from that file, so the page cannot state a count the data does not contain.
Each answer below restates a section above it, in the words a buyer uses rather than the words a pipeline uses. Nothing here is new information; if an answer and a section disagree, the section is the one that is generated and the answer is the one that is wrong.
Two public endpoints, both free and both published in section 1 of this page: Miami-Dade County's ArcGIS permit layer (miamidade_permit_data), which carries a contractor phone number, and the City of Miami's own layer (Building_Permits_Since_2014), which carries the trade scope in its permit text but publishes no phone number on any of its 41 fields. Nothing is scraped from a paywalled source and nothing is bought from a broker.
Yes, and we publish the query. Miami-Dade's permit layer is an open ArcGIS FeatureServer with no key and no licence fee; you can paste a query into a browser and get the same records this product is built from. The step-by-step version, with field meanings, is at /miami-dade-permit-search.html. What EarlyFloor sells is the daily run, the filtering and the published counts, not access to something secret.
A first-match-wins classifier over the permit type and description. The literal patterns are /remodel/, /renovat/, /kitchen/, /bathroom/, /\bbath\b/, /interior build.?out/, /tenant improvement/, /interior alter/ and /\baddition\b/. Trade-specific rules run first, so an A/C changeout is classified hvac and never reaches this feed. Every shipped row carries an evidence column naming the exact pattern that matched and the exact county text it matched in.
The Miami-Dade County layer does, on many records, and that is the only reason this product exists. The City of Miami layer does not — 41 fields, none of them a telephone number — so its rows ship in a separate name-only file with the filer's company mailing address instead. The City of Orlando dataset does publish a phone column - our field map simply never asked for it until 2026-08-19 - and with it read Orlando produced 2 rows in the 2026-08-19 run - one commercial and one whose occupancy that source does not resolve, neither residential - which is why no Orlando feed is sold. An earlier figure of 4 counted two fire-alarm sub-permits that the gate now removes.
Usually not. On most rows you are calling a remodel and asking whether floors are in it. The exception is the declared-floor-scope file: rows whose county text itself matches wood/laminate/tile/vinyl, floor covering, carpet, flooring, tile work or vinyl plank. Those are the strongest rows in the product and they come from the City of Miami, one municipality out of the county's 34 — so a crew working Hialeah or Doral will essentially never get one. The coverage matrix at /coverage.html states this up front.
Six gates, all listed in section 4: rows with fewer than 7 digits of phone go to the name-only file rather than the phone feed; permits valued under $2,500 are dropped unless they carry declared floor scope; implausible valuations are excluded; government-owned properties are excluded; applicants whose own company name contains a floor word are dropped, because we will not sell a flooring contractor a job another flooring contractor already has; and permits are collapsed by project so one building is not sold as several jobs.
Whether the flooring is already subcontracted — nothing in the public record says this, and it is the biggest unknown in permit-based prospecting. Whether a county permit is still open, because the Miami-Dade layer publishes no status field at all. Which trade the work is, since there is no trade code. And what your close rate would be, because EarlyFloor has never had a paying subscriber, so there is no close rate to quote.
Every row carries its county permit ID and the exact county text that classified it. Take any permit ID from /sample.html, query the endpoint published in section 1, and compare digit by digit. That is the intended use of the sample page: verification before payment, not a teaser.
Related: run the Miami-Dade permit query yourself · permit leads vs marketplace leads · what is covered and what is not · a real week of rows.