DEV Community

Loomwise
Loomwise

Posted on

Florida Roofing Permit Leads: 900+ Re-Roof Permits a Month from Miami-Dade and Orlando Open Data (Python)

Florida replaces a lot of roofs. Hurricanes, insurance companies that won't cover roofs older than 15 years, and the state's strict building code all mean a steady flow of re-roof permits, and Florida counties publish those permits as open data.

A new roofing permit is a lead for gutter and soffit installers, solar companies (the best time to add panels is right after a new roof), insulation contractors, and roofing suppliers who want to know which contractors are busiest.

This post shows how to pull every roofing permit issued in the last 30 days in Miami-Dade County and Orlando with one Python script. No login, no scraping, no API key.

Run on Oct 22, 2026:

  • Miami-Dade County (unincorporated): 819 roofing permits in 30 days
  • Orlando: 84 roofing permits in 30 days
  • Busiest Miami-Dade roofers: T & S Roofing Systems (26 permits), AZ Contracting of South Florida (15), Big G Roofing & More (9) and T & E Roofing (9). Another 22 were pulled by the owner.

The data sources

Area Platform Dataset
Miami-Dade County ArcGIS REST Building permit data
City of Orlando Socrata Permit applications

Two different platforms, so two small functions. ArcGIS uses SQL-style where clauses and pages through results with resultOffset. Socrata uses SoQL.

The script

import requests
from datetime import date, datetime, timedelta, timezone
DAYS = 30
since = date.today() - timedelta(days=DAYS)

def orlando():
    # Socrata (SoQL): City of Orlando permits
    return requests.get("https://data.cityoforlando.net/resource/ryhf-m453.json", params={
        "$select": "permit_number,issue_permit_date,permit_address,project_name,worktype,estimated_cost,contractor_name",
        "$where": f"issue_permit_date >= '{since}' AND (upper(project_name) like '%ROOF%' OR upper(application_type) like '%ROOF%')",
        "$order": "issue_permit_date DESC", "$limit": 5000}, timeout=120).json()

def miami_dade():
    # ArcGIS REST: Miami-Dade County permits (unincorporated area)
    url = "https://services.arcgis.com/8Pc9XBTAsYuxx9Ny/arcgis/rest/services/miamidade_permit_data/FeatureServer/0/query"
    out, offset = [], 0
    while True:
        js = requests.get(url, params={
            "where": f"PermitIssuedDate >= DATE '{since}' AND (UPPER(DetailDescriptionComments) LIKE '%ROOF%' OR UPPER(ProposedUseDescription) LIKE '%ROOF%')",
            "outFields": "PermitNumber,PermitIssuedDate,PropertyAddress,DetailDescriptionComments,EstimatedValue,ContractorName",
            "resultOffset": offset, "resultRecordCount": 1000, "f": "json"}, timeout=120).json()
        feats = [f["attributes"] for f in js.get("features", [])]
        for a in feats:  # "date" fields come back as epoch ms; "date only" fields as text
            d = a["PermitIssuedDate"]
            if isinstance(d, (int, float)):
                a["PermitIssuedDate"] = datetime.fromtimestamp(d / 1000, timezone.utc).date().isoformat()
        out += feats
        if not js.get("exceededTransferLimit"): return out
        offset += len(feats)

o, m = orlando(), miami_dade()
print(f"Orlando: {len(o)} roofing permits in {DAYS} days")
print(f"Miami-Dade: {len(m)} roofing permits in {DAYS} days")
from collections import Counter
print("Top Miami-Dade roofing contractors:", Counter(a.get("ContractorName") for a in m if a.get("ContractorName")).most_common(5))
Enter fullscreen mode Exit fullscreen mode

Three things that will trip you up

  1. ArcGIS dates come in two types. Regular date fields come back as epoch milliseconds, but newer "date only" fields (like Miami-Dade's PermitIssuedDate) come back as text. The script handles both. Check the layer's field list at .../FeatureServer/0?f=json when you add a new county.
  2. ArcGIS caps each page. If exceededTransferLimit is true, there are more rows, so keep paging with resultOffset. Forget this and you'll silently get only the first 1,000 or 2,000 rows.
  3. "OWNER" isn't a contractor. Owner-builder permits list OWNER as the contractor. Drop them if you're building a contractor list. Keep them if you sell to homeowners doing their own work.

Turning it into a lead list

  • Solar and gutters: target the addresses of finished re-roofs, since homeowners just spent money on the roof and are thinking about the house.
  • Suppliers and software: group by ContractorName to see who pulls the most permits, then sell to the busiest 50.
  • Storm response: after a storm, run with DAYS = 7 daily to watch permit volume spike by area.

Optional: I run this as a hosted, scheduled search: Florida Roofing Permits (ready-made search) (paid per result). Disclosure: it's my tool. The script above does the same job for free.

Top comments (0)