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
| Field | Type | Description |
|---|---|---|
| address | string · required | Free-form US address. Examples: "123 Main St, Austin, TX 78701", "5701 W Loma Ln Glendale AZ". |
| lead_id | string | Your internal id for the lead. Echoed back in the response so you can correlate without holding state. |
| owner_match_name | string | Name 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
| Field | Type | Description |
|---|---|---|
| roof.area_sqft | number | Total roof area, square feet, computed from Solar API rooftop polygon. |
| roof.squares | number | Roofing squares (area_sqft / 100). Headline number for roofers. |
| roof.predominant_pitch | string | Most-common pitch across roof facets, format "X/12". |
| roof.complexity | enum | One of `simple` | `moderate` | `cut_up`. Drives waste % + labor estimates. |
| roof.num_facets | number | Distinct roof planes detected. Higher = more complex. |
| roof.linear_measurements.eaves_ft | number | Total eave length. Drip edge + gutter scope. |
| roof.linear_measurements.rakes_ft | number | Total rake length. Drip edge scope. |
| roof.linear_measurements.ridges_ft | number | Total ridge length. Ridge cap scope. |
| roof.linear_measurements.valleys_ft | number | Total valley length. Underlayment + flashing. |
| roof.linear_measurements.hips_ft | number | Total hip length. Hip cap scope. |
Gutter fields
| Field | Type | Description |
|---|---|---|
| gutter.linear_feet | number | Total linear feet of gutter scope. Equals the roof's eave length (which is what gutters attach to). |
| gutter.downspout_count_estimate | number | Rule-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
| Field | Type | Description |
|---|---|---|
| siding.wall_sqft_estimate | number | Wall surface area in square feet. Building perimeter × (stories × ~10 ft per story). Coarse but in the right ballpark for quoting. |
| siding.perimeter_ft | number | Building perimeter (the drip-edge length of the roof). Use directly for fascia + trim measurements. |
| siding.stories | number | Above-ground story count from property records. Defaults to 1 when records are missing. |
Solar fields
| Field | Type | Description |
|---|---|---|
| solar.suitability | enum | `high` | `medium` | `low` | `unsuitable`. Single field for lead-aggregator routing. High = $80-150 solar lead; low/unsuitable = roof-only. |
| solar.max_panel_count | number | Maximum panels that fit on the roof per Google Solar API. Buyers use this for system sizing. |
| solar.kw_potential | number | Max system DC capacity in kilowatts. Derived from max_panel_count × panel_capacity_watts. |
| solar.annual_kwh_potential | number | Estimated annual generation in kWh at maximum system size. Drives payback + savings calcs. |
| solar.max_array_area_sqft | number | Usable rooftop area for panels in square feet. |
| solar.sunshine_hours_per_year | number | Annual sun-hours at this latitude/orientation. Lower = lower suitability. |
| solar.panel_capacity_watts | number | Assumed per-panel rating used in the math (Google Solar API default, typically 250-400W). |
Property fields
| Field | Type | Description |
|---|---|---|
| property.year_built | number | Year the structure was built, per county assessor records. |
| property.estimated_roof_age_years | number | Best-effort roof age. Derived from permit history when available; falls back to year_built. |
| property.owner_match | boolean | true when `owner_match_name` from the request matches the deed-of-record. Filters wholesalers + storm-chasers. |
| property.owner_occupied | boolean | true when the property's mailing address matches the property address (homestead heuristic). |
| property.lot_size_sqft | number | Parcel size in square feet, from county records. |
| property.stories | number | Number of above-ground stories. Drives labor + safety equipment cost. |
| property.bedrooms | number | Bedroom count, county-assessor data. |
| property.bathrooms | number | Bathroom count, county-assessor data. |
Storm fields
| Field | Type | Description |
|---|---|---|
| storm.hail_2024 | string | Largest hail event in calendar 2024 within 1 mile. Format "X.XXin". Empty string if none. |
| storm.hail_5yr_count | number | Hail events ≥1.0in in the last 5 years within 1 mile, per NCEI Storm Events. |
| storm.wind_5yr_max | string | Max measured wind in the last 5 years within 1 mile. Format "XXmph". |
| storm.last_event_date | string | Date of the most recent qualifying storm event. ISO format (YYYY-MM-DD). Drives the claim-eligible window. |
| storm.claim_eligible_window | object | Derived 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)
| Field | Type | Description |
|---|---|---|
| contact_compliance.tcpa_safe_to_call | boolean | null | Result 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_listed | boolean | null | True if the phone is on the federal Do-Not-Call list. |
| contact_compliance.last_checked | string | ISO timestamp of the last scrub. Cached 24h to avoid repeat upstream charges. |
Quality + meta
| Field | Type | Description |
|---|---|---|
| data_quality.confidence | enum | `high` | `medium` | `low`. We only bill when confidence is `high` or `medium`. |
| data_quality.imagery_quality | enum | `HIGH` | `MEDIUM` | `LOW`. LOW indicates canopy occlusion or stale imagery. |
| data_quality.footprint_match | boolean | true when our roof polygon aligns with the parcel's primary structure footprint. |
| billable | boolean | true 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.| HTTP | Code | What to do |
|---|---|---|
| 400 | invalid_json | Body wasn't valid JSON. Check Content-Type + payload. |
| 400 | address_required | Missing address field. |
| 401 | invalid_key | Header missing or key revoked. Rotate via support. |
| 402 | billing_required | Subscription payment failed. Update card in your billing portal. |
| 422 | address_unresolvable | Geocoder couldn't place the address. Not billed. |
| 422 | no_solar_coverage | Address is outside Solar API coverage. Not billed. |
| 429 | rate_limited | Honor Retry-After header. Default cap 10 rps per key. |
| 500 | internal_error | Our fault. Retry with exponential backoff. Not billed. |
Rate limits
- /v1/enrich — 10 rps per API key. 429 returns a
Retry-Afterheader 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.
| Volume | Per call | Notes |
|---|---|---|
| 0 – 5,000 / mo | $3.95 | Entry tier, no minimum. |
| 5,001 – 15,000 / mo | $3.25 | Auto-applied. |
| 15,001 – 30,000 / mo | $2.45 | Auto-applied. |
| 30,000+ / mo | $1.95 | Email support for >100k contracts. |
Quality guarantee
billable: false and that call doesn't count toward your usage. No tickets, no clawbacks, no end-of-month reconciliation.Changelog
- 2026-05 —
linear_measurementsnow includes hips. Storm 5-year wind max added. - 2026-04 — Prewarm endpoint launched. Volume tier breakpoints widened.
- 2026-03 —
owner_match_namerequest parameter live. - 2026-02 — v1 GA. Public launch.
60-second self-serve key. Card on file via Stripe.
No NDA. No procurement loop. Cancel any time.