Skip to content

Map tiles in MapLibre, Leaflet and QGIS

The tile routes render a preset, a named recipe of bands, stretch and colour map, as a mosaic of the scenes in a time window.

Available now

Preset Shows Sentinel-2 Landsat 8-9
true-color Natural colour the visual (TCI) asset red, green, blue
false-color Vegetation in red nir (B08), red, green nir08, red, green
ndvi Vegetation index, -0.2 to 0.8 on a red-yellow-green ramp nir (B08), red nir08, red

The recipes are the web map’s. On Sentinel-2 they use the 10 m broad NIR band, B08, rather than the cross-sensor nir08; see Presets.

Coming later ndwi, ndmi (moisture), nbr (burn ratio) and swir on optical collections, and hillshade and elevation on the DEM.

https://api.satellitedata.net/v1/tiles/{presetId}/{tileMatrixSetId}/{z}/{x}/{y}.{format}
?collection=sentinel-2-l2a
&datetime=2026-06-01T00:00:00Z/2026-08-31T23:59:59Z
&maxCloudCoverage=20
  • tileMatrixSetId: WebMercatorQuad for web maps, or WorldCRS84Quad.
  • format: png, jpg or webp (ndvi serves png and webp).
  • collection: sentinel-2-l2a (the default) or landsat-ot-l2, or an alias.
  • datetime: the window to mosaic over; the default is the last 30 days.
  • maxCloudCoverage: skip scenes with more cloud than this percentage; the default is the preset’s (30, or 60 for ndvi).
  • tilesize: 256 (default) or 512.

Each tile mosaics up to 50 scenes from the window, least cloudy first. There is no pixel cloud mask yet, so pick a window and ceiling that leave a clear scene on top.

Zoom ranges: Sentinel-2 8 to 15, Landsat 7 to 14. Outside them a tile answers 400 zoom-out-of-range.

Tiles need a key. The TileJSON document that describes them does not.

Terminal window
curl -s "https://api.satellitedata.net/v1/tiles/ndvi/WebMercatorQuad/tilejson.json?collection=landsat-ot-l2&datetime=2026-06-01T00:00:00Z/2026-08-31T23:59:59Z&maxCloudCoverage=40"

The answer is TileJSON 3.0.0. Its tiles template repeats your query parameters, so the map library requests exactly the mosaic you asked for, and it carries minzoom, maxzoom, bounds and attribution. See TileJSON.

MapLibre loads TileJSON as a raster source. Add the key to requests for the API with transformRequest:

const map = new maplibregl.Map({
container: 'map',
center: [-93.25, 44.95],
zoom: 11,
style: { version: 8, sources: {}, layers: [] },
transformRequest: (url) =>
url.startsWith('https://api.satellitedata.net/')
? { url, headers: { 'X-API-Key': apiKey } }
: { url },
});
map.on('load', () => {
map.addSource('ndvi', {
type: 'raster',
url:
'https://api.satellitedata.net/v1/tiles/ndvi/WebMercatorQuad/tilejson.json' +
'?collection=sentinel-2-l2a&datetime=2026-06-01T00:00:00Z/2026-08-31T23:59:59Z&maxCloudCoverage=20',
tileSize: 256,
});
map.addLayer({ id: 'ndvi', type: 'raster', source: 'ndvi' });
});

MapLibre shows the TileJSON attribution in its attribution control. A full page is in the Quickstart.

Leaflet’s L.tileLayer cannot send headers, so fetch each tile yourself:

const KeyedTileLayer = L.TileLayer.extend({
createTile(coords, done) {
const img = document.createElement('img');
img.alt = '';
fetch(this.getTileUrl(coords), { headers: { 'X-API-Key': apiKey } })
.then((r) => {
if (!r.ok) throw new Error(`tile ${r.status}`);
return r.blob();
})
.then((blob) => {
img.onload = () => URL.revokeObjectURL(img.src);
img.src = URL.createObjectURL(blob);
done(null, img);
})
.catch((err) => done(err, img));
return img;
},
});
const map = L.map('map').setView([44.95, -93.25], 11);
new KeyedTileLayer(
'https://api.satellitedata.net/v1/tiles/true-color/WebMercatorQuad/{z}/{x}/{y}.png' +
'?collection=sentinel-2-l2a&datetime=2026-06-01T00:00:00Z/2026-08-31T23:59:59Z&maxCloudCoverage=20',
{
minZoom: 8, // Sentinel-2: 8 to 15; Landsat: 7 to 14
maxZoom: 15,
attribution: 'Contains modified Copernicus Sentinel data 2026',
},
).addTo(map);

Set minZoom and maxZoom from the collection’s zoom range, which the TileJSON reports. The attribution line is required; see Attribution.

  1. In the Browser panel, right-click XYZ Tiles and choose New Connection.

  2. Paste the tile URL with {z}/{x}/{y} placeholders and your query parameters, for example:

    https://api.satellitedata.net/v1/tiles/false-color/WebMercatorQuad/{z}/{x}/{y}.png?collection=landsat-ot-l2&datetime=2026-06-01T00:00:00Z/2026-08-31T23:59:59Z&maxCloudCoverage=30
  3. Set the minimum and maximum zoom to the collection’s range (8 to 15 for Sentinel-2, 7 to 14 for Landsat).

  4. Add an HTTP header X-API-Key with your key. Recent QGIS versions have an HTTP headers section in the connection dialog; without it, QGIS cannot send the key.

  5. Double-click the connection to add the layer.

Coming later A WMTS capabilities document per preset (.../WMTSCapabilities.xml), so QGIS and ArcGIS Pro can add a layer from one URL.

  • Zoom. Tiles exist only within the collection’s zoom range.
  • Time windows. A short window may leave gaps where no scene passed; a long one costs more scenes to read per tile and is slower. A few weeks to a summer is a good start.
  • Rate limits. A pan or zoom asks for 20 to 60 tiles at once. The per-key limit of 50 requests a second allows for that; see Limits.
  • One scene, not a mosaic. Use item previews for a quick image of one scene, or the older tiles host, which renders single scenes by id.
  • harmonize levels beyond offset_removed, and a pixel cloud mask per preset.
  • Anonymous tiles served from the CDN cache, with cache headers by window age.