GET /v1/grid
A rectangular grid of terrain data over a bounding box. Resample elevation (Copernicus GLO-30) or light pollution (VIIRS) across a grid with fixed spatial resolution. Cells may be null where data is unavailable. The grid's row 0 is the northernmost edge.
Request
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
dataset | string | yes | — | elevation, light-pollution, or terrain. |
minLat | float | yes | — | Minimum latitude of the bounding box, −90..90. |
minLng | float | yes | — | Minimum longitude of the bounding box, −180..180. |
maxLat | float | yes | — | Maximum latitude of the bounding box, −90..90. |
maxLng | float | yes | — | Maximum longitude of the bounding box, −180..180. |
cols | int | yes | — | Number of grid columns (West–East), 1–256. |
rows | int | yes | — | Number of grid rows (South–North), 1–256. Constraint: cols × rows ≤ 4096. |
interpolation | string | no | bilinear | bilinear or nearest. |
lightAzimuthDeg | float | no | — | Light source azimuth. 0 = N, clockwise, [0, 360). Optional; supply together with lightAltitudeDeg or neither. Only affects dataset=terrain (adds the illumination band); ignored by other datasets. |
lightAltitudeDeg | float | no | — | Light source altitude above the horizon, −90..90. Optional; supply together with lightAzimuthDeg or neither. Only affects dataset=terrain (adds the illumination band); ignored by other datasets. |
Providing only one of lightAzimuthDeg / lightAltitudeDeg returns 400 with incomplete_light_params, regardless of dataset. The pair only affects the response for dataset=terrain (it is accepted but has no effect for elevation / light-pollution); omit both to get aspect_deg / slope_deg without illumination.
Examples
curl -H "X-API-Key: gi_live_..." \
"https://geoinsight.dev/v1/grid?dataset=elevation&minLat=52.0&minLng=14.0&maxLat=52.5&maxLng=14.5&cols=32&rows=32"
import requests
r = requests.get(
"https://geoinsight.dev/v1/grid",
params={
"dataset": "elevation",
"minLat": 52.0, "minLng": 14.0,
"maxLat": 52.5, "maxLng": 14.5,
"cols": 32, "rows": 32,
},
headers={"X-API-Key": "gi_live_..."},
)
data = r.json()
print(data["bands"]["elevation"]["values"][0])
const res = await fetch(
"https://geoinsight.dev/v1/grid?dataset=elevation&minLat=52.0&minLng=14.0&maxLat=52.5&maxLng=14.5&cols=32&rows=32",
{ headers: { "X-API-Key": "gi_live_..." } },
);
const data = await res.json();
console.log(data.bands.elevation.values[0]);
Response
{
"dataset": "elevation",
"bbox": {"minLat": 52.0, "minLng": 14.0, "maxLat": 52.5, "maxLng": 14.5},
"rows": 32,
"cols": 32,
"cell_deg": {"lat": 0.0156, "lng": 0.0156},
"resolution_m": 30,
"bands": {
"elevation": {
"units": "m",
"values": [
[52.1, 51.8, 52.3, null, 51.9],
[53.2, 52.7, 52.1, 52.4, 52.0]
]
}
}
}
| Field | Type | Unit | Description |
|---|---|---|---|
dataset |
string | — | Which dataset was sampled: elevation, light-pollution or terrain. Determines the shape of bands. |
bbox.minLat |
number | deg | Southern edge of the sampled box, echoed from the request. |
bbox.minLng |
number | deg | Western edge of the sampled box. |
bbox.maxLat |
number | deg | Northern edge of the sampled box. |
bbox.maxLng |
number | deg | Eastern edge of the sampled box. |
rows |
integer | count | Number of grid rows returned; equals the length of every values matrix. |
cols |
integer | count | Number of grid columns; equals the length of every row inside values. |
cell_deg.lat |
number | deg | Latitude span of a single cell. Multiply by ~111 320 m for an approximate cell height in metres. |
cell_deg.lng |
number | deg | Longitude span of a single cell. Metric width shrinks with cos(latitude), so cells are not square away from the equator. |
resolution_m |
integer | m | Ground sample distance of the underlying raster (30 for elevation/terrain, 463 for light-pollution). If cell_deg is finer than this, neighbouring cells repeat the same source pixel. |
bands |
object | — | Sampled data, keyed by band name. Shape differs by dataset — see the two rows below. |
bands.<name>.units |
string | — | elevation and light-pollution only: unit label for the band (m, nW/cm2/sr, bortle 1-9). |
bands.<name>.values |
array | per band | elevation and light-pollution only: 2D matrix [rows][cols]. Row 0 is the northernmost. Cells are null where the source has no data. |
bands.<name> |
array | per band | terrain only: the 2D matrix directly, with NO units/values wrapper. Bands: aspect_deg, slope_deg and — only when light params are supplied — illumination. |
Datasets
Available datasets:
elevation— Copernicus GLO-30 (global coverage, 30 m resolution).light-pollution— VIIRS VNL Annual V2 (seeGET /v1/light-pollutionfor attribution and caveats).terrain— per-cell aspect/slope (and optional illumination) derived from Copernicus GLO-30, 30 m resolution. See "Terrain dataset" below.
Light-pollution dataset: radiance & Bortle
With dataset=light-pollution the grid returns two bands with the same units/values shape as elevation: radiance_nw_cm2_sr (the raw VIIRS night-lights radiance) and estimated_bortle (a 1–9 Bortle class derived from it). Source resolution is 463 m, so a cell_deg finer than that repeats the same source pixel across neighbouring cells.
Examples (light-pollution)
curl -H "X-API-Key: gi_live_..." \
"https://geoinsight.dev/v1/grid?dataset=light-pollution&minLat=52.20&minLng=20.90&maxLat=52.30&maxLng=21.05&cols=2&rows=2"
import requests
r = requests.get(
"https://geoinsight.dev/v1/grid",
params={"dataset": "light-pollution", "minLat": 52.20, "minLng": 20.90,
"maxLat": 52.30, "maxLng": 21.05, "cols": 2, "rows": 2},
headers={"X-API-Key": "gi_live_..."},
)
print(r.json()["bands"]["estimated_bortle"]["values"])
const res = await fetch(
"https://geoinsight.dev/v1/grid?dataset=light-pollution&minLat=52.20&minLng=20.90&maxLat=52.30&maxLng=21.05&cols=2&rows=2",
{ headers: { "X-API-Key": "gi_live_..." } },
);
const { bands } = await res.json();
console.log(bands.estimated_bortle.values);
Response (light-pollution)
{
"dataset": "light-pollution",
"bbox": {"minLat": 52.20, "minLng": 20.90, "maxLat": 52.30, "maxLng": 21.05},
"rows": 2,
"cols": 2,
"cell_deg": {"lat": 0.05, "lng": 0.075},
"resolution_m": 463,
"bands": {
"radiance_nw_cm2_sr": {
"units": "nW/cm2/sr",
"values": [
[0.25, null],
[2.5, 48.0]
]
},
"estimated_bortle": {
"units": "bortle 1-9",
"values": [
[4, null],
[5, 9]
]
}
}
}
Each cell of estimated_bortle is derived from the matching radiance_nw_cm2_sr cell — here 0.25 → rural Bortle 4, 2.5 → suburban 5, 48 → inner-city 9; a null radiance (no VIIRS data) gives a null Bortle. The Bortle mapping is a heuristic — see the light-pollution endpoint for the estimate's caveats and attribution.
Terrain dataset: aspect, slope & illumination
With dataset=terrain, the grid returns aspect_deg and slope_deg bands and, when lightAzimuthDeg/lightAltitudeDeg are also given, an illumination band. Unlike the elevation / light-pollution bands above, terrain bands are 2D value matrices directly — there is no units/values wrapper object.
Examples (terrain)
curl -H "X-API-Key: gi_live_..." \
"https://geoinsight.dev/v1/grid?dataset=terrain&minLat=45.80&minLng=6.80&maxLat=45.82&maxLng=6.82&cols=2&rows=2&lightAzimuthDeg=100&lightAltitudeDeg=8.5"
import requests
r = requests.get(
"https://geoinsight.dev/v1/grid",
params={
"dataset": "terrain",
"minLat": 45.80, "minLng": 6.80,
"maxLat": 45.82, "maxLng": 6.82,
"cols": 2, "rows": 2,
"lightAzimuthDeg": 100, "lightAltitudeDeg": 8.5,
},
headers={"X-API-Key": "gi_live_..."},
)
data = r.json()
print(data["bands"]["illumination"][0])
const res = await fetch(
"https://geoinsight.dev/v1/grid?dataset=terrain&minLat=45.80&minLng=6.80&maxLat=45.82&maxLng=6.82&cols=2&rows=2&lightAzimuthDeg=100&lightAltitudeDeg=8.5",
{ headers: { "X-API-Key": "gi_live_..." } },
);
const data = await res.json();
console.log(data.bands.illumination[0]);
Response (terrain)
{
"dataset": "terrain",
"bbox": {"minLat": 45.8, "minLng": 6.8, "maxLat": 45.82, "maxLng": 6.82},
"rows": 2,
"cols": 2,
"cell_deg": {"lat": 0.01, "lng": 0.01},
"resolution_m": 30,
"bands": {
"aspect_deg": [
[92.4, null],
[100.0, 5.0]
],
"slope_deg": [
[31.7, null],
[20.0, 2.0]
],
"illumination": [
[0.6409, null],
[0.0, 0.1447]
]
}
}
Terrain band fields:
aspect_deg— the compass direction the slope faces (0 = N, clockwise).nullon flat terrain (no defined downhill direction) or where elevation data is missing (nodata) — in the example above, row 0 / col 1 is a nodata cell (bothaspect_degandslope_degarenull).slope_deg— steepness of the terrain, 0 = flat.nullwhere elevation data is missing (nodata).illumination— present only whenlightAzimuthDeg/lightAltitudeDegare given; how directly each cell faces the given light source, from 0 (facing away / grazing / in shadow) to 1 (directly facing),nullon nodata cells.illuminationaccounts for terrain cast shadows (a ridge blocking the sun), unlike the local-only/v1/sun-exposure; a shadowed cell →0. In the example above, row 1 / col 0 (aspect_deg: 100.0, nearly facing the light directly) would compute to roughly0.48from slope/aspect geometry alone, but the cell sits behind a taller ridge, so the actualilluminationis forced to0.0— a cast shadow.
Constraints
cols × rows ≤ 4096— maximum 4,096 cells per call.interpolationmay bebilinear(smooth, default) ornearest(sharp).