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: bytes

In 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 here

Layer 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:

Archivesource-layerMax zoom
census-tracts.pmtilescensustractsgeojsonseq14
places.pmtilesplacesgeojsonseq14
school-districts.pmtileselementary12
school-districts.pmtilessecondary12
school-districts.pmtilesunified12
us-basemap.pmtileswater14
zip-codes.pmtileszipcodes12

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.

ScopeNAIP ScoredNAIP + LiDAR
Single county$299$799
Metro bundle (3–5 counties)$699$1,999
Full state$2,499$5,999
Annual refresh30%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

ColumnUnitsSourceValidationRequires
addr_unique—derivedreasonable—
gers_id—derivedvalidated—
assessor_class—derivedvalidated—

Detectors

ColumnUnitsSourceValidationRequires
pool—naipvalidated · 97.2% precisionparcel boundary
pool_p—naipreasonableparcel boundary
solar—naipbeta · 85.9% precision—
solar_p—naipreasonable—
roof_shape—naipvalidated · 92.6% precision—
roof_shape_p—naipreasonable—
detached—naipvalidated · 89.7% precisionparcel boundary
detached_p—naipreasonableparcel boundary
detached_src—derivedvalidatedparcel boundary
lot_open_frac—derivedvalidatedparcel boundary

Roof geometry

ColumnUnitsSourceValidationRequires
roof_h_p50_mmlidar_ilhmpreasonableLiDAR
roof_h_p90_mmlidar_ilhmpreasonableLiDAR
roof_h_max_mmlidar_ilhmpreasonableLiDAR
stories_est—derivedunvalidatedLiDAR
roof_pitch_proxy_mmlidar_ilhmpreasonableLiDAR
roof_planarity_std_mmlidar_ilhmpreasonableLiDAR
roof_tilt_degdeglidar_ilhmpreasonableLiDAR
roof_resid_mmlidar_ilhmpreasonableLiDAR
roof_area_sqftsqftlidar_ilhmpreasonableLiDAR
roof_mean_slope_degdeglidar_ilhmpreasonableLiDAR
roof_aspect_degdeglidar_ilhmpreasonableLiDAR
roof_south_frac—lidar_ilhmpreasonableLiDAR
solar_potential_sqftsqftlidar_ilhmpunvalidatedLiDAR

Per-face solar

ColumnUnitsSourceValidationRequires
solar_tier—lidar_ilhmpreasonable—
solar_sqft_ssqftlidar_ilhmpreasonable—
solar_sqft_sewsqftlidar_ilhmpreasonable—
solar_sqft_allsqftlidar_ilhmpreasonable—
solar_flat_sqftsqftlidar_ilhmpreasonable—
solar_best_sqftsqftlidar_ilhmpreasonable—
solar_best_aspect_degdeglidar_ilhmpreasonable—
solar_best_slope_degdeglidar_ilhmpreasonable—
solar_facets_s—lidar_ilhmpreasonable—
solar_facets_sew—lidar_ilhmpreasonable—
solar_facets_all—lidar_ilhmpreasonable—
facet_count—derivedreasonable—
facet_area_sqftsqftderivedreasonable—
facet_south_sqftsqftderivedreasonable—
facet_south_frac—derivedreasonable—
facet_has_usable_south—derivedreasonable—
solar_shade_class—lidar_ilhmpreasonable—

Insurance

ColumnUnitsSourceValidationRequires
bldg_height_ftftlidar_ilhmp, overture_footprintsvalidatedLiDAR
bldg_src—lidar_ilhmpvalidatedLiDAR
roof_complexity_score—derivedunvalidatedLiDAR
tree_overhang_5ftpointslidar_ilhmpreasonableLiDAR
tree_overhang_30ftpointslidar_ilhmpreasonableLiDAR
defensible_space_pct%lidar_ilhmpreasonableLiDAR
tree_count—derivedunvalidatedLiDAR

Terrain and drainage

ColumnUnitsSourceValidationRequires
terrain_slope_degdeglidar_ilhmpreasonableLiDAR + parcel boundary
lot_relief_mmlidar_ilhmpreasonableLiDAR + parcel boundary
terrain_aspect_degdeglidar_ilhmpreasonableLiDAR + parcel boundary
grade_to_house_ftftlidar_ilhmpvalidatedLiDAR + parcel boundary

Vegetation and fuels

ColumnUnitsSourceValidationRequires
tree_canopy_frac—lidar_ilhmpreasonableLiDAR
max_tree_h_mmlidar_ilhmpreasonableLiDAR
roof_overhang_frac—lidar_ilhmpreasonableLiDAR
veg_low_frac—lidar_ilhmpreasonableLiDAR
veg_med_frac—lidar_ilhmpreasonableLiDAR
ladder_fuel_frac—lidar_ilhmpreasonableLiDAR

Pool confirmation

ColumnUnitsSourceValidationRequires
pool_void_sqftsqftlidar_ilhmpunvalidatedLiDAR + parcel boundary
pool_void_compactness—lidar_ilhmpunvalidatedLiDAR + parcel boundary

Roof reflectance

ColumnUnitsSourceValidationRequires
roof_int_p50—lidar_ilhmpreasonableLiDAR
roof_int_rel_p10—lidar_ilhmpreasonableLiDAR
roof_int_rel_p90—lidar_ilhmpreasonableLiDAR
roof_int_cv—lidar_ilhmpreasonableLiDAR
roof_int_patch_frac—lidar_ilhmpunvalidatedLiDAR

Yard features

ColumnUnitsSourceValidationRequires
npts_norm—lidar_ilhmpreasonableLiDAR + parcel boundary
bldg6_frac—lidar_ilhmpreasonableLiDAR + parcel boundary
veg5_frac—lidar_ilhmpreasonableLiDAR + parcel boundary
grd_frac—lidar_ilhmpreasonableLiDAR + parcel boundary
tall_frac—lidar_ilhmpreasonableLiDAR + parcel boundary
bldgh_frac—lidar_ilhmpreasonableLiDAR + parcel boundary
maxh—lidar_ilhmpreasonableLiDAR + parcel boundary
h_p50—lidar_ilhmpreasonableLiDAR + parcel boundary
h_p75—lidar_ilhmpreasonableLiDAR + parcel boundary
h_p90—lidar_ilhmpreasonableLiDAR + parcel boundary
h_std—lidar_ilhmpreasonableLiDAR + parcel boundary
b6_cover—lidar_ilhmpreasonableLiDAR + parcel boundary
b6_maxdist—lidar_ilhmpreasonableLiDAR + parcel boundary
b6_cnt_norm—lidar_ilhmpreasonableLiDAR + parcel boundary
b6_p90h—lidar_ilhmpreasonableLiDAR + parcel boundary
tall_cover—lidar_ilhmpreasonableLiDAR + parcel boundary

Errors

400Neither an address nor a lat/lon pair was supplied
401Missing, malformed, or revoked API key
402Free allowance exhausted and no payment method on file
404Address resolved, but the property is outside coverage
422Address could not be resolved to a parcel
429Rate 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.