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.appfrom the address on file and we'll rotate it.

Tiers - one API, free & paid

Your key's tier decides which fields return. Free keys get the zero-cost layers: storm history, FEMA hazard risk, Census neighborhood, and areactivation flag (when you pass lead_date). Paid keys ($3.95/record, sliding to $1.95 at volume) get everything in free plusthe real-time roof + property record, roof area, facets & pitch, edge measurements, material takeoff, cost estimate, complexity, pool/garage flags, owner & property details, and solar. Premium fields are simply omitted on the free tier; upgrade and they appear with no code change (same key). Every measurementcarries a confidence score, and you're never billed for a low-confidence read.

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

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.
lead_datestring (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.
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

Free keys return storm, risk (FEMA), neighborhood (Census), and reactivation (when lead_date is sent). Paid keys add roof, property, solar, and data_quality. Numeric fields can be null when source data is missing, never 0 as a fallback. The field tables below marked paid are omitted on the free tier.

200 OK

{
  "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": {
      "drip_edge_ft": 457
    }
  },
  "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.drip_edge_ftnumberBuilding 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).

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." }

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.

Single-address checks are always free — call /v1/enrich one address at a time, unlimited. The price here is only for bulk batch processing: the first 1,000 addresses are free, then $0.01 per address, so a 50,000-address bulk run is $500, far below the $3.95 full-enrich rate, because reactivation only screens storm history.

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.

FieldTypeDescription
addressesarray · requiredUp 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.
monitorbooleanDefault true. When true, every address is enrolled in ongoing next-storm monitoring so you're alerted the next time a qualifying event lands.
emailstringOptional. We email this address a summary when the job finishes.
webhook_urlstringOptional. https only. We POST a completion event here when the job finishes (see below).
curl -X POST https://api.rooftap.app/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://api.rooftap.app/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:

HTTPCodeWhat to do
401invalid_keyHeader missing or key unrecognized. Same key as /v1/enrich.
402upgrade_requiredFree keys get 1,000 addresses free for the lifetime of the key. Past that you must upgrade to continue at $0.01/address.
400chunk_too_largeMore than 1,000 addresses in one request. Split into chunks of 1,000 and POST each.
429rate_limitedMax 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 queuedprocessing completed (or failed).

Query paramTypeDescription
includestringsummary returns just the summary block and omits results.
statusstringfollow_up filters results to hot leads only (the worth-a-call-today addresses).
limitnumberPage size. Max 1,000, default 500.
offsetnumberResult offset for paging.
curl https://api.rooftap.app/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

FieldTypeDescription
addressstringThe address as supplied in the request.
external_refstring | nullYour reference, echoed back when you passed the object form `{ address, external_ref }`. `null` for bare-string entries.
statusenum`done` once processed, or `error` if the address couldn't be checked. Use `worth_following_up` for the hot-lead flag, not this.
matchedbooleantrue when the address geocoded and matched a parcel with NOAA storm history.
last_eventstring | nullDate of the most recent qualifying storm event near this address. ISO format (YYYY-MM-DD), `null` if none.
max_hail_innumber | nullLargest hail diameter in inches across the screened history. `null` if no hail on record.
events_5yrnumberCount of qualifying storm events in the last 5 years within range of this address.
worth_following_upbooleanThe headline flag. true when a storm since the lead date makes this a worth-a-call-today reactivation candidate.
triggerobject | nullThe storm that flagged the address: `{ date, type, hail_in, wind_mph }`. `null` when `worth_following_up` is false.
days_since_last_stormnumber | nullWhole days since the most recent severe (qualifying) storm. `null` when there's no qualifying storm on record.
claim_window_daysnumberThe 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_windowbooleanWhether the most recent storm is still inside that claim window.
prioritynumber | nullCall-priority, 1 = call first through 5 = floor (lowest). `null` when there's no qualifying storm. Results are returned sorted by `priority` ascending (1 first).
monitoredbooleantrue 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.
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: falseand that call doesn't count toward your usage. No tickets, no clawbacks, no end-of-month reconciliation.

Changelog

  • 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_measurements slimmed to drip_edge_ft only 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-03 - owner_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 →