API guide
How to think about the response, what coverage does to it, and what every column means. The endpoint contract itself — parameters, status codes, the exact shape of every field — lives in the generated reference, which is emitted from the schema the API serves and cannot disagree with it.
Five services, one hostname, one key: /v1/profile for one property, /v1/search for every property in a county matching a filter, /v1/geocode/* for addresses and coordinates, and /v1/tiles/* for the imagery and base map archives. Bulk access to a whole county is the data licence, which is a purchase rather than an endpoint.
Authentication
Bearer token. Create and revoke keys in your dashboard. A key is shown in full exactly once, at creation, because only a hash of it is stored. Losing it means rotating it.
curl https://api.oklocate.com/v1/profile \
-H "Authorization: Bearer ok_live_..." \
-G --data-urlencode "address=1620 E Main Cross St, Taylorville, IL 62568"Keys are prefixed ok_live_ or ok_test_. The prefix is not secret and is safe to log.
One price, every field
Every response carries every measurement the county can offer. There is no cheaper request that quietly returns less, and no depth parameter to get wrong. Volume and price come from your plan, on the pricing page.
To shrink a response, name the columns you want with fields=. That changes the payload, never the price.
Request
Identify a property by address or by coordinates. Coordinates skip geocoding and are the faster path. Every parameter, with its type and bounds, is in the generated reference rather than repeated here — a hand-kept copy of a contract is a copy that eventually disagrees with it, and this page carried a tier parameter for four days after the API stopped reading one.
Response, and the one rule that matters
A field the county cannot measure is omitted, never returned as null. Read _meta.featuresAvailable before parsing and you can tell "this property has no pool" from "we cannot see pools in this county". Those are different facts and a null would conflate them.
48 fields carry a sentinel of -1.0 meaning "could not be computed for this property". The API omits those, so a sentinel never reaches you formatted as a measurement.
A sentinel is not always absence. roof_aspect_deg stores the same -1.0 to mean flat — a real answer about the roof, not a missing one — and it is sent rather than omitted. A flat roof has no meaningful bearing, and that is precisely what a solar installer is asking about, so discarding it would throw away the answer.
Lake, Illinois — 81 fields
Full 57-column LiDAR and real parcel boundaries, so every group is present.
{
"_meta": {
"countyFips": "17097",
"countyName": "Lake, Illinois",
"lidarStatus": "full_57",
"featuresAvailable": [ 81 columns ],
"clayVersion": "1.5"
},
"address": {
"addr_unique": …,
"gers_id": …
},
"detectors": {
"pool": …,
"pool_p": …,
"solar": …,
"solar_p": …,
"roof_shape": …,
"roof_shape_p": …,
"detached": …,
"detached_p": …,
"detached_src": …
},
"roof_geometry": {
"roof_h_p50_m": …,
"roof_h_p90_m": …,
"roof_h_max_m": …,
"stories_est": …,
"roof_pitch_proxy_m": …,
"roof_planarity_std_m": …,
"roof_tilt_deg": …,
"roof_resid_m": …,
"roof_area_sqft": …,
"roof_mean_slope_deg": …,
"roof_aspect_deg": …,
"roof_south_frac": …,
"solar_potential_sqft": …
},
"solar_facets": {
"solar_tier": …,
"solar_sqft_s": …,
"solar_sqft_sew": …,
"solar_sqft_all": …,
"solar_flat_sqft": …,
"solar_best_sqft": …,
"solar_best_aspect_deg": …,
"solar_best_slope_deg": …,
"solar_facets_s": …,
"solar_facets_sew": …,
"solar_facets_all": …,
"facet_count": …,
"facet_area_sqft": …,
"facet_south_sqft": …,
"facet_south_frac": …,
"facet_has_usable_south": …,
"solar_shade_class": …
},
"insurance": {
"bldg_height_ft": …,
"bldg_src": …,
"roof_complexity_score": …,
"tree_overhang_5ft": …,
"tree_overhang_30ft": …,
"defensible_space_pct": …,
"tree_count": …
},
"roof_reflection": {
"roof_int_p50": …,
"roof_int_rel_p10": …,
"roof_int_rel_p90": …,
"roof_int_cv": …,
"roof_int_patch_frac": …
},
"terrain_drainage": {
"terrain_slope_deg": …,
"lot_relief_m": …,
"terrain_aspect_deg": …,
"grade_to_house_ft": …
},
"vegetation_fuels": {
"tree_canopy_frac": …,
"max_tree_h_m": …,
"roof_overhang_frac": …,
"veg_low_frac": …,
"veg_med_frac": …,
"ladder_fuel_frac": …
},
"pool_void": {
"pool_void_sqft": …,
"pool_void_compactness": …
},
"yard_features": {
"npts_norm": …,
"bldg6_frac": …,
"veg5_frac": …,
"grd_frac": …,
"tall_frac": …,
"bldgh_frac": …,
"maxh": …,
"h_p50": …,
"h_p75": …,
"h_p90": …,
"h_std": …,
"b6_cover": …,
"b6_maxdist": …,
"b6_cnt_norm": …,
"b6_p90h": …,
"tall_cover": …
}
}Cook, Illinois — 82 fields
The same endpoint against a county with no LiDAR. Note what is absent: no roof geometry, no terrain, no vegetation. Not nulls, absent.
{
"_meta": {
"countyFips": "17031",
"countyName": "Cook, Illinois",
"lidarStatus": "full_57",
"featuresAvailable": [ 82 columns ]
},
"address": {
"addr_unique": …,
"gers_id": …,
"assessor_class": …
},
"detectors": {
"pool": …,
"pool_p": …,
"solar": …,
"solar_p": …,
"roof_shape": …,
"roof_shape_p": …,
"detached": …,
"detached_p": …,
"detached_src": …
},
"roof_geometry": {
"roof_h_p50_m": …,
"roof_h_p90_m": …,
"roof_h_max_m": …,
"stories_est": …,
"roof_pitch_proxy_m": …,
"roof_planarity_std_m": …,
"roof_tilt_deg": …,
"roof_resid_m": …,
"roof_area_sqft": …,
"roof_mean_slope_deg": …,
"roof_aspect_deg": …,
"roof_south_frac": …,
"solar_potential_sqft": …
},
"solar_facets": {
"solar_tier": …,
"solar_sqft_s": …,
"solar_sqft_sew": …,
"solar_sqft_all": …,
"solar_flat_sqft": …,
"solar_best_sqft": …,
"solar_best_aspect_deg": …,
"solar_best_slope_deg": …,
"solar_facets_s": …,
"solar_facets_sew": …,
"solar_facets_all": …,
"facet_count": …,
"facet_area_sqft": …,
"facet_south_sqft": …,
"facet_south_frac": …,
"facet_has_usable_south": …,
"solar_shade_class": …
},
"insurance": {
"bldg_height_ft": …,
"bldg_src": …,
"roof_complexity_score": …,
"tree_overhang_5ft": …,
"tree_overhang_30ft": …,
"defensible_space_pct": …,
"tree_count": …
},
"roof_reflection": {
"roof_int_p50": …,
"roof_int_rel_p10": …,
"roof_int_rel_p90": …,
"roof_int_cv": …,
"roof_int_patch_frac": …
},
"terrain_drainage": {
"terrain_slope_deg": …,
"lot_relief_m": …,
"terrain_aspect_deg": …,
"grade_to_house_ft": …
},
"vegetation_fuels": {
"tree_canopy_frac": …,
"max_tree_h_m": …,
"roof_overhang_frac": …,
"veg_low_frac": …,
"veg_med_frac": …,
"ladder_fuel_frac": …
},
"pool_void": {
"pool_void_sqft": …,
"pool_void_compactness": …
},
"yard_features": {
"npts_norm": …,
"bldg6_frac": …,
"veg5_frac": …,
"grd_frac": …,
"tall_frac": …,
"bldgh_frac": …,
"maxh": …,
"h_p50": …,
"h_p75": …,
"h_p90": …,
"h_std": …,
"b6_cover": …,
"b6_maxdist": …,
"b6_cnt_norm": …,
"b6_p90h": …,
"tall_cover": …
}
}Code examples
Python
import os, requests
resp = requests.get(
"https://api.oklocate.com/v1/profile",
headers={"Authorization": f"Bearer {os.environ['OKLOCATE_API_KEY']}"},
params={"lat": 42.31, "lon": -87.98},
timeout=10,
)
resp.raise_for_status()
profile = resp.json()
# Check availability before reading a field. A missing key means the county
# cannot measure it -- not that the property lacks the feature.
available = set(profile["_meta"]["featuresAvailable"])
if "grade_to_house_ft" in available:
grade = profile["terrain_drainage"]["grade_to_house_ft"]
if grade < 0:
print("Ground falls toward the foundation")
else:
print(f"No terrain data for {profile['_meta']['countyName']}")JavaScript
const res = await fetch(
"https://api.oklocate.com/v1/profile?" +
new URLSearchParams({ lat: "42.31", lon: "-87.98" }),
{ headers: { Authorization: `Bearer ${process.env.OKLOCATE_API_KEY}` } },
);
if (!res.ok) throw new Error(`OKLocate ${res.status}`);
const profile = await res.json();
const available = new Set(profile._meta.featuresAvailable);
if (available.has("solar_potential_sqft")) {
console.log(profile.roof_geometry.solar_potential_sqft, "sq ft usable");
}Searching a county
/v1/search filters every house in a county on any combination of measured attributes. It returns the true match count plus a page of rows, and it is metered per call — the count costs the same as the rows. Live counties today: 17097, 17089, 17111, 17167, 17143, 17179, 17031, 17163, 17161, 17043, 17093, 17107, 17119, 17197, 17201, 17113, 17019, 17115, 17099, 17183, 17091, 17199, 17073, 17037, 17095, 17195, 17055, 17001, 17077, 17027, 17117, 17063, 17029, 17103, 17141, 17007, 17075, 17105, 17177, 17049, 17135, 17173, 17203, 17121, 17021, 17057, 17133, 17011, 17137, 17051, 17157, 17081, 17015, 17033, 17189, 17165, 17083, 17149, 17159, 17023, 17041, 17053, 17067, 17145, 17005, 17191, 17125, 17109, 17147, 17193, 17039, 17045, 17079, 17139, 17087, 17131, 17085, 17127, 17035, 17123, 17187, 17181, 17061, 17017, 17129, 17185, 17071, 17175, 17047, 17059, 17155, 17169, 17003, 17009, 17065, 17101, 17151, 17013, 17069, 17153, 17025, 17171, 12001, 12003, 12005, 12007, 12009, 12011, 12013, 12015, 12017, 12019, 12021, 12023, 12027, 12029, 12031, 12033, 12035, 12037, 12039, 12041, 12043, 12045, 12047, 12049, 12051, 12053, 12055, 12057, 12059, 12061, 12063, 12065, 12067, 12069, 12071, 12073, 12075, 12077, 12079, 12081, 12083, 12085, 12086, 12087, 12089, 12091, 12093, 12095, 12097, 12099, 12101, 12103, 12105, 12107, 12109, 12111, 12113, 12115, 12117, 12119, 12121, 12123, 12125, 12127, 12129, 12131, 12133, 48001, 48003, 48005, 48007, 48009, 48011, 48013, 48015, 48017, 48019, 48021, 48023, 48025, 48027, 48029, 48031, 48033, 48035, 48037, 48039, 48041, 48043, 48045, 48047, 48049, 48051, 48053, 48055, 48057, 48059, 48061, 48063, 48065, 48067, 48069, 48071, 48073, 48075, 48077, 48079, 48081, 48083, 48085, 48087, 48089, 48091, 48093, 48095, 48097, 48099, 48101, 48103, 48105, 48107, 48109, 48111, 48113, 48115, 48117, 48119, 48121, 48123, 48125, 48127, 48129, 48131, 48133, 48135, 48137, 48139, 48141, 48143, 48145, 48147, 48149, 48151, 48153, 48155, 48157, 48159, 48161, 48163, 48165, 48167, 48169, 48171, 48173, 48175, 48177, 48179, 48181, 48183, 48185, 48187, 48189, 48191, 48193, 48195, 48197, 48199, 48201, 48203, 48205, 48207, 48209, 48211, 48213, 48215, 48217, 48219, 48221, 48223, 48225, 48227, 48229, 48231, 48233, 48235, 48237, 48239, 48241, 48243, 48245, 48247, 48249, 48251, 48253, 48255, 48257, 48259, 48261, 48263, 48265, 48267, 48269, 48271, 48273, 48275, 48277, 48279, 48281, 48283, 48285, 48287, 48289, 48291, 48293, 48295, 48297, 48299, 48301, 48303, 48305, 48307, 48309, 48311, 48313, 48315, 48317, 48319, 48321, 48323, 48325, 48327, 48329, 48331, 48333, 48335, 48337, 48339, 48341, 48343, 48345, 48347, 48349, 48351, 48353, 48355, 48357, 48359, 48361, 48363, 48365, 48367, 48369, 48371, 48373, 48375, 48377, 48379, 48381, 48383, 48385, 48387, 48389, 48391, 48393, 48395, 48397, 48399, 48401, 48403, 48405, 48407, 48409, 48411, 48413, 48415, 48417, 48419, 48421, 48423, 48425, 48427, 48429, 48431, 48433, 48435, 48437, 48439, 48441, 48443, 48445, 48447, 48449, 48451, 48453, 48455, 48457, 48459, 48461, 48463, 48465, 48467, 48469, 48471, 48473, 48475, 48477, 48479, 48481, 48483, 48485, 48487, 48489, 48491, 48493, 48495, 48497, 48499, 48501, 48503, 48505, 48507, 36103, 36059, 36029, 36055, 36119, 36067, 36081, 36071, 36063, 36027, 36001, 36065, 36091, 36087, 36007, 36111, 36085, 36013, 36083, 36075, 36101, 36069, 36089, 36093, 36045, 36009, 36117, 36047, 36011, 36015, 36079, 36105, 36053, 36021, 36019, 36115, 36077, 36025, 36051, 36043, 36113, 36037, 36017, 36109, 36039, 36107, 36003, 36033, 36057, 36035, 36031, 36121, 36073, 36023, 36095, 36049, 36099, 36005, 36123, 36097, 36041, 36061, 08041, 08059, 08031, 08005, 08001, 08123, 08035, 08069, 08013, 08101, 08077, 08067, 08014, 08043, 08085, 08045, 08029, 08039, 08087, 08037, 08093, 08083, 08075, 08117, 08119, 08107, 08049, 08015, 08089, 08051, 08071, 08007, 08125, 08105, 08097, 08021, 08099, 08027, 08081, 08003, 08063, 08019, 08055, 08103, 08121, 08109, 08113, 08091, 08065, 08047, 08073, 08095, 08023, 08115, 08009, 08017, 08011, 08079, 08033, 08061, 08025, 08057, 08053, 08111, 04013, 04019, 04021, 04025, 04015, 04027, 04003, 04017, 04005, 04007, 04023, 04001, 04009, 04012, 04011, 06037, 06073, 06059, 06065, 06071, 06067, 06085, 06001, 06013, 06019, 06029, 06077, 06111, 06081, 06099, 06097, 06061, 06107, 06095, 06079, 06083, 06053, 06017, 06041, 06047, 06089, 06007, 06087, 06113, 06023, 06057, 06039, 06055, 06031, 06025, 06075, 06103, 06101, 06045, 06109, 06009, 06033, 06093, 06115, 06005, 06069, 06063, 06035, 06021, 06043, 06011, 06105, 06015, 06049, 06051, 06027, 06091, 06003, 42003, 42091, 42017, 42071, 42129, 42133, 42029, 42045, 42011, 42079, 42095, 42077, 42101, 42043, 42125, 42049, 42069, 42041, 42019, 42055, 42007, 42089, 42051, 42021, 42085, 42027, 42075, 42107, 42013, 42001, 42073, 42063, 42005, 42033, 42081, 42097, 42103, 42037, 42111, 42039, 42009, 42015, 42025, 42121, 42065, 42127, 42099, 42087, 42117, 42061, 42059, 42031, 42083, 42119, 42109, 42067, 42035, 42131, 42115, 42047, 42123, 42105, 42093, 42057, 42113, 42023, 42053.
curl https://api.oklocate.com/v1/search \
-H "Authorization: Bearer ok_live_..." \
-G --data-urlencode "county=17031" \
--data-urlencode "filters=pool:eq:true;bldg_height_ft:gte:20" \
--data-urlencode "limit=100"Read _meta.filtersIgnored before trusting the results. A filter naming a column this county cannot measure is ignored rather than rejected, so one unusable filter does not fail a request whose others are fine. But an ignored filter makes the result set wider than you asked for, not narrower: pool:eq:true dropped in a county with no pool detector returns every house. The array names each one and why.
countMatched is always the full total, never capped by limit. Page with offset; ordering is stable, so a page boundary cannot skip or repeat a house. A county outside coverage is a 404, never a count of zero — those are different answers.
Geocoding
Address to coordinates, coordinates to address, and type-ahead suggestions — on the same hostname and the same key as everything else. Metered per call against the same plan allowance as profiles and searches.
curl https://api.oklocate.com/v1/geocode/search \
-H "Authorization: Bearer ok_live_..." \
-G --data-urlencode "q=1600 Pennsylvania Ave NW, Washington, DC"
curl https://api.oklocate.com/v1/geocode/reverse \
-H "Authorization: Bearer ok_live_..." \
-G --data-urlencode "lat=38.8976751" --data-urlencode "lon=-77.036547"/search returns results, each with lat, lon, match and a stable Overture gers_id. /reverse adds distance_m and sorts by it. /suggest returns suggestions for type-ahead.
Match types are never blended. exact, interpolated and nearest are different claims about how a coordinate was arrived at — an interpolated point is a position along a street range, not a rooftop — and match says which you got. Read it before treating a coordinate as a building.
Imagery and base map tiles
30cm aerial imagery, a vector base map and boundary overlays, proxied on the same hostname and the same key. Point a MapLibre, Mapbox or Google Maps client at these and nothing else changes about your map.
The traffic is range requests, not tile URLs
This is the one thing to understand before writing any code against it. Clients load these through pmtiles://, which range-reads a single archive file and pulls the tile out of the bytes — so your client requests byte ranges of one big object rather than a URL per tile. The base map archive is around 8.8 GB, and you never fetch it whole.
A GET to a .pmtiles path with no Range header returns 416, deliberately. That is not an error to work around — it is the archive refusing to send you gigabytes. Send a range and you get 206 with Content-Range.
# Refused, on purpose — never request a whole archive
curl -I https://api.oklocate.com/v1/tiles/us-basemap.pmtiles \
-H "Authorization: Bearer ok_live_..."
# HTTP/2 416
# A range read: this is what pmtiles.js does for you
curl -sD - https://api.oklocate.com/v1/tiles/us-basemap.pmtiles \
-H "Authorization: Bearer ok_live_..." \
-H "Range: bytes=0-16383" -o /dev/null
# HTTP/2 206
# content-range: bytes 0-16383/8802689831
# accept-ranges: bytesIn a browser you would not write either of those. Load the archive as a protocol and let the client do the range reads:
import { FetchSource, PMTiles, Protocol } from "pmtiles";
import * as maplibregl from "maplibre-gl";
// maplibre-gl 6 under a bundler also needs setWorkerUrl(); see its install guide.
const url = "https://api.oklocate.com/v1/tiles/us-basemap.pmtiles";
// Your key travels as a request header, never in the URL, where it would
// land in access logs, Referer headers and browser history.
//
// It has to be attached HERE, to the archive's own source. A MapLibre
// transformRequest does NOT work for pmtiles:// — see the note below.
const protocol = new Protocol({ metadata: true });
protocol.add(
new PMTiles(
new FetchSource(url, new Headers({ Authorization: "Bearer ok_live_..." })),
),
);
maplibregl.addProtocol("pmtiles", protocol.tile);
new maplibregl.Map({
container: "map",
style: {
version: 8,
// The URL here must match `url` above EXACTLY, character for character.
sources: { basemap: { type: "vector", url: `pmtiles://${url}` } },
layers: [/* ... */],
},
});transformRequest does not authenticate an archive
This is the one mistake worth warning about, because it half-works and is miserable to debug. MapLibre’s transformRequest applies to requests MapLibre itself issues — so a {z}/{x}/{y} tile URL picks your header up and authenticates fine. A pmtiles:// archive does not. The protocol handler reads only the URL from the request it is handed and builds its own fetch source with empty headers, so the Authorization you added is dropped on every range read.
A map wired that way renders its vector overlays and silently 401s its base map. Attach the header to a FetchSource as above instead — and keep the two URL strings identical, because protocol.add indexes by exact URL and a mismatch quietly falls back to an unauthenticated source.
Two hostnames, one service
tiles.oklocate.com/{path} is the same service as api.oklocate.com/v1/tiles/{path}, with the same key, the same path shapes and the same semantics. Use whichever reads better in your style; the shorter host is there because a tile URL ends up repeated across every layer in a map style.
Archives are served straight from object storage with no intermediate hop. /robots.txt and the bare host answer without a key; everything else needs one.
Three path shapes, and no others
Anything else is rejected with 400 before it leaves our Worker. The allowlist exists because this endpoint forwards a path you supply to a fixed origin, and without it your key would be a way to reach anything else on that service.
GET /v1/tiles/{name}/{z}/{x}/{y}.{ext} one tile; {name} has no slashes
GET /v1/tiles/{name}.json TileJSON, read from the archive header
GET /v1/tiles/{path}.pmtiles byte ranges; slashes allowed hereLayer ids do not match archive names
Several archives serve a layer whose id is nothing like the filename, so a style referencing the filename renders an empty map with no error. The ones that differ:
| Archive | source-layer | Max zoom |
|---|---|---|
| census-tracts.pmtiles | censustractsgeojsonseq | 14 |
| places.pmtiles | placesgeojsonseq | 14 |
| school-districts.pmtiles | elementary | 12 |
| school-districts.pmtiles | secondary | 12 |
| school-districts.pmtiles | unified | 12 |
| us-basemap.pmtiles | water | 14 |
| zip-codes.pmtiles | zipcodes | 12 |
school-districts.pmtiles serves three layers — elementary, secondary and unified — rather than one. Check {name}.json before writing a style against any archive.
Aerial resolution is not uniform
National coverage stops at zoom 15. Zoom 16–18 exists for Illinois only. Ask for a higher zoom elsewhere and you get blank tiles rather than an error, so set maxzoom on your raster source to match what actually exists for the area you are showing.
Tiles do not draw down your profile allowance
They are authenticated like everything else, but a pan or a zoom is dozens of range reads and charging those against a monthly profile allowance would let one embedded map exhaust the quota your API integration depends on. So tile responses carry no X-OKLocate-Quota-* headers, and imagery is billed on its own plan, metered on parcel views.
Usage reporting for imagery is not live yet. The archives are served by a shared tile service that does not report usage, and a parcel view is a viewport event rather than a request, so it cannot be counted from these reads. Imagery works today and is not yet metered — your dashboard says so rather than showing a zero.
County data licence
The whole dataset for a county, a metro or a state, delivered as files you keep. This is the answer to "I need every property, not one at a time" — a search returns a working page of matches, and past a certain volume you want the data in your own warehouse rather than a paging loop.
A one-off purchase, not a subscription, with an optional annual refresh. Storage rights are included by definition — this is a licence rather than a lookup, so there is nothing to withhold.
| Scope | NAIP Scored | NAIP + LiDAR |
|---|---|---|
| Single county | $299 | $799 |
| Metro bundle (3–5 counties) | $699 | $1,999 |
| Full state | $2,499 | $5,999 |
| Annual refresh | 30% | 30% |
So a full-state NAIP + LiDAR licence is $5,999 once, and $1,800 a year after that if you want new vintages. The refresh is optional and tied to a real event — a new NAIP or LiDAR flight becoming available for your counties — not an annual invoice for the same files.
What is available
NAIP Scored covers 689 counties and NAIP + LiDAR covers 672. Both counts are read from the same coverage records the rest of this site uses, so this page cannot offer you a county the coverage page does not list. NAIP Scored carries the detector columns — pool, roof type, solar flag, detached structure. NAIP + LiDAR adds all 57 physical measurements.
Delivery
- Parquet download via R2 pre-signed URL
- Direct Iceberg catalog endpoint (Databricks, Snowflake, DuckDB)
- CSV on request
One file per county FIPS, with a manifest whose row and column counts are read back from the files themselves rather than from what the export intended. The column names and meanings are the same ones in the field reference below, so anything you built against /v1/profile reads the bulk files without a mapping layer.
How to buy one
Talk to us — there is no self-serve checkout for this yet. Email team@oklocate.com with the counties you need and which of the two products, and we will scope it. Everything else on this page you can start using with a key from your dashboard; this one is a conversation.
Field reference
Every field, its tier, its source, and its validation status. Where a measurement rests on an unchecked constant, it says so. See the data dictionary for full descriptions and methodology.
Address
| Column | Units | Source | Validation | Requires |
|---|---|---|---|---|
| addr_unique | — | derived | reasonable | — |
| gers_id | — | derived | validated | — |
| assessor_class | — | derived | validated | — |
Detectors
| Column | Units | Source | Validation | Requires |
|---|---|---|---|---|
| pool | — | naip | validated · 97.2% precision | parcel boundary |
| pool_p | — | naip | reasonable | parcel boundary |
| solar | — | naip | beta · 85.9% precision | — |
| solar_p | — | naip | reasonable | — |
| roof_shape | — | naip | validated · 92.6% precision | — |
| roof_shape_p | — | naip | reasonable | — |
| detached | — | naip | validated · 89.7% precision | parcel boundary |
| detached_p | — | naip | reasonable | parcel boundary |
| detached_src | — | derived | validated | parcel boundary |
| lot_open_frac | — | derived | validated | parcel boundary |
Roof geometry
| Column | Units | Source | Validation | Requires |
|---|---|---|---|---|
| roof_h_p50_m | m | lidar_ilhmp | reasonable | LiDAR |
| roof_h_p90_m | m | lidar_ilhmp | reasonable | LiDAR |
| roof_h_max_m | m | lidar_ilhmp | reasonable | LiDAR |
| stories_est | — | derived | unvalidated | LiDAR |
| roof_pitch_proxy_m | m | lidar_ilhmp | reasonable | LiDAR |
| roof_planarity_std_m | m | lidar_ilhmp | reasonable | LiDAR |
| roof_tilt_deg | deg | lidar_ilhmp | reasonable | LiDAR |
| roof_resid_m | m | lidar_ilhmp | reasonable | LiDAR |
| roof_area_sqft | sqft | lidar_ilhmp | reasonable | LiDAR |
| roof_mean_slope_deg | deg | lidar_ilhmp | reasonable | LiDAR |
| roof_aspect_deg | deg | lidar_ilhmp | reasonable | LiDAR |
| roof_south_frac | — | lidar_ilhmp | reasonable | LiDAR |
| solar_potential_sqft | sqft | lidar_ilhmp | unvalidated | LiDAR |
Per-face solar
| Column | Units | Source | Validation | Requires |
|---|---|---|---|---|
| solar_tier | — | lidar_ilhmp | reasonable | — |
| solar_sqft_s | sqft | lidar_ilhmp | reasonable | — |
| solar_sqft_sew | sqft | lidar_ilhmp | reasonable | — |
| solar_sqft_all | sqft | lidar_ilhmp | reasonable | — |
| solar_flat_sqft | sqft | lidar_ilhmp | reasonable | — |
| solar_best_sqft | sqft | lidar_ilhmp | reasonable | — |
| solar_best_aspect_deg | deg | lidar_ilhmp | reasonable | — |
| solar_best_slope_deg | deg | lidar_ilhmp | reasonable | — |
| solar_facets_s | — | lidar_ilhmp | reasonable | — |
| solar_facets_sew | — | lidar_ilhmp | reasonable | — |
| solar_facets_all | — | lidar_ilhmp | reasonable | — |
| facet_count | — | derived | reasonable | — |
| facet_area_sqft | sqft | derived | reasonable | — |
| facet_south_sqft | sqft | derived | reasonable | — |
| facet_south_frac | — | derived | reasonable | — |
| facet_has_usable_south | — | derived | reasonable | — |
| solar_shade_class | — | lidar_ilhmp | reasonable | — |
Insurance
| Column | Units | Source | Validation | Requires |
|---|---|---|---|---|
| bldg_height_ft | ft | lidar_ilhmp, overture_footprints | validated | LiDAR |
| bldg_src | — | lidar_ilhmp | validated | LiDAR |
| roof_complexity_score | — | derived | unvalidated | LiDAR |
| tree_overhang_5ft | points | lidar_ilhmp | reasonable | LiDAR |
| tree_overhang_30ft | points | lidar_ilhmp | reasonable | LiDAR |
| defensible_space_pct | % | lidar_ilhmp | reasonable | LiDAR |
| tree_count | — | derived | unvalidated | LiDAR |
Terrain and drainage
| Column | Units | Source | Validation | Requires |
|---|---|---|---|---|
| terrain_slope_deg | deg | lidar_ilhmp | reasonable | LiDAR + parcel boundary |
| lot_relief_m | m | lidar_ilhmp | reasonable | LiDAR + parcel boundary |
| terrain_aspect_deg | deg | lidar_ilhmp | reasonable | LiDAR + parcel boundary |
| grade_to_house_ft | ft | lidar_ilhmp | validated | LiDAR + parcel boundary |
Vegetation and fuels
| Column | Units | Source | Validation | Requires |
|---|---|---|---|---|
| tree_canopy_frac | — | lidar_ilhmp | reasonable | LiDAR |
| max_tree_h_m | m | lidar_ilhmp | reasonable | LiDAR |
| roof_overhang_frac | — | lidar_ilhmp | reasonable | LiDAR |
| veg_low_frac | — | lidar_ilhmp | reasonable | LiDAR |
| veg_med_frac | — | lidar_ilhmp | reasonable | LiDAR |
| ladder_fuel_frac | — | lidar_ilhmp | reasonable | LiDAR |
Pool confirmation
| Column | Units | Source | Validation | Requires |
|---|---|---|---|---|
| pool_void_sqft | sqft | lidar_ilhmp | unvalidated | LiDAR + parcel boundary |
| pool_void_compactness | — | lidar_ilhmp | unvalidated | LiDAR + parcel boundary |
Roof reflectance
| Column | Units | Source | Validation | Requires |
|---|---|---|---|---|
| roof_int_p50 | — | lidar_ilhmp | reasonable | LiDAR |
| roof_int_rel_p10 | — | lidar_ilhmp | reasonable | LiDAR |
| roof_int_rel_p90 | — | lidar_ilhmp | reasonable | LiDAR |
| roof_int_cv | — | lidar_ilhmp | reasonable | LiDAR |
| roof_int_patch_frac | — | lidar_ilhmp | unvalidated | LiDAR |
Yard features
| Column | Units | Source | Validation | Requires |
|---|---|---|---|---|
| npts_norm | — | lidar_ilhmp | reasonable | LiDAR + parcel boundary |
| bldg6_frac | — | lidar_ilhmp | reasonable | LiDAR + parcel boundary |
| veg5_frac | — | lidar_ilhmp | reasonable | LiDAR + parcel boundary |
| grd_frac | — | lidar_ilhmp | reasonable | LiDAR + parcel boundary |
| tall_frac | — | lidar_ilhmp | reasonable | LiDAR + parcel boundary |
| bldgh_frac | — | lidar_ilhmp | reasonable | LiDAR + parcel boundary |
| maxh | — | lidar_ilhmp | reasonable | LiDAR + parcel boundary |
| h_p50 | — | lidar_ilhmp | reasonable | LiDAR + parcel boundary |
| h_p75 | — | lidar_ilhmp | reasonable | LiDAR + parcel boundary |
| h_p90 | — | lidar_ilhmp | reasonable | LiDAR + parcel boundary |
| h_std | — | lidar_ilhmp | reasonable | LiDAR + parcel boundary |
| b6_cover | — | lidar_ilhmp | reasonable | LiDAR + parcel boundary |
| b6_maxdist | — | lidar_ilhmp | reasonable | LiDAR + parcel boundary |
| b6_cnt_norm | — | lidar_ilhmp | reasonable | LiDAR + parcel boundary |
| b6_p90h | — | lidar_ilhmp | reasonable | LiDAR + parcel boundary |
| tall_cover | — | lidar_ilhmp | reasonable | LiDAR + parcel boundary |
Errors
| 400 | Neither an address nor a lat/lon pair was supplied |
| 401 | Missing, malformed, or revoked API key |
| 402 | Free allowance exhausted and no payment method on file |
| 404 | Address resolved, but the property is outside coverage |
| 422 | Address could not be resolved to a parcel |
| 429 | Rate limit exceeded; retry after the Retry-After header |
404 and 422 are deliberately distinct. 404 means we resolved the address and do not cover it; 422 means we could not resolve it at all. Retrying helps with neither, but they call for different fixes.