Export API
Host: https://data.topoexport.com. The interactive reference of every endpoint and schema is served at the same address: data.topoexport.com.
Authentication
Send your secret key as a bearer token, from your server only:
curl -H "Authorization: Bearer $TOPOEXPORT_SECRET_KEY" "https://data.topoexport.com/usage"
The secret key is in the Export API tab of Services & API in the TopoExport app, where you can also regenerate it.
Workflow
An export takes two requests: find the datasets that cover a place, then download one of them over a bounding box.
1. Find datasets for a location
GET /lookup?lat={lat}&lon={lon}
GET /lookup?bbox={min_lon},{min_lat},{max_lon},{max_lat}
The response gives the deepest region containing the point (or the center of the bounding box) and the best datasets available there, one per type. Abridged:
{
"region": {
"id": "…",
"isoCode": "FR-METRO",
"name": "Metropolitan France",
"localName": "France métropolitaine"
},
"datasets": [
{
"id": "…",
"type": "BUILDINGS_3D",
"minYear": 2024,
"maxYear": 2025,
"precision": 2,
"srids": [2154],
"lookupSrid": 2154,
"regionIsoCode": "FR-METRO",
"product": {
"name": "BD TOPO",
"licence": "CC BY 4.0",
"publisher": { "name": "Institut national de l'information géographique et forestière", "shortName": "IGN" }
}
}
]
}
type:BUILDINGS_2D,BUILDINGS_3D,PARCELS,ROADS,RAILWAYS,WATERWAYS,GREEN_AREAS,TREES,HEDGES,CANOPY,RASTER_DTM(terrain),RASTER_DSM(surface), among others.precision: the level of detail forBUILDINGS_3D, the grid resolution forRASTER_DTM.lookupSrid: the coordinate reference system (EPSG code) best suited to this location. Pass it astargetSridin step 2.product: the source of the data, its publisher and its licence. Credit them as the licence requires.
2. Download features
GET /datasets/{id}/features?bbox={min_lon},{min_lat},{max_lon},{max_lat}&targetSrid={epsg}&format={format}
curl -H "Authorization: Bearer $TOPOEXPORT_SECRET_KEY" \
-o buildings.geojson \
"https://data.topoexport.com/datasets/$DATASET_ID/features?bbox=2.341735,48.852743,2.351291,48.857090&targetSrid=2154&format=geojson"
import { writeFile } from "node:fs/promises";
const params = new URLSearchParams({
bbox: "2.341735,48.852743,2.351291,48.857090",
targetSrid: "2154",
format: "glb",
});
const response = await fetch(`https://data.topoexport.com/datasets/${datasetId}/features?${params}`, {
headers: { Authorization: `Bearer ${process.env.TOPOEXPORT_SECRET_KEY}` },
});
if (!response.ok) throw new Error(`Export failed: HTTP ${response.status}`);
await writeFile("buildings.glb", Buffer.from(await response.arrayBuffer()));
import os
import requests
response = requests.get(
f"https://data.topoexport.com/datasets/{dataset_id}/features",
params={"bbox": "2.341735,48.852743,2.351291,48.857090", "targetSrid": 2154, "format": "geojson"},
headers={"Authorization": f"Bearer {os.environ['TOPOEXPORT_SECRET_KEY']}"},
timeout=300,
)
response.raise_for_status()
open("buildings.geojson", "wb").write(response.content)
The response body is the file itself, with the Content-Type of its format.
| Parameter | Required | Meaning |
|---|---|---|
bbox | yes | min_lon,min_lat,max_lon,max_lat in WGS 84 degrees. At most 100 km² per request |
targetSrid | yes | EPSG code of the output coordinates, usually the lookupSrid of step 1 |
format | yes | geojson, glb, obj, tiff or png, depending on the dataset type (see Formats) |
resolution | no | Grid resolution of raster output, in metres. Defaults to the dataset's resolution |
lod | no | Level of detail of 3D buildings: 1 or 2 |
terrainDatasetId | no | A RASTER_DTM dataset id: 3D output is placed on this terrain |
buildingDefaultHeight | no | Height in metres given to buildings without one. Default 6 |
treeDefaultHeight | no | Height in metres given to trees without one. Default 6 |
geolocate | no | true (default): coordinates in targetSrid. false: coordinates relative to the south-west corner of the bounding box |
outlines | no | Roads only: true returns road surfaces instead of centrelines. Default false |
stepContour | no | Terrain in glb only: returns contour lines at this interval, in metres |
Complete example
Find the 3D buildings dataset that covers an area, then download it as glTF in the coordinate system suited to that location. A location with no 3D buildings dataset gets a clear message instead of an error.
BBOX="2.341735,48.852743,2.351291,48.857090"
AUTH="Authorization: Bearer $TOPOEXPORT_SECRET_KEY"
# 1. datasets covering the bounding box (jq picks the 3D buildings)
read -r DATASET_ID SRID < <(
curl -sf -H "$AUTH" "https://data.topoexport.com/lookup?bbox=$BBOX" |
jq -r '.datasets[] | select(.type == "BUILDINGS_3D") | "\(.id) \(.lookupSrid)"' | head -n 1
)
[ -n "$DATASET_ID" ] || { echo "No 3D buildings dataset here"; exit 1; }
# 2. features of that dataset over the same bounding box
curl -sf -H "$AUTH" -o buildings.glb \
"https://data.topoexport.com/datasets/$DATASET_ID/features?bbox=$BBOX&targetSrid=$SRID&format=glb&lod=2"
import os
import requests
API = "https://data.topoexport.com"
HEADERS = {"Authorization": f"Bearer {os.environ['TOPOEXPORT_SECRET_KEY']}"}
BBOX = "2.341735,48.852743,2.351291,48.857090"
# 1. datasets covering the bounding box
lookup = requests.get(f"{API}/lookup", params={"bbox": BBOX}, headers=HEADERS, timeout=30)
lookup.raise_for_status()
dataset = next((d for d in lookup.json()["datasets"] if d["type"] == "BUILDINGS_3D"), None)
if dataset is None:
raise SystemExit("No 3D buildings dataset here")
product = dataset["product"]
print(f"{product['name']} by {product['publisher']['name']}, licence {product['licence']}")
# 2. features of that dataset over the same bounding box
export = requests.get(
f"{API}/datasets/{dataset['id']}/features",
params={"bbox": BBOX, "targetSrid": dataset["lookupSrid"], "format": "glb", "lod": 2},
headers=HEADERS,
timeout=300,
)
export.raise_for_status()
with open("buildings.glb", "wb") as file:
file.write(export.content)
import { writeFile } from "node:fs/promises";
const API = "https://data.topoexport.com";
const headers = { Authorization: `Bearer ${process.env.TOPOEXPORT_SECRET_KEY}` };
const bbox = "2.341735,48.852743,2.351291,48.857090";
// 1. datasets covering the bounding box
const lookup = await fetch(`${API}/lookup?${new URLSearchParams({ bbox })}`, { headers });
if (!lookup.ok) throw new Error(`Lookup failed: HTTP ${lookup.status}`);
const dataset = (await lookup.json()).datasets.find((d) => d.type === "BUILDINGS_3D");
if (!dataset) throw new Error("No 3D buildings dataset here");
const { name, licence, publisher } = dataset.product;
console.log(`${name} by ${publisher.name}, licence ${licence}`);
// 2. features of that dataset over the same bounding box
const params = new URLSearchParams({ bbox, targetSrid: String(dataset.lookupSrid), format: "glb", lod: "2" });
const exported = await fetch(`${API}/datasets/${dataset.id}/features?${params}`, { headers });
if (!exported.ok) throw new Error(`Export failed: HTTP ${exported.status}`);
await writeFile("buildings.glb", Buffer.from(await exported.arrayBuffer()));
The secret key must stay on your server: call the Export API from a backend or a script, never from a web page.
Formats
| Dataset type | geojson | glb | obj | tiff | png |
|---|---|---|---|---|---|
BUILDINGS_2D | ✓ | ||||
BUILDINGS_3D | ✓ | ✓ | ✓ | ||
PARCELS, RAILWAYS | ✓ | ||||
ROADS, WATERWAYS, GREEN_AREAS, TREES, HEDGES, CANOPY | ✓ | ✓ | |||
RASTER_DTM | ✓ | ✓ | ✓ | ||
RASTER_DSM | ✓ | ✓ |
Media types: application/geo+json, model/gltf-binary, model/obj, image/tiff, image/png. Another combination answers 400.
Other endpoints
| Endpoint | Returns |
|---|---|
GET /datasets/{id} | One dataset, same fields as in /lookup |
GET /regions/lookup?lat=&lon= | The region containing a point |
GET /regions/{id} | One region |
GET /products/{id} | One product (name, licence, publisher) |
GET /publishers/{id} | One publisher |
GET /usage | clientId, maxCredits and usedCredits of your key for the current period |
Credits
An export request costs, in export credits:
hectares of the bounding box (rounded up) × 2 for glb × the dataset's multiplier
- One credit is one hectare of one layer: 1 km² of buildings in GeoJSON costs 100 credits, the same area in
glbcosts 200. - The multiplier is 1 for most datasets; some, such as AI-detected trees, count double.
- Only successful responses are billed.
usedCreditsin/usagecan lag a few minutes behind your actual consumption.
| API Trial | API | Enterprise | |
|---|---|---|---|
| Map credits (tiles + geocoding) | 2,000,000 for the trial | 5,000,000 per month | Custom |
| Beyond the map credits | Requests refused (429) | €3, $3 or £3 per 1,000,000 credits, up to your cap | Custom |
| Export API credits | 4,000 for the trial | 4,000 per month | Custom |
| Beyond the export credits | Requests refused (403) | €0.003, $0.003 or £0.003 per credit, up to your cap | Custom |
| API keys | 1 | 10 | Custom |
| Commercial use | No | Yes | Yes |
| Attribution required | Yes | Yes | No |
On the API plan, credits beyond the included ones are billed up to the monthly spending cap you set in the Export API tab. When the cap is reached, or the trial credits are used up, export requests answer 403 until the next period.
Errors
| Status | Meaning |
|---|---|
400 | Invalid parameter: bounding box over 100 km² or malformed, format not available for this dataset, lod other than 1 or 2, stepContour not positive |
401 | Missing or invalid key |
403 | Export credits used up for the period |
404 | Unknown dataset, region, product or publisher, or no region at this location |
Geocoding
Autocomplete, search, structured search, reverse geocoding and place lookup worldwide, with Pelias-compatible parameters and GeoJSON responses.
Migration prompts
Prompts for coding assistants that move a project from Mapbox, MapTiler, Stadia Maps or Google Maps Platform to the TopoExport APIs, where an equivalent exists.