API · v1

RoofTap Enrichment API

One JSON call. Roof + property + storm data on every address. Same shape every CRM. Pay-as-you-go, never billed for bad reads.

Quickstart

Activate a key in 60 seconds at /integrations/signup. You'll see the key once on the success page — copy it before closing the tab. Every key is prefixed with rt_live_.

curl -X POST https://api.rooftap.app/v1/enrich \
  -H "X-API-Key: rt_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{ "address": "5701 W Loma Ln, Glendale, AZ 85302" }'

Authentication

Every request must include the X-API-Key header. Keys are tied to one billing account. Treat them like passwords: do not embed in client-side JavaScript or commit to git.

Lost your key? Email support@rooftap.app from the address on file and we'll rotate it.

POST /v1/enrich

The primary endpoint. Returns the full enrichment payload synchronously, typical p50 1-2 seconds, p95 under 5 seconds for addresses without an existing cache hit.

Request

FieldTypeDescription
addressstring · requiredFree-form US address. Examples: "123 Main St, Austin, TX 78701", "5701 W Loma Ln Glendale AZ".
lead_idstringYour internal id for the lead. Echoed back in the response so you can correlate without holding state.
owner_match_namestringName on the lead form. We compare against deed-of-record and return property.owner_match.
POST https://api.rooftap.app/v1/enrich
X-API-Key: rt_live_abc123...
Content-Type: application/json

{
  "address": "5701 W Loma Ln, Glendale, AZ 85302",
  "lead_id": "lead-9821",
  "owner_match_name": "Sarah Johnson"
}

Response shape

All four sections (roof, property, storm, data_quality) are returned on every successful call. Numeric fields can be null when source data is missing — never 0 as a fallback.

200 OK · 1.8s

{
  "ok": true,
  "lead_id": "lead-9821",
  "billable": true,
  "address": {
    "input":      "5701 W Loma Ln, Glendale, AZ 85302",
    "formatted":  "5701 W Loma Ln, Glendale, AZ 85302, USA",
    "lat":        33.5601,
    "lng":       -112.1888,
    "place_id":   "ChIJ..."
  },
  "roof": {
    "area_sqft":          4003,
    "squares":            44,
    "predominant_pitch":  "4/12",
    "complexity":         "cut_up",
    "num_facets":         9,
    "linear_measurements": {
      "eaves_ft":   127,
      "rakes_ft":   330,
      "ridges_ft":  130,
      "valleys_ft": 232,
      "hips_ft":    104
    }
  },
  "gutter": {
    "linear_feet":             127,
    "downspout_count_estimate": 4
  },
  "siding": {
    "wall_sqft_estimate": 2540,
    "perimeter_ft":        254,
    "stories":               1
  },
  "solar": {
    "suitability":             "high",
    "max_panel_count":          34,
    "kw_potential":             13.6,
    "annual_kwh_potential":     14820,
    "max_array_area_sqft":      612,
    "sunshine_hours_per_year":  1820,
    "panel_capacity_watts":     400
  },
  "property": {
    "year_built":                 1972,
    "estimated_roof_age_years":   18,
    "owner_match":                true,
    "owner_occupied":             true,
    "lot_size_sqft":              7800,
    "stories":                    1,
    "bedrooms":                   3,
    "bathrooms":                  2,
    "last_sale_date":             "2024-09-12",
    "last_sale_price":            412000,
    "sold_within_12mo":           true
  },
  "storm": {
    "hail_2024":      "1.75in",
    "hail_5yr_count": 4,
    "wind_5yr_max":   "72mph",
    "last_event_date": "2024-04-14",
    "claim_eligible_window": {
      "event_date":     "2024-04-14",
      "expires":        "2026-04-14",
      "days_remaining": 0
    }
  },
  "data_quality": {
    "confidence":      "high",
    "imagery_quality": "HIGH",
    "footprint_match": true
  }
}

Roof fields

FieldTypeDescription
roof.area_sqftnumberTotal roof area, square feet, computed from Solar API rooftop polygon.
roof.squaresnumberRoofing squares (area_sqft / 100). Headline number for roofers.
roof.predominant_pitchstringMost-common pitch across roof facets, format "X/12".
roof.complexityenumOne of `simple` | `moderate` | `cut_up`. Drives waste % + labor estimates.
roof.num_facetsnumberDistinct roof planes detected. Higher = more complex.
roof.linear_measurements.eaves_ftnumberTotal eave length. Drip edge + gutter scope.
roof.linear_measurements.rakes_ftnumberTotal rake length. Drip edge scope.
roof.linear_measurements.ridges_ftnumberTotal ridge length. Ridge cap scope.
roof.linear_measurements.valleys_ftnumberTotal valley length. Underlayment + flashing.
roof.linear_measurements.hips_ftnumberTotal hip length. Hip cap scope.

Gutter fields

FieldTypeDescription
gutter.linear_feetnumberTotal linear feet of gutter scope. Equals the roof's eave length (which is what gutters attach to).
gutter.downspout_count_estimatenumberRule-of-thumb 1 downspout per 35 ft of gutter, with a floor of 2. Adjust against the actual property if you have it.

Siding fields

FieldTypeDescription
siding.wall_sqft_estimatenumberWall surface area in square feet. Building perimeter × (stories × ~10 ft per story). Coarse but in the right ballpark for quoting.
siding.perimeter_ftnumberBuilding perimeter (the drip-edge length of the roof). Use directly for fascia + trim measurements.
siding.storiesnumberAbove-ground story count from property records. Defaults to 1 when records are missing.

Solar fields

FieldTypeDescription
solar.suitabilityenum`high` | `medium` | `low` | `unsuitable`. Single field for lead-aggregator routing. High = $80-150 solar lead; low/unsuitable = roof-only.
solar.max_panel_countnumberMaximum panels that fit on the roof per Google Solar API. Buyers use this for system sizing.
solar.kw_potentialnumberMax system DC capacity in kilowatts. Derived from max_panel_count × panel_capacity_watts.
solar.annual_kwh_potentialnumberEstimated annual generation in kWh at maximum system size. Drives payback + savings calcs.
solar.max_array_area_sqftnumberUsable rooftop area for panels in square feet.
solar.sunshine_hours_per_yearnumberAnnual sun-hours at this latitude/orientation. Lower = lower suitability.
solar.panel_capacity_wattsnumberAssumed per-panel rating used in the math (Google Solar API default, typically 250-400W).

Property fields

FieldTypeDescription
property.year_builtnumberYear the structure was built, per county assessor records.
property.estimated_roof_age_yearsnumberBest-effort roof age. Derived from permit history when available; falls back to year_built.
property.owner_matchbooleantrue when `owner_match_name` from the request matches the deed-of-record. Filters wholesalers + storm-chasers.
property.owner_occupiedbooleantrue when the property's mailing address matches the property address (homestead heuristic).
property.lot_size_sqftnumberParcel size in square feet, from county records.
property.storiesnumberNumber of above-ground stories. Drives labor + safety equipment cost.
property.bedroomsnumberBedroom count, county-assessor data.
property.bathroomsnumberBathroom count, county-assessor data.

Storm fields

FieldTypeDescription
storm.hail_2024stringLargest hail event in calendar 2024 within 1 mile. Format "X.XXin". Empty string if none.
storm.hail_5yr_countnumberHail events ≥1.0in in the last 5 years within 1 mile, per NCEI Storm Events.
storm.wind_5yr_maxstringMax measured wind in the last 5 years within 1 mile. Format "XXmph".
storm.last_event_datestringDate of the most recent qualifying storm event. ISO format (YYYY-MM-DD). Drives the claim-eligible window.
storm.claim_eligible_windowobjectDerived window when a homeowner can still file an insurance claim for damage from the last event. { event_date, expires (event_date + 24mo), days_remaining }. Storm-chaser roofers use this to time outreach.

Contact compliance fields (premium add-on)

FieldTypeDescription
contact_compliance.tcpa_safe_to_callboolean | nullResult of federal + state DNC scrub against the contact phone. Only relevant for outbound cold outreach. Inbound consumer-initiated forms are TCPA-exempt.
contact_compliance.dnc_listedboolean | nullTrue if the phone is on the federal Do-Not-Call list.
contact_compliance.last_checkedstringISO timestamp of the last scrub. Cached 24h to avoid repeat upstream charges.

Quality + meta

FieldTypeDescription
data_quality.confidenceenum`high` | `medium` | `low`. We only bill when confidence is `high` or `medium`.
data_quality.imagery_qualityenum`HIGH` | `MEDIUM` | `LOW`. LOW indicates canopy occlusion or stale imagery.
data_quality.footprint_matchbooleantrue when our roof polygon aligns with the parcel's primary structure footprint.
billablebooleantrue when this call counts toward your monthly usage. false on quality-rejects + 4xx errors.

POST /v1/enrich/prewarm

Optional. Hit this at lead intake to kick off the enrichment fetch in the background. Returns 202 in under 50ms — the next/v1/enrich call on the same address (the one that runs when your routing decision happens) lands a warm cache and returns sub-500ms.

Prewarm calls don't bill. Only the real call does.

POST https://api.rooftap.app/v1/enrich/prewarm
X-API-Key: rt_live_abc123...

{ "address": "5701 W Loma Ln, Glendale, AZ 85302",
  "lead_id":  "lead-9821" }

202 Accepted · <50ms
{ "ok": true, "lead_id": "lead-9821",
  "message": "Prewarm queued. Hit POST /v1/enrich on the same address in 2-6s for a cache hit." }

Errors

All errors return JSON with { ok: false, error, message }. The error field is a stable machine-readable code; message is human-readable and may change between releases.

401 Unauthorized · invalid_key
{ "ok": false, "error": "invalid_key",
  "message": "X-API-Key header missing or unrecognized." }

422 Unprocessable Entity · address_unresolvable
{ "ok": false, "error": "address_unresolvable",
  "message": "Could not geocode the supplied address.",
  "billable": false }

429 Too Many Requests · rate_limited
{ "ok": false, "error": "rate_limited",
  "message": "Rate limit: 10 rps. Retry after 600ms." }
// Honor the Retry-After response header.
HTTPCodeWhat to do
400invalid_jsonBody wasn't valid JSON. Check Content-Type + payload.
400address_requiredMissing address field.
401invalid_keyHeader missing or key revoked. Rotate via support.
402billing_requiredSubscription payment failed. Update card in your billing portal.
422address_unresolvableGeocoder couldn't place the address. Not billed.
422no_solar_coverageAddress is outside Solar API coverage. Not billed.
429rate_limitedHonor Retry-After header. Default cap 10 rps per key.
500internal_errorOur fault. Retry with exponential backoff. Not billed.

Rate limits

  • /v1/enrich — 10 rps per API key. 429 returns a Retry-After header in seconds.
  • /v1/enrich/prewarm — 30 rps per API key (separate bucket).
  • Need higher caps for an aggregator burst? Email support, we lift to 50+ rps once we see your traffic profile.

Billing

Volume tiers are auto-applied for the entire month based on your final volume. Run 16,000 calls in a month and every call that period prices at $2.45 — including the first 5,000.

VolumePer callNotes
0 – 5,000 / mo$3.95Entry tier, no minimum.
5,001 – 15,000 / mo$3.25Auto-applied.
15,001 – 30,000 / mo$2.45Auto-applied.
30,000+ / mo$1.95Email support for >100k contracts.

Quality guarantee

We never bill for bad data. If we can't place the address, can't resolve a roof polygon with confidence, or the underlying imagery is canopy-occluded, the response includes billable: false and that call doesn't count toward your usage. No tickets, no clawbacks, no end-of-month reconciliation.

Changelog

  • 2026-05linear_measurements now includes hips. Storm 5-year wind max added.
  • 2026-04 — Prewarm endpoint launched. Volume tier breakpoints widened.
  • 2026-03owner_match_name request parameter live.
  • 2026-02 — v1 GA. Public launch.
Ship today

60-second self-serve key. Card on file via Stripe.

No NDA. No procurement loop. Cancel any time.

Get an API key →