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://www.rooftap.app/api/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.appfrom the address on file and we'll rotate it.
Three products, one endpoint
The tier body param selects the product. What's free and what bills is never ambiguous:
| tier | What you get | Price |
|---|---|---|
lite (default) | Satellite-measured roof area, predominant pitch, squares, confidence flag. Main structure only — see Multiple structures. | Free · unlimited — no card required. A bare call can never surprise-bill. |
full | The complete record: everything in lite plus owner & property details, storm history + claim window, AI roof condition, material takeoff + cost estimate, solar, gutter, risk & neighborhood, and the shareable report link. area_sqft is still the main structure; additional structures are added via verify_url. | $3.95/record (sliding to $1.95 at volume). First 3 full records free on every key (trial_calls_remaining in the payload); keys without billing get a 402 after that. |
premium | Vendor-verified, ordering-grade measurements — full per-edge lengths (eaves / rakes / ridges / hips / valleys), guaranteed. | $13.95 flat — coming soon (currently returns 501 premium_not_yet_available). |
// tier omitted -> LITE. Free, unlimited, on any key.
POST /v1/enrich { "address": "5701 W Loma Ln, Glendale, AZ 85302" }
200 OK
{
"ok": true,
"billable": false,
"tier": "lite",
"roof": {
"area_sqft": 4003,
"predominant_pitch": "4/12",
"squares": 44
},
"formatted_address": "5701 W Loma Ln, Glendale, AZ 85302, USA",
"data_quality": { "confidence": "high" }
}How to read our accuracy labels. Roof area and pitch are satellite measurements, benchmark-validated, and carry the data_quality.confidence score (high / medium / low). Derived numbers (cost band, takeoff quantities, accessories, roof-age) are labeled estimates in their field docs, and records data (owner, year built, sale history) is sourced from our property-records provider. You are billed only on high-confidence reads. medium and low both return 422 and are never billed— we measured the property but won't charge for a number we can't stand behind (imagery quality, geocode precision, or a footprint cross-check fell short). Benchmarked against 41 HOVER reports through this endpoint: high lands at 4.7% median error, 87% within ±10%, while medium runs 17.6% median with 1-in-5 orderable. tier:"lite" still returns those measurements free, so you keep the estimate without paying for it.
Multiple structures
area_sqft is always the main structure. Not the whole property. A detached garage, shop, or casita is not included in that number, on either tier.
This is deliberate. Deciding which nearby buildings belong to a property cannot be done reliably from satellite data — a large outbuilding on this lot and a smaller house next door look identical on both size and distance. Rather than guess and silently over- or under-measure, we measure the main structure and tell you when we can see others.
Every response carries data_quality.structure_scope, which is currently always "primary_only". When other structures are detected nearby:
| Field | Tier | Meaning |
|---|---|---|
data_quality.additional_structures_possible | lite | true when at least one other structure looks like it may belong to this property. Boolean only — lite stays thin. |
data_quality.nearby_structures_detected | full | Count of structures that may belong to this property and are not in area_sqft. |
data_quality.nearby_structures_note | full | Human-readable summary — count, distance to nearest, and total footprint — suitable for showing a contractor verbatim. |
verify_url | full | Where a contractor adds the missing structures. Opens the report with draw tools; measurements added there update the record. |
// tier:"full" on a property with a detached building
{
"roof": { "area_sqft": 2988, "predominant_pitch": "4/12", "squares": 34 },
"verify_url": "https://www.rooftap.app/r/9f2c...",
"data_quality": {
"confidence": "high",
"structure_scope": "primary_only",
"nearby_structures_detected": 1,
"nearby_structures_note": "1 additional structure detected nearby (nearest 13m,
1,589 sqft of footprint) that may belong to this property. This measurement
covers the MAIN STRUCTURE ONLY — review the aerial and add any additional
structures before ordering material."
}
}
// tier omitted -> lite, same property
{
"roof": { "area_sqft": 2988, "predominant_pitch": "4/12", "squares": 34 },
"report_link": "https://www.rooftap.app/r/9f2c...",
"data_quality": {
"confidence": "high",
"structure_scope": "primary_only",
"additional_structures_possible": true
}
}verify_urlthrough to whoever is quoting the job, and surface the flag when it's set. A roofer who orders material from a main-structure number on a property with a detached garage will be short. The flag plus the link is how they catch it before ordering — we can tell them a structure is there, but only a human can confirm it belongs to the job.Storm data
Every enrich response carries a storms block. By default it's just the last event — storms.last_storm (date, type, hail size, wind, distance) plus the derived storms.claim_eligible_window. That's the answer to "when did the last storm hit this address, how big, and is the claim window still open" in one cheap call. The window we search is 2 years, which matches the ~24-month insurance claim window — older events aren't actionable.
Need the full history? Pass include: ["storm_history"] and the block expands to the complete 2-year event list (events[], newest first) plus max_hail_inches, max_wind_mph, lookback_years, and radius_miles.
// DEFAULT — last event only
POST /v1/enrich
{ "address": "5701 W Loma Ln, Glendale, AZ 85302" }
"storms": {
"last_storm": {
"date": "2025-09-18", "type": "hail",
"hail_in": 1.75, "wind_mph": null, "distance_miles": 2.5
},
"claim_eligible_window": {
"event_date": "2025-09-18", "expires": "2027-09-18", "days_remaining": 463
}
}
// OPT IN — full 2-year history
POST /v1/enrich
{ "address": "5701 W Loma Ln, Glendale, AZ 85302",
"include": ["storm_history"] }
"storms": {
"last_storm": { ... },
"claim_eligible_window": { ... },
"events": [
{ "noaa_event_id": "...", "event_type": "Hail", "event_date": "2025-09-18",
"hail_size_inches": 1.75, "wind_speed_mph": null, "city": "Glendale",
"state_abbr": "AZ", "distance_miles": 2.5, "damage_description": null }
],
"max_hail_inches": 1.75, "max_wind_mph": 72,
"lookback_years": 2, "radius_miles": 10
}tier: "full" calls (lite carries no storm data), and a full call that narrows to "include": ["storms"] bills like any other. Your key's 3 free full records cover integration testing. For screening a list of addresses against storm history, use Bulk reactivation — that's priced per-address for exactly that job.POST /v1/enrich
The primary endpoint. Returns the full enrichment payload synchronously. Cold call latency (first time we see an address) is typically 4-7 seconds, Solar API + property + storm history fetched in parallel. Cached addresses (same parcel called twice within the 7-day cache TTL) return sub-second. If you need to keep enrichment off your hot path, use the prewarm endpoint.
Request
| Field | Type | Description |
|---|---|---|
| address | string · required | Free-form US address. Examples: "123 Main St, Austin, TX 78701", "5701 W Loma Ln Glendale AZ". |
| tier | "lite" | "full" | "premium" | Defaults to lite (free). Pass "full" for the complete $3.95 record; "premium" is coming soon. See Three products, one endpoint. |
| lead_id | string | Your internal id for the lead. Echoed back in the response so you can correlate without holding state. |
| lead_date | string (YYYY-MM-DD) | Date the lead was generated. When present, the response adds a reactivation flag, true if a qualifying storm hit since that date. |
| include | string[] | Narrow which blocks come back. Defaults to ["roof", "property", "imagery", "storms", "solar"]. Pass a subset to skip work you don't need. Two storm-specific tokens: storms returns the storms block (just the last event by default), and storm_history expands it to the full 2-year event list — see Storm data. Narrowing include trims the response, not the price — every call (including ["storms"]) bills the per-record rate once your trial allotment is used. |
POST https://www.rooftap.app/api/v1/enrich
X-API-Key: rt_live_abc123...
Content-Type: application/json
{
"address": "5701 W Loma Ln, Glendale, AZ 85302",
"lead_id": "lead-9821"
}
// No "tier" -> LITE. Free, unlimited, never billed.
// Use this for integration testing and bulk address validation.To get the complete billable record, add tier: "full":
POST https://www.rooftap.app/api/v1/enrich
X-API-Key: rt_live_abc123...
Content-Type: application/json
{
"address": "5701 W Loma Ln, Glendale, AZ 85302",
"tier": "full", // <-- BILLS $3.95 PER CALL
"lead_id": "lead-9821"
}tier: "full" bills $3.95 per callonce your 3 trial records are used, and there is no further prompt — every call after that is a charge. If you're testing an integration, validating an address list, or running anything in bulk for the first time, leave tier out and use lite — free and unlimited. Your remaining trial records, month-to-date charges, and spend caps are all on your API key page.Response shape
The field tables below document the full tier (tier: "full") — roof, property, solar, imagery, storms, risk (FEMA), neighborhood (Census), reactivation (when lead_date is sent), and data_quality. Numeric fields can be null when source data is missing, never 0 as a fallback.
200 OK
{
"ok": true,
"lead_id": "lead-9821",
"billable": true,
"roof": {
"area_sqft": 4003,
"predominant_pitch": "4/12",
"complexity": "cut_up",
"num_facets": 9,
"linear_measurements": {
"drip_edge_ft": 457
},
"material_takeoff": {
"squares": 44,
"shingle_bundles": 132,
"underlayment_rolls": 5,
"nails": 14080,
"accessories_estimate": {
"cost_usd_low": 1100,
"cost_usd_high": 1850,
"includes": ["ice & water shield", "starter strip", "ridge cap", "drip edge"],
"note": "Estimated from roof geometry — verify quantities on-site before ordering."
}
},
"material_assumption": {
"material": "Concrete tile",
"basis_source": "regional_default_for_AZ"
},
"ai_condition": {
"material": "tile",
"ai_est_roof_age_years": 18,
"ai_est_condition": "fair",
"replace_within_3yr": "medium",
"ai_roof_comments": "Uniform terracotta tile with mild fading...",
"basis": "aerial_vision+year_built",
"model": "gemini-2.5-flash"
}
},
"gutter": {
"linear_feet": 127,
"downspout_count_estimate": 4
},
"estimated_bid_range_usd": {
"primary": { "low": 18400, "high": 27600, "basis_material": "Concrete tile" }
},
"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
},
"imagery": {
"lat": 33.5601,
"lng": -112.1888,
"formatted_address": "5701 W Loma Ln, Glendale, AZ 85302, USA",
"imagery_date": "2025-03-14"
},
"property": {
"owner_name": "JOHNSON FAMILY TRUST",
"year_built": 1972,
"roof_age_estimate": { "years": 18, "basis": "estimated_cycle", "lifecycle_phase": "mid_life" },
"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
},
"storms": {
"last_storm": {
"date": "2025-09-18",
"type": "hail",
"hail_in": 1.75,
"wind_mph": null,
"distance_miles": 2.5
},
"claim_eligible_window": {
"event_date": "2025-09-18",
"expires": "2027-09-18",
"days_remaining": 463
}
},
"verify_url": "https://www.rooftap.app/r/a1b2c3d4e5f6a7b8",
"data_quality": {
"confidence": "high",
"imagery_quality": "HIGH",
"polygon_source": "msbf",
"refinement_recommended": false
}
}Roof fields
| Field | Type | Description |
|---|---|---|
| roof.area_sqft | number | Total roof area, square feet. Satellite-measured (Google Solar) and reconciled against the building footprint polygon — our most accurate measurement (±6% median on our benchmark set). |
| roof.predominant_pitch | string | Most-common pitch across roof facets, format "X/12". Satellite-measured; benchmark-validated. |
| roof.complexity | enum | One of `simple` | `moderate` | `cut_up`. Heuristic tier from facet count + pitch — directional, not a measurement. |
| roof.num_facets | number | Distinct roof planes detected by satellite. Treat as approximate (segmentation can split or merge planes); useful as a complexity signal, not an exact count. |
| roof.linear_measurements.drip_edge_ft | number | Building outline perimeter, eaves + rakes summed. Drives drip-edge material quantity. Auto-mode reports ship this field only; per-edge breakdowns (eaves/rakes/ridges/hips/valleys) are surfaced when the contractor manually labels edges on the verify page (HIGH-confidence override). |
| roof.material_takeoff.squares | number | Roofing squares including recommended waste. Headline number for roofers; rides on area_sqft, so it inherits measurement-grade accuracy. |
| roof.material_takeoff.shingle_bundles / underlayment_rolls / nails | number | Area-driven quantities (3 bundles/square, coverage-standard rolls, ~320 nails/square). |
| roof.material_takeoff.accessories_estimate | object | null | ONE bundled dollar band for the edge-driven accessories (ice & water shield, starter strip, ridge cap, drip edge): { cost_usd_low, cost_usd_high, includes[], note }. Bundled deliberately — itemized per-edge quantities aren't satellite-measurable to ordering grade, so we ship a stable cost band and you verify quantities on the roof. |
| roof.material_assumption | object | The material the cost math assumed: { material, basis_source } — property-records roof_type when known, else the regional default. |
| roof.ai_condition | object | null | AI visual assessment of the roof from aerial imagery + records: { material, ai_est_roof_age_years, ai_est_condition (new|good|fair|worn|poor), replace_within_3yr (low|medium|high), ai_roof_comments, basis, model }. This is an AI ESTIMATE (the basis field says exactly what went in) — not a measurement, not permit data. Strongest for material ID and coarse condition; omitted when the model can't produce a usable read. |
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. |
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.owner_name | string | null | Owner of record, sourced from our property-records provider. We surface the recorded name rather than a match boolean — legal names, trusts, and LLCs make automated name-matching unreliable; eyeball it against your lead. |
| property.year_built | number | Year the structure was built. Sourced from our property-records provider (Rentcast). |
| property.roof_age_estimate | object | { years, basis, year_built, lifecycle_phase }. An ESTIMATE derived from year_built and typical re-roof cycles — the basis field says which. Not permit data. For a visual read of the actual roof, see roof.ai_condition. |
| property.owner_occupied | boolean | null | Owner-occupancy flag as reported by the property-records provider. |
| property.lot_size_sqft | number | Parcel size in square feet, from property records. |
| property.stories | number | Number of above-ground stories. Drives labor + safety equipment cost. |
| property.bedrooms | number | Bedroom count, from property records. |
| property.bathrooms | number | Bathroom count, from property records. |
Storm fields
| Field | Type | Description |
|---|---|---|
| storms.last_storm | object | null | The single most recent NOAA storm event within 10 miles, returned by default on every call: { date (YYYY-MM-DD), type (`hail` | `wind` | `tornado` | `hurricane` | `tropical storm` | `winter storm` | `ice storm`), hail_in, wind_mph, distance_miles }. `null` when there's no qualifying event in the 2-year window. `winter storm` (snow load, blizzards) and `ice storm` (freezing-rain accretion) events never carry hail_in/wind_mph — the damage mechanism is ice/snow load, not hail or wind. They come from confirmed NCEI records scoped to an NWS forecast zone, so distance_miles is measured to that zone's centroid rather than to a point observation. |
| storms.claim_eligible_window | object | null | 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 }. `null` when there's no qualifying event. Storm-chaser roofers use this to time outreach. |
| storms.events | array | Full event list, newest first (max 50). Returned ONLY when you pass include=["storm_history"]. Each: { noaa_event_id, event_type, event_date, hail_size_inches, wind_speed_mph, city, state_abbr, distance_miles, damage_description }. |
| storms.max_hail_inches | number | null | Largest hail diameter across the 2-year window, in inches. Included with `storm_history`. `null` if no hail on record. |
| storms.max_wind_mph | number | null | Highest wind gust across the 2-year window, in mph. Included with `storm_history`. `null` if none on record. |
| storms.lookback_years | number | Years of NOAA history searched. Currently 2. Included with `storm_history`. |
| storms.radius_miles | number | Search radius around the subject property, in miles. Currently 10. Included with `storm_history`. |
Quality + meta
| Field | Type | Description |
|---|---|---|
| verify_url | string | null | Shareable link to a hosted, presentable measurement report for this exact lookup — roof wireframe, measurements, solar, storm history and property details, with a one-click PDF download. Returned on every successful call (trial calls included); attach it to a CRM deal or job, or hand it to the homeowner. Link lives at `https://www.rooftap.app/r/<token>` and stays live ~30 days. `null` only on the rare best-effort issuance failure. See Measurement report below. |
| data_quality.imagery_quality | enum | `HIGH` | `MEDIUM` | `LOW`. LOW indicates canopy occlusion or stale imagery. |
| data_quality.polygon_source | string | null | Which building-footprint source anchored the measurement (`msbf` | `osm` | `manual_override`), or null when no polygon was available. |
| billable | boolean | true when this call counts toward your monthly usage. false on lite calls (always free), quality-rejects, 4xx errors, and trial calls (every key ships with 3 free full records; the response includes trial_calls_remaining while they last). |
| trial_calls_remaining | number | Present on trial calls only — how many free calls are left on this key. Once it hits zero, calls bill normally. |
Measurement report
Every billable call returns a verify_url — a shareable link to a hosted, presentable measurement report for that exact lookup. No need to render the JSON yourself: the page shows the roof wireframe, area / facets / pitch, edge measurements, solar potential, storm history and property details, with a one-click PDF download. It's the same report viewer homeowners see, served at https://www.rooftap.app/r/<token>.
"verify_url": "https://www.rooftap.app/r/a1b2c3d4e5f6a7b8"
Drop it straight onto a CRM deal or job (e.g. a JobTread job or a HubSpot deal property), or hand it to the homeowner. The link stays live for about 30 days — long enough to survive a typical lead-to-close cycle. On a medium/low-confidence read (data_quality.refinement_recommended: true) the page also lets the contractor refine the building selection; high-confidence reads get the link too, with nothing to refine. The field is null only on the rare best-effort issuance failure — re-call to get a fresh one.
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://www.rooftap.app/api/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." }Bulk reactivation
Run an aged lead list back through NOAA storm history to find the homeowners worth a call today. This is storm-only screening(NOAA storm history + a worth-a-call-today flag + optional ongoing next-storm monitoring), not a full enrich. It's async + batched: you enqueue a job and poll for results (or wait for a webhook). Use it to wake a dormant CRM, not to measure a roof, when you need measurements use /v1/enrich.
POST /v1/reactivate
Enqueue an async batch of up to 1,000 addresses per request. Authenticate with the same X-API-Key header you use for /v1/enrich. Returns 202 immediately with a job_id and a status_url to poll. For a 50k+ list, split into chunks of 1,000 and POST each chunk, every chunk returns its own job_id.
| Field | Type | Description |
|---|---|---|
| addresses | array · required | Up to 1,000 entries. Each entry is either a free-form address string, or an object { address, external_ref? } where external_ref is echoed back so you can correlate to your CRM. |
| monitor | boolean | Default true. When true, every address is enrolled in ongoing next-storm monitoring so you're alerted the next time a qualifying event lands. |
| string | Optional. We email this address a summary when the job finishes. | |
| webhook_url | string | Optional. https only. We POST a completion event here when the job finishes (see below). |
curl -X POST https://www.rooftap.app/api/v1/reactivate \
-H "X-API-Key: rt_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"addresses": [
"5701 W Loma Ln, Glendale, AZ 85302",
{ "address": "1804 E Vista Ave, Phoenix, AZ 85020", "external_ref": "lead-9821" }
],
"monitor": true,
"email": "ops@yourcompany.com",
"webhook_url": "https://yourcompany.com/hooks/rooftap"
}'
202 Accepted
{
"ok": true,
"job_id": "rjb_7f3c9a2e",
"queued": 2,
"status": "queued",
"status_url": "https://www.rooftap.app/api/v1/reactivate/rjb_7f3c9a2e",
"message": "Batch queued. Poll status_url or wait for the completion webhook."
}The 202 body carries ok, job_id, queued (count accepted this request), status ("queued"), status_url, and a message.
Errors specific to this endpoint:
| HTTP | Code | What to do |
|---|---|---|
| 401 | invalid_key | Header missing or key unrecognized. Same key as /v1/enrich. |
| 402 | upgrade_required | Free keys get 1,000 addresses free for the lifetime of the key. Past that you must upgrade to continue at $0.01/address. |
| 400 | chunk_too_large | More than 1,000 addresses in one request. Split into chunks of 1,000 and POST each. |
| 429 | rate_limited | Max 60 batches per minute per key. Honor Retry-After. |
GET /v1/reactivate/{job_id}
Poll job status and pull results. Authenticate with X-API-Key, you can only read your own jobs. status moves queued → processing → completed (or failed).
| Query param | Type | Description |
|---|---|---|
| include | string | summary returns just the summary block and omits results. |
| status | string | follow_up filters results to hot leads only (the worth-a-call-today addresses). |
| limit | number | Page size. Max 1,000, default 500. |
| offset | number | Result offset for paging. |
curl https://www.rooftap.app/api/v1/reactivate/rjb_7f3c9a2e?status=follow_up \
-H "X-API-Key: rt_live_abc123..."
200 OK
{
"ok": true,
"job_id": "rjb_7f3c9a2e",
"status": "completed",
"summary": {
"total": 2,
"processed": 2,
"follow_ups": 1,
"monitored": 2,
"errors": 0
},
"results": [
{
"address": "5701 W Loma Ln, Glendale, AZ 85302",
"external_ref": null,
"status": "done",
"matched": true,
"last_event": "2026-05-26",
"max_hail_in": 1.75,
"events_5yr": 4,
"worth_following_up": true,
"trigger": { "date": "2026-05-26", "type": "hail", "hail_in": 1.75, "wind_mph": null },
"days_since_last_storm": 8,
"claim_window_days": 365,
"within_claim_window": true,
"priority": 1,
"monitored": true
}
],
"paging": { "limit": 500, "offset": 0, "returned": 1 }
}The response carries ok, job_id, status, a summary block (total, processed, follow_ups, monitored, errors), the results array, and paging (limit, offset, returned).
Reactivation result fields
| Field | Type | Description |
|---|---|---|
| address | string | The address as supplied in the request. |
| external_ref | string | null | Your reference, echoed back when you passed the object form `{ address, external_ref }`. `null` for bare-string entries. |
| status | enum | `done` once processed, or `error` if the address couldn't be checked. Use `worth_following_up` for the hot-lead flag, not this. |
| matched | boolean | true when the address geocoded and matched a parcel with NOAA storm history. |
| last_event | string | null | Date of the most recent qualifying storm event near this address. ISO format (YYYY-MM-DD), `null` if none. |
| max_hail_in | number | null | Largest hail diameter in inches across the screened history. `null` if no hail on record. |
| events_5yr | number | Count of qualifying storm events in the last 5 years within range of this address. |
| worth_following_up | boolean | The headline flag. true when a storm since the lead date makes this a worth-a-call-today reactivation candidate. |
| trigger | object | null | The storm that flagged the address: `{ date, type, hail_in, wind_mph }`. `null` when `worth_following_up` is false. |
| days_since_last_storm | number | null | Whole days since the most recent severe (qualifying) storm. `null` when there's no qualifying storm on record. |
| claim_window_days | number | The address state's property-claim window in days, used to rank this lead. Defaults to 730 (2 years) when the state can't be determined. |
| within_claim_window | boolean | Whether the most recent storm is still inside that claim window. |
| priority | number | null | Call-priority, 1 = call first through 5 = floor (lowest). `null` when there's no qualifying storm. Results are returned sorted by `priority` ascending (1 first). |
| monitored | boolean | true when this address was enrolled in ongoing next-storm monitoring (driven by the request `monitor` flag). |
priority is a call-ordering heuristic, not legal or insurance advice. We rank fresher storms higher so your team works the list top-down. The scale:
- 1 - severe storm 14 days ago or less.
- 2 - 15 to 90 days ago.
- 3 - 91 to 365 days ago.
- 4 - 366 days up to the state's claim window (only reachable in 2-year states).
- 5 - past the state's claim window, or older than 2 years. Still listed, just the lowest rank.
The claim window is per US state (roughly 1 to 2 years) and is a best-effort heuristic. When we can't determine the state we default to 2 years (claim_window_days: 730). Leads are never dropped: anything past the window simply floors to priority 5.
Completion webhook
Optional. If you set webhook_urlon the enqueue call, we POST a completion event there when the job finishes, so you don't have to poll.
POST https://yourcompany.com/hooks/rooftap
Content-Type: application/json
{
"event": "reactivation.completed",
"job_id": "rjb_7f3c9a2e",
"summary": {
"total": 50000,
"processed": 50000,
"errors": 0,
"follow_ups": 3142,
"monitored": 50000
}
}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. |
| 422 | medium_confidence_property | New. We measured it but won't charge — imagery quality, geocode precision, or a footprint cross-check fell short. Not billed. tier:"lite" returns the same measurement free. confidence_reasons explains which check failed. |
| 422 | low_confidence_property | Heavy canopy, stale imagery, or footprint mismatch. Not billed. |
| 429 | lite_daily_cap_reached | New. Free lite calls are capped per key per rolling 24h. Lite runs the full measurement pipeline, so it is free but not unlimited. Running legitimate volume? Email support and we'll raise it. |
| 429 | monthly_call_cap_reached | New. The key's monthly call cap is now enforced across every tier, including free lite. calls_used and call_cap are in the body. |
| 402 | spend_cap_reached | Monthly spend cap hit. Raise it on your account page, or use tier:"lite", which is always free. |
| 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 billable calls in a month and every one that period prices at $2.45, including the first 5,000. Trial-allotment calls and quality-rejected reads (billable: false) don't count toward your volume or your bill.
Each billable call is charged independently, including repeat lookups of the same address. Results are licensed for the end customer whose lookup triggered the call: don't store and re-serve our data to other third parties in place of a fresh call. One end-customer lookup, one call.
| 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: falseand that call doesn't count toward your usage. No tickets, no clawbacks, no end-of-month reconciliation.Changelog
- 2026-07-30 - Billing now requires
highconfidence.mediumjoinslowin returning 422 unbilled (medium_confidence_property) — benchmarking against 41 HOVER reports put medium at 17.6% median error vs 4.7% for high, and we won't charge for a number we can't stand behind. Lite still returns those measurements free. Also new: multiple-structure disclosure (structure_scope,nearby_structures_detected,additional_structures_possible) and per-key daily/monthly call caps (lite_daily_cap_reached,monthly_call_cap_reached). - 2026-07 - Lite / Full / Premium. New
tierbody param. Breaking: a call withouttiernow defaults to LITE — free, unlimited, returnsroof.area_sqft+predominant_pitch+squaresonly,billable: false. Passtier: "full"to keep receiving (and being billed for) the complete record. Full-tier trial allotment is 3 free records per key.tier: "premium"(vendor-verified per-edge measurements, $13.95 flat) is announced and returns 501 until live. - 2026-07 - Accuracy-tiered response. Added:
roof.ai_condition(AI material / est. age / condition / replace-within-3yr + comments),property.owner_name(owner of record),roof.material_takeoff.accessories_estimate(one bundled dollar band for ice & water / starter / ridge cap / drip edge, replacing the itemized edge-driven lines),trial_calls_remaining. Removed:siding(moving to its own endpoint),contact_compliance(was a private-beta stub),property.footprint_area_sqft, and theowner_match_namerequest parameter (never shipped; the recorded owner name is surfaced instead). Breaking: the free tier and the storm-only-free carve-out are retired — every call bills after the key's one-time trial allotment. - 2026-06 - Storm block reworked. Responses now return a single
storms.last_stormevent by default (plusclaim_eligible_window); passinclude: ["storm_history"]for the full event list. The history window is now 2 years (was 5), aligned to the claim window. Storm-only calls (any request with no premium layer, e.g.include: ["storms"]) are now free on every key — no measurement runs and they're never billed, even on a paid key. Breaking: the oldstorm.hail_2024/hail_5yr_count/wind_5yr_maxfields are replaced by the structuredstormsblock. - 2026-06 - Bulk reactivation launched. Async batch storm-screening for aged lead lists:
POST /v1/reactivate+GET /v1/reactivate/{job_id}, first 1,000 addresses free, then $0.01/address, with optional ongoing next-storm monitoring and a completion webhook. - 2026-05 - Storm Alerts webhook API launched in free beta. Register addresses, get signed POSTs when qualifying hail/wind events land within 10 mi.
linear_measurementsslimmed todrip_edge_ftonly in auto-mode reports; per-edge fields ship when the report is refined via the on-screen labeling tool. Storm 5-year wind max added. - 2026-04 - Prewarm endpoint launched. Volume tier breakpoints widened.
- 2026-02 - v1 GA. Public launch.
60-second self-serve key. Card on file via Stripe.
No NDA. No procurement loop. Cancel any time.