Adding Elevation to Property Data — One API Call per Address
Add global elevation to every property in your real-estate catalog. Single-call REST API or 500 points per batch — covers flood risk, view, walkability.
A real-estate listing shows price, beds, square feet, schools, photos — and ignores the single number that explains half the flood story, half the view story, and most of the walkability story. That number is elevation.
This post walks through enriching every address in a property catalog with elevation in one API call per address (or 500 per call), globally, in production. No GIS team required, no DEM tile management, no per-state coverage gaps. By the end you will have a working Python script that turns address,lat,lng into address,lat,lng,elevation_m and a clear picture of what the number means before you ship it to the front end.
Why elevation matters in property data
Three things every PropTech product eventually needs that elevation answers cheaply.
Flood narrative. A property at +2 m near a coastline and a property at +30 m three blocks inland share a ZIP code and look identical in a spreadsheet. They have very different insurance premiums, very different resale curves after a storm season, and very different buyer concerns at the kitchen-table negotiation. Raw elevation is not a flood-zone classification (that is a regulatory call that belongs to FEMA in the US and equivalent agencies abroad), but it is the first-order signal — and unlike the official flood maps, it is global, current, and queryable from your application code.
View premium. The MLS field for "view" is a marketing checkbox. Elevation, combined with the neighbouring buildings' rooftops and the local terrain, is the math behind whether the kitchen window actually sees the ocean or sees the back of a strip mall. A simple delta — this listing's elevation minus the average elevation of every parcel within 300 m — is the cheapest "view potential" signal you can compute.
Walkability and access. A property on a 15° slope is a different product from the same square footage on a flat lot, especially for a 75-year-old buyer or a family with a stroller. Elevation deltas between an address and the nearest grocery store / school / transit stop produce a far more honest walkability score than a flat distance number.
You can ship all three with one field added to your existing address pipeline.
What the API gives you
CSV2GEO exposes elevation two ways. Pick the one that fits how your pipeline already moves.
`GET /api/v1/elevation` — a dedicated endpoint that takes 1 to 500 points per request and returns a height in metres for each. Use this when you have a CSV of coordinates and want to enrich them in bulk, or when you are building a route-profile chart and need 200 points along a polyline.
`?include=elevation` on every Places endpoint — when you are already calling /api/v1/places or /api/v1/places/nearby to find amenities around an address, append ?include=elevation and each result comes back with an ele field. No second round-trip, no extra credit per point — the elevation lookup is batched server-side into the same response. This is the right pattern when you are building a listing detail page that shows nearby coffee shops, schools, and parks, and you want each pin's elevation in the response.
Both are backed by a global 30 m DEM that covers every continent, including Antarctica. Coverage is real: a probe of Mt Everest returns 8,731 m, the Dead Sea shore returns −415 m, Death Valley's Badwater Basin returns −80 m, the summit of Mauna Kea returns 4,198 m. The negative numbers matter — they are the cheapest proof you can do that the data is real elevation and not a zeroed-out fallback for "I do not have a tile here."
A 25-line Python script that enriches a property catalog
The smallest useful version. Drop a CSV in, get a CSV out. No SDK, no third-party packages beyond requests.
import csv
import os
import requests
API = "https://csv2geo.com/api/v1/elevation"
KEY = os.environ["CSV2GEO_API_KEY"]
BATCH = 500 # /v1/elevation accepts up to 500 points per call
def chunks(seq, n):
for i in range(0, len(seq), n):
yield seq[i:i+n]
with open("properties.csv") as fin, open("properties_with_elevation.csv", "w", newline="") as fout:
reader = csv.DictReader(fin)
fields = reader.fieldnames + ["elevation_m"]
writer = csv.DictWriter(fout, fieldnames=fields)
writer.writeheader()
rows = list(reader)
for batch in chunks(rows, BATCH):
pts = "|".join(f"{r['lat']},{r['lng']}" for r in batch)
r = requests.get(API, params={"points": pts, "api_key": KEY}, timeout=30)
r.raise_for_status()
heights = r.json()["results"]
for row, h in zip(batch, heights):
row["elevation_m"] = h.get("elevation_m")
writer.writerow(row)A 50,000-row catalog runs in 100 API calls and finishes in well under a minute on a residential connection. The same script in Node, Go, Ruby, or any HTTP-capable language is essentially the same — there is no client library to depend on, no SDK version to pin, no upgrade treadmill.
What the response actually looks like
A single point:
curl -G "https://csv2geo.com/api/v1/elevation" \
--data-urlencode "points=38.8977,-77.0365" \
--data-urlencode "api_key=$CSV2GEO_API_KEY"returns:
{
"meta": {"count": 1},
"results": [
{"lat": 38.8977, "lng": -77.0365, "elevation_m": 18}
]
}And the same call along a polyline of 200 points (a property's driveway, a candidate hiking trail, a city block) returns the same shape with 200 entries — the ordering is preserved, so you can zip() the response straight back to your input array.
Two response details that matter for production code.
`null` is a valid answer. A point in the middle of the ocean or on a missing tile returns "elevation_m": null. Real 0 m is encoded as the integer 0. Branch on is None (Python) or === null (JavaScript), not on a falsy check — coastal addresses really do have elevations of 0 metres and you do not want to drop them.
Units are metres. If your front end speaks feet, convert in your serializer, not in the API call. Keep one unit at rest in your database; a column called elevation_m is more reviewable in five years than a column called elevation whose unit lives in a comment.
The Places integration — when you do not want a second call
A typical property detail page already shows nearby amenities. The Places endpoint can return their elevation in the same request, so you do not need to spend a second credit, a second round-trip, or a second piece of orchestration code.
curl -G "https://csv2geo.com/api/v1/places/nearby" \
--data-urlencode "lat=39.7392" \
--data-urlencode "lng=-104.9903" \
--data-urlencode "radius=500" \
--data-urlencode "limit=10" \
--data-urlencode "include=elevation" \
--data-urlencode "api_key=$CSV2GEO_API_KEY"Each entry in results will carry an ele field next to the existing location, name, categories, and so on. A coffee shop two blocks from the property will tell you it sits at 1,597 m — you can render the elevation delta in the same template that lists distance and walking time.
A useful design pattern: surface the delta, not the raw number. "This café is 4 m below the property" is human; "Café elevation 1,593 m" is data. The API gives you both numbers — you only ship one to the user.
Designing the pipeline: batch vs streaming
Two reasonable architectures, picked by how listings move through your system.
Batch — the right default for catalog enrichment
When a new listing lands in your warehouse, run a nightly job that geocodes it (/api/v1/geocode), then groups all newly-geocoded addresses into batches of 500 and calls /api/v1/elevation once per batch. Write elevation_m to the same row. The whole pipeline is two HTTP calls per ~500 listings — bulk-friendly, retriable, observable.
Streaming — when a user is waiting
When a user pulls up a listing detail page that you have never enriched, call /api/v1/places/nearby?include=elevation synchronously. Both the amenities and their elevations land in one response, well under 200 ms on a warm connection, and you cache the result to your CDN or to Redis with a long TTL — addresses do not move, and elevation does not change on the timescales that matter for a property listing.
The pattern that does NOT work in production is per-address calls inside a tight loop on the front-end render path. The API will happily serve them, but you are paying for 50 calls when one batched call would do — and you are spending latency budget that belongs to your user.
What you should not claim from raw elevation
The honest line. A blog post that quietly pretends elevation IS flood-zone classification will get you sued.
Elevation is a number; FEMA flood zones are a regulatory classification. They draw on elevation, on storm history, on policy, on local engineering reports — and the official classifications change on a slow regulatory cadence that you do not control. Use elevation as a signal that surfaces "this address might be worth checking against the official flood maps" — do not display "Flood Zone AE" because your elevation number is low.
Elevation is a point sample, not a roof height. The 30 m DEM samples the terrain at a 30-metre grid, not the top of the building. A 60-storey condominium and a single-storey house at the same lat/lng return the same elevation. If you need roof height, you need a different dataset and a different price tag — the Aerial Imagery API is the right next step for that, and we will cover it in the next post.
Negative elevation is a feature, not a bug. Coastal addresses in the Netherlands, parts of New Orleans, the Salton Sea — these are below sea level. Negative numbers are correct answers. If your front-end formatter strips the minus sign or clips at zero, you will produce confidently wrong content for some of your most flood-relevant listings.
Cost math for a real-world catalog
A 50,000-property MLS dataset, enriched with elevation once on import and refreshed quarterly (you do not need to refresh more often — terrain moves on geological timescales) is:
- 50,000 elevation lookups per quarter = 200,000 per year
- At 500 points per call, that is 400 API calls per quarter — billable as 400 credits on a metered plan
- Add 1 geocoding call per row on the initial import (50,000 credits, one-time)
- Every paid bracket covers this within its included monthly call budget — even the entry tier
The free tier (no credit card) covers small-to-medium pilot work — 3,000 requests per day, which at 500 points per call is 1.5 million elevation lookups per day. That is enough to enrich a small county's tax-parcel database in a single morning.
See the live numbers on the API pricing page; the published brackets are the prices, no quote process.
How to ship this on Monday
A reasonable engineering plan for a team adding elevation enrichment to a 100,000-listing catalog.
Step 1: Pilot on 500 addresses
Pick a representative slice — one coastal market, one inland market, one mountainous market. Run the 25-line script. Eyeball the outputs against any address you personally know. A house in Denver should return ~1,600 m. A house in Miami should return ~1 m. A house in Boulder should return ~1,650 m. If those numbers come back wrong, you have a problem; if they look right, you are 95% done with the technical work.
Step 2: Wire it into your enrichment job
Add elevation_m FLOAT to your listing schema. Update your nightly enrichment job to call /api/v1/elevation after geocoding, in batches of 500. Log the call count to your APM — you want a sanity-check graph that "calls per listing batched" stays close to 1, not 500.
Step 3: Render the delta, not the number
In your listing detail template, render elevation as a delta against the local average for the listing's neighbourhood. "12 m above the local average" is human; "Elevation: 47 m" is GIS jargon. The raw number stays in the database for analytics and for advanced filters (e.g. "show me oceanfront properties above 5 m").
Step 4: Add an elevation filter to your search
Buyers who have been through one hurricane season do not need a UX research study to know they want to filter by elevation. Add a slider on your search page — "minimum elevation" — and watch the conversion rate on coastal listings shift quietly upward. The query is one WHERE elevation_m >= ? on the column you just populated.
Step 5: Refresh quarterly
The DEM does not change. Your address list does. Re-enrich any new listing once on import; do not refresh existing listings unless their lat/lng changed.
Frequently Asked Questions
How accurate is the elevation data?
The DEM is 30 m horizontal resolution and globally consistent. Vertical accuracy is typically within ±5 m for most populated areas — better in places with high-quality national LIDAR coverage, worse in densely forested or rapidly-changing terrain. For property-level use cases (flood signal, view potential, walkability), this is well within the noise of the use case. If you need sub-metre vertical accuracy for engineering work, you need a survey-grade dataset, not a global API.
Does it cover countries outside the US?
Yes — coverage is global. We have verified results from every populated continent, including extreme points (Mt Everest, Dead Sea shore, Death Valley, Mauna Kea). Antarctica returns sensible numbers too if you ever need them.
What happens for points in the ocean?
You get "elevation_m": null, distinct from a real 0 m elevation. Branch on null, not on falsy. A pier or a boathouse address might legitimately return 0 m — that is correct.
Can I get elevation along a polyline (e.g. for a route profile)?
Yes. Pass the polyline as a |-separated list of lat,lng points (up to 500 per call). The response preserves order, so you can render an elevation profile chart directly from the JSON.
Is there an SDK?
Python and Node SDKs are available, but the REST API is intentionally simple — most production users wrap it in their own tiny client, which is what we recommend for enterprise pipelines. One HTTP call, one response, no version pinning.
How does this interact with reverse geocoding?
Independently. /api/v1/reverse takes a lat/lng and returns an address; /api/v1/elevation takes the same lat/lng and returns a height. You can call both in parallel from your application, or batch the elevation lookup after a batched reverse-geocode if you need both fields per row.
Related Articles
- Reverse geocoding accuracy in meters — how to talk about geocoding accuracy honestly in your product UX
- Add IP geolocation to your geocoding stack — same API key, one more call, country/region/city/county
- Benchmarking geocoding APIs — honest numbers — what to measure and what to ignore when picking a geocoding vendor
- Caching geocoding results — 90% cost reduction — addresses do not move; elevation does not change; cache aggressively
- Geocoding addresses in 200+ countries — what global coverage actually means in practice
---
*I.A. / CSV2GEO Creator*
Use our batch geocoding tool to convert thousands of addresses to coordinates in minutes. Start with 100 free addresses.
Try Batch Geocoding Free →