Per-Policy Roof and Terrain Snapshots Without Satellite Licenses

Pull an aerial image and ground elevation per policy address via one REST call each. Public-domain imagery, no per-image license, US coverage.

| May 27, 2026
Per-Policy Roof and Terrain Snapshots Without Satellite Licenses

Insurance underwriting needs an honest look at the property before it binds a policy. A spreadsheet row that reads *single-family residence, 2,100 sq ft, built 1978, replacement cost $410k* is not enough. The roof could be a 25-year-old shingle one storm away from total loss. The lot could be a 12° slope that turns every downpour into a mudslide. The neighbouring structure could be a detached garage with a tarp instead of a roof. None of that is in the structured policy data.

Carriers that ship a serious underwriting workflow solve this with imagery. They want a current top-down image of the parcel — accurate enough to count roof segments, see whether a tree is overhanging the chimney, and tell a hipped roof from a flat one — and they want a number for the ground elevation so the flood narrative is honest. The way most teams get there is a six-figure satellite imagery contract with a minimum commit, a sales cycle that takes a quarter, and an integration that requires a GIS engineer who knows what an EPSG code is.

This post shows the alternative. One REST endpoint returns a public-domain aerial image per policy address. A second REST endpoint returns ground elevation per address. Both are billed per call, both work today, both ship with the same API key that does your geocoding. No license negotiation, no minimum commit, no GIS engineer required.

What "good enough" looks like for underwriting

Three concrete questions an underwriter asks. The cost of a good answer should be a few cents, not a few thousand dollars per policy.

What is the roof made of and how big is it? Asphalt shingle versus metal versus tile shifts the policy premium by tens of percent in many markets. A top-down image at 30-50 cm per pixel — resolution typical of public aerial flights flown for agricultural and infrastructure purposes — is sharp enough that a human reviewer can classify the roof material and count segments in fifteen seconds. Carriers that automate the classification with a small computer-vision model trained on a thousand labelled examples reach 90%+ agreement with human reviewers on roof material and 85%+ on roof condition. Neither of those numbers requires the licensed centimetre-scale imagery a satellite vendor sells; the agricultural-program imagery is enough.

Is the property on a slope that changes the risk profile? Wind risk is one thing on a flat lot and another on a ridgeline that catches gusts. Flood risk for an inland property with a +30 m elevation is materially different from a coastal property at +2 m even when both share a ZIP code. Hail risk has weak elevation correlation but strong terrain correlation — exposed plateaus get hit harder than valley bottoms. A single elevation lookup per address gets you the first-order signal; pairing it with the elevation of the three nearest road centrelines gives you the local slope.

What does the parcel actually look like right now? Detached structures, pool, trampoline, blue-tarped roof from last year's claim, abandoned vehicle on cinder blocks — every one of these is a re-rating event or a non-renewal trigger. Underwriters used to drive past the property; that does not scale to a million policies. An aerial image per policy lets a one-FTE inspection team review a renewal book of fifty thousand in a quarter.

You can ship all three with two endpoints. Both are documented at csv2geo.com/api.

The two endpoints

`GET /api/v1/property/image` — returns a top-down aerial image of the parcel surrounding a given coordinate. Parameters:

| Parameter | Required | Default | Notes | |---|---|---|---| | q | conditional | — | free-text address; either this OR lat+lng | | lat, lng | conditional | — | WGS-84 coordinates; either this OR q | | size | optional | 350 | edge length in metres, range 1 to 2000; 350 m frames a typical single-family lot with three or four neighbouring parcels for context | | format | optional | png | one of png, jpg, webp |

The image is sourced from a public-domain US aerial-imagery program flown on a multi-year cycle by federal agencies. Coverage is the contiguous United States plus most of Alaska, Hawaii, and Puerto Rico. Response is the raw image binary with Content-Type: image/png (or whichever format you requested), so you can pipe it straight into an <img src> tag, an S3 upload, or a computer-vision pipeline. Each call costs 1 credit, and the result is cached server-side for 30 days, so repeated lookups against the same coordinate during a renewal cycle do not double-bill.

`GET /api/v1/elevation` — returns the ground elevation in metres above sea level for one or more points. Parameters:

| Parameter | Required | Default | Notes | |---|---|---|---| | points | required | — | semicolon-separated lat,lng pairs, up to 500 per request | | api_key | required | — | standard auth |

Coverage is global at roughly 30 m horizontal resolution. The number is the first-order signal for flood, slope-driven wind risk, and view potential. Anchor probes you can use to sanity-check the data: Mt Everest returns 8,731 m, Death Valley's Badwater Basin returns −80 m, the Dead Sea shore returns −415 m, Mauna Kea's summit returns 4,198 m. Negative numbers are real — if your provider returns 0 for points below sea level, that is a tell that you are looking at a clamped-to-zero implementation, not a real digital elevation model.

A walk-through of the elevation endpoint with deeper coverage detail lives in Adding Elevation to Property Data. This post focuses on the imagery side and the underwriting workflow that pairs the two.

A worked example: enrich a policy book

Imagine a CSV of in-force policies: policy_id,address,zip,replacement_cost. The goal is to enrich each row with image_url, elevation_m, and a terrain_class we derive from the elevation. The output drops into the underwriter's review queue ordered by flagged risk.

Step 1 — geocode each address

The aerial-image endpoint accepts free-text via q, but production pipelines geocode first so you keep the lat/lng on file for downstream joins. A single curl example to ground the pattern:

curl -s "https://csv2geo.com/api/v1/geocode?q=1600+Pennsylvania+Ave+NW+Washington+DC&api_key=$KEY" \
  | jq '.results[0] | {lat, lng, confidence}'

Returns latitude, longitude, and a confidence score. Confidence below 0.7 is the underwriter's signal to verify the address by hand before binding — see Reverse-Geocoding Accuracy and the Distance Meters for the full rationale.

Step 2 — pull the aerial image per address

Now the image endpoint. One call per coordinate:

curl -s -o "policy_$POLICY_ID.png" \
  "https://csv2geo.com/api/v1/property/image?lat=$LAT&lng=$LNG&size=350&format=png&api_key=$KEY"

The PNG lands on disk. For pipeline use the same in Python:

import requests, os

API = "https://csv2geo.com/api/v1"
KEY = os.environ["CSV2GEO_KEY"]

def fetch_aerial(policy_id, lat, lng, size_m=350):
    r = requests.get(
        f"{API}/property/image",
        params={"lat": lat, "lng": lng, "size": size_m, "format": "jpg",
                "api_key": KEY},
        timeout=30,
    )
    if r.status_code == 400:
        # Most common: outside US coverage area.
        return None, "out_of_coverage"
    r.raise_for_status()
    path = f"out/{policy_id}.jpg"
    with open(path, "wb") as f:
        f.write(r.content)
    return path, None

Or in Node:

import { writeFile } from 'node:fs/promises';

const API = 'https://csv2geo.com/api/v1';
const KEY = process.env.CSV2GEO_KEY;

async function fetchAerial(policyId, lat, lng, sizeM = 350) {
  const url = `${API}/property/image?lat=${lat}&lng=${lng}` +
              `&size=${sizeM}&format=jpg&api_key=${KEY}`;
  const r = await fetch(url);
  if (r.status === 400) return { path: null, reason: 'out_of_coverage' };
  if (!r.ok) throw new Error(`http ${r.status}`);
  const buf = Buffer.from(await r.arrayBuffer());
  const path = `out/${policyId}.jpg`;
  await writeFile(path, buf);
  return { path, reason: null };
}

Either client handles the 400 out_of_coverage case explicitly — properties outside the US (territories included) fall back to the underwriter's manual workflow. Other 4xx and 5xx errors retry with exponential backoff; see Exponential Backoff — When to Retry, When to Stop for the retry policy that keeps you below the rate limit during a bulk run.

Step 3 — pull elevation per address

Batch the elevation call. Up to 500 points per request keeps the per-policy network cost essentially free:

def fetch_elevations(coords):  # coords: list of (lat, lng)
    points = ";".join(f"{lat},{lng}" for lat, lng in coords)
    r = requests.get(
        f"{API}/elevation",
        params={"points": points, "api_key": KEY},
        timeout=30,
    )
    r.raise_for_status()
    return r.json()["elevations"]  # list of floats, same order as input

A 50,000-policy renewal book becomes 100 elevation API calls and a few minutes of wall-clock time.

Step 4 — derive a terrain class

The terrain-class rule is whatever your actuarial team wants. A simple starting point:

def terrain_class(ele_m):
    if ele_m < 5:     return "coastal_low"        # flood-priority review
    if ele_m < 30:    return "coastal_or_floodplain"
    if ele_m > 1500:  return "high_altitude"       # snow load, wind
    return "standard"

This rule is intentionally crude. The point is to demonstrate the pattern — a policy with terrain_class != standard gets flagged for a human reviewer and the aerial image attached to the review ticket. Replace the thresholds with whatever your historical loss data supports.

Step 5 — write the enriched output

The final CSV row goes from policy_id,address,zip,replacement_cost to policy_id,address,zip,replacement_cost,lat,lng,elevation_m,terrain_class,image_path. Underwriters who pull the row into their review tool see the image inline next to the structured fields, with a terrain flag that tells them when to look harder.

Production considerations

Three things that bite teams who treat the integration as a quick afternoon hack.

Coverage gaps and the US-only constraint. The aerial-image endpoint covers the contiguous United States plus Alaska, Hawaii, and Puerto Rico. Properties outside that footprint return a 400 out_of_coverage error. Build the fallback path before you ship — for non-US policies the underwriter sees a "no aerial available, manual photo upload required" CTA in the review queue. Pretending the endpoint covers everything and silently dropping foreign policies will create a reconciliation nightmare three months later when someone asks why the loss ratio in Mexico got worse.

Cache headers and CDN positioning. Each image response carries Cache-Control: private, max-age=2592000 (30 days). Behind a CDN, the same coordinate served twice in 30 days costs you one credit and one origin pull. For a renewal book that re-runs annually, that drops effective cost by 90%+ on the second cycle. See Caching Geocoding Results — 90% Cost Reduction for the broader caching pattern that applies here verbatim.

Image freshness and the multi-year flight cycle. Public aerial programs fly each state every two to three years. An image returned today might have been captured 14 months ago. For most underwriting decisions this is fine — the roof material does not change quarterly, and the lot does not move. For a policy bound after a known recent event (wildfire, hurricane), the underwriter must pair the aerial with a current ground photo from the policyholder. Build the request-a-photo path next to the aerial display, not in a separate workflow.

Compute the per-policy cost honestly. Aerial image: 1 credit. Elevation lookup: 1 credit per point (batched up to 500 per call, so a 500-point batch is also 500 credits — the batch saves network round-trips, not money). Geocoding: 1 credit. A complete enrichment of one policy address is 3 credits. At paid pricing starting from $54/month for 100,000 calls, the marginal cost per policy is under $0.002. That is the number to use when defending the build to finance.

What this replaces and what it does not

Honest scope. The endpoint pair above replaces a particular slice of the satellite-imagery contract: the "give me an aerial of this address" use case. It does not replace:

  • Change detection between two recent capture dates. If your underwriting needs to compare last quarter's image to this quarter's, the multi-year refresh cycle on public-program imagery is too coarse.
  • Centimetre-scale resolution. For counting individual asphalt tiles or measuring a crack in a foundation, you still need a paid imagery contract or a drone flight.
  • International coverage at the same resolution. Outside the US there is no equivalent public-domain program with national coverage at 30-50 cm. Some European agencies publish national orthoimagery but the integration is per-country.
  • Cloud-free guarantees on demand. Public flights are scheduled, not on-demand; if the only flight over a parcel last year happened on a cloudy day, that is the image you get for the next two years.

For US single-family and small-multi-family underwriting workflows — the volume bucket where most carriers spend most of their imagery budget — the two-endpoint pattern above is a credible alternative to the licensed satellite contract. For change detection, centimetre resolution, and international coverage, the licensed contract still has a job to do.

A note on data lineage and audit

Underwriting decisions get appealed. When a policyholder challenges a non-renewal that was triggered by an aerial image, the carrier needs to produce the exact image, the date it was captured, and the licensing terms that allow its use. Both endpoints in this post return responses backed by public-domain federal aerial imagery — there is no licensing restriction on archival storage, no per-display royalty, no "you cannot show this to the policyholder in litigation" clause. Save the image binary to S3 keyed by (policy_id, capture_date), log the API response headers, and the legal review is straightforward.

FAQ

Is the image current enough for a real underwriting decision? Most underwriting decisions tolerate 1-3 year image age — roof material, lot shape, and ground-mounted structures do not change quarterly. For policies bound immediately after a known event (wildfire, hurricane, hailstorm), pair the aerial with a current policyholder-uploaded photo.

What happens for addresses outside the United States? The endpoint returns HTTP 400 with error.code = out_of_coverage. Build the fallback to a manual-photo-upload path in the underwriter UI before you ship. Do not silently drop foreign policies.

How much does enriching one policy actually cost? Three credits total: one geocode, one image, one elevation. At paid pricing starting from $54/month for 100,000 calls, that is under $0.002 per policy. The cache absorbs renewal re-runs at near-zero cost.

Can the response be cached aggressively? Yes. The image endpoint returns Cache-Control: private, max-age=2592000 (30 days). Behind a CDN, repeat lookups against the same coordinate are free. For an annual renewal book, the second year drops to a 90%+ cache-hit ratio.

Does the image come back with any metadata embedded? The response is raw image binary. Capture date and source program metadata are returned in response headers — log them alongside the image binary in S3 so the legal-defensibility chain is complete.

Why one image plus elevation rather than just elevation? Elevation alone misses the structures-on-the-lot story. A 0.4-acre lot at +5 m with a tarped roof and an abandoned car is a different policy from the same lot with a new roof and a tended garden. The image catches what elevation cannot, and elevation catches what the image cannot.

What happens if the aerial program does not have coverage for the state of a given address? A small handful of US territories and edge regions return out_of_coverage. The fallback is the same as for international: the underwriter UI shows a manual-photo-upload CTA. Plan for a 1-3% out-of-coverage rate on a national US policy book.

Related articles

---

*I.A. / CSV2GEO Creator*

Ready to geocode your addresses?

Use our batch geocoding tool to convert thousands of addresses to coordinates in minutes. Start with 100 free addresses.

Try Batch Geocoding Free →