Sentinel-2 L2A
Available now Collection id sentinel-2-l2a. Aliases include sentinel-2-c1-l2a (Earth Search), S2L2A (Sentinel Hub) and SENTINEL2_L2A (CDSE openEO), matched without regard to case.
Overview
Section titled “Overview”Sentinel-2 is the Copernicus programme’s optical mission. Each satellite carries the MultiSpectral Instrument (MSI), which records 13 bands from visible light to shortwave infrared at 10, 20 and 60 m. Level-2A is bottom-of-atmosphere reflectance, produced by ESA’s Sen2Cor processor, which also writes a scene classification layer and cloud and snow probabilities.
satellitedata.net serves Collection 1, ESA’s consistent reprocessing of the archive, as Element 84 indexes it in Earth Search under sentinel-2-c1-l2a. The files are cloud-optimized GeoTIFFs in public S3 buckets in AWS us-west-2, the same region the API runs in. Item ids stay native, for example S2A_T31TCF_20260829T105044_L2A.
Mission references: ESA SentiWiki on the Sentinel-2 mission, its processing levels and products; the AWS registry entry for Sentinel-2 COGs.
Basic facts
Section titled “Basic facts”| Satellites | Sentinel-2A (launched 2015), 2B (2017) and 2C (2024); item platform is sentinel-2a, sentinel-2b or sentinel-2c |
| Spatial resolution | 10 m (B02, B03, B04, B08), 20 m (red edge, B8A, SWIR, SCL), 60 m (B01, B09) |
| Revisit | 5 days at the equator with two satellites in orbit, more often at higher latitudes |
| Swath | 290 km |
| Tiling | MGRS grid; each item covers one 110 × 110 km tile, such as 31TCF (item property grid:code = MGRS-31TCF) |
| Processing level | Level-2A, bottom-of-atmosphere reflectance (Sen2Cor), Collection 1 |
| Temporal extent in the API | From 27 June 2015, as the live collection document reports; see data availability for gaps |
| Zoom range for tiles | 8 to 15 |
| Credit line | Contains modified Copernicus Sentinel data {years} |
Spectral bands, with the wavelengths and bandwidths Earth Search publishes in each item’s eo:bands. ESA publishes slightly different spectral response figures per satellite; see SentiWiki for those.
| Band | Common name | Earth Search asset | Also accepted | Centre (nm) | Bandwidth (nm) | Resolution | Type | Scale | Offset | Nodata |
|---|---|---|---|---|---|---|---|---|---|---|
| B01 | coastal |
coastal |
CoastalAerosol |
443 | 27 | 60 m | uint16 | 0.0001 | −0.1 | 0 |
| B02 | blue |
blue |
B2 |
490 | 98 | 10 m | uint16 | 0.0001 | −0.1 | 0 |
| B03 | green |
green |
B3 |
560 | 45 | 10 m | uint16 | 0.0001 | −0.1 | 0 |
| B04 | red |
red |
B4 |
665 | 38 | 10 m | uint16 | 0.0001 | −0.1 | 0 |
| B05 | rededge071 |
rededge1 |
RedEdge1 |
704 | 19 | 20 m | uint16 | 0.0001 | −0.1 | 0 |
| B06 | rededge075 |
rededge2 |
RedEdge2 |
740 | 18 | 20 m | uint16 | 0.0001 | −0.1 | 0 |
| B07 | rededge078 |
rededge3 |
RedEdge3 |
783 | 28 | 20 m | uint16 | 0.0001 | −0.1 | 0 |
| B08 | nir |
nir |
NIR_Broad, B8 |
842 | 145 | 10 m | uint16 | 0.0001 | −0.1 | 0 |
| B8A | nir08 |
nir08 |
NIR_Narrow |
865 | 33 | 20 m | uint16 | 0.0001 | −0.1 | 0 |
| B09 | nir09 |
nir09 |
WaterVapor |
945 | 26 | 60 m | uint16 | 0.0001 | −0.1 | 0 |
| B11 | swir16 |
swir16 |
SWIR1 |
1610 | 143 | 20 m | uint16 | 0.0001 | −0.1 | 0 |
| B12 | swir22 |
swir22 |
SWIR2 |
2190 | 242 | 20 m | uint16 | 0.0001 | −0.1 | 0 |
Earth Search’s own common name for B05, B06 and B07 is rededge; the API uses the STAC 1.1 names rededge071, rededge075 and rededge078 so that each one is unique.
Class, probability and atmosphere layers:
| Layer | Earth Search asset | Also accepted | Content | Resolution | Type | Scale | Offset | Nodata |
|---|---|---|---|---|---|---|---|---|
| SCL | scl |
Scene classification, classes 0 to 11 | 20 m | uint8 | none | 0 | 0 | |
| CLD | cloud |
MSK_CLDPRB |
Cloud probability, 0 to 100 % | 20 m | uint8 | none | 0 | 0 |
| SNW | snow |
MSK_SNWPRB |
Snow probability, 0 to 100 % | 20 m | uint8 | none | 0 | 0 |
| AOT | aot |
Aerosol optical thickness at 550 nm | 20 m | uint16 | 0.001 | 0 | 0 | |
| WVP | wvp |
Water vapour column in cm | 20 m | uint16 | 0.001 | 0 | 0 |
Every name in the tables resolves in POST /v1/process: the native name, the common name, the asset name and the aliases, without regard to case. The GeoTIFF’s band descriptions are the Earth Search asset names.
B10, the 1,375 nm cirrus band, is not part of Level-2A. ESA uses it during atmospheric correction and keeps it only in Level-1C, which is coming later.
Units and values
Section titled “Units and values”Stored values are integers. Collection 1 reflectance is:
reflectance = DN × 0.0001 − 0.1The −0.1 offset is ESA’s BOA_ADD_OFFSET of −1,000, introduced with processing baseline 04.00 in January 2022. Collection 1 reprocessed the older scenes to the same convention, so every item in this collection carries the offset, and its raster:bands metadata says so. Do not choose the offset from the acquisition date.
What you get depends on three processing options in POST /v1/process:
units |
harmonize |
Spectral bands come back as | Sample type |
|---|---|---|---|
REFLECTANCE (default) |
either | DN × 0.0001 − 0.1 | FLOAT32, nodata NaN |
DN |
offset_removed (default) |
DN − 1,000, so DN = 10,000 × reflectance | INT16 |
DN |
none |
stored DN, with the scale and offset written into the GeoTIFF | UINT16, nodata 0 |
clipNegative: truesets converted values below zero to zero, which is what Sentinel Hub’sharmonizeValuesdoes. On the older download API the same switch isclamp: true.output.sampleTypeis optional. If you give it, it must match the table.INT16on its own selectsDNwithoffset_removed.- With
REFLECTANCE, AOT and WVP get their scale too (unitless and cm). SCL, CLD and SNW always come back as stored.
See Values, scale and offset and Harmonization levels.
Cloud and quality layers
Section titled “Cloud and quality layers”Each item has eo:cloud_cover, the percentage of the scene Sen2Cor flags as cloud. Filter scenes with it in search (query), tiles (maxCloudCoverage) and process (dataFilter.maxCloudCoverage).
For pixels, request SCL as a band:
| SCL | Class |
|---|---|
| 0 | No data |
| 1 | Saturated or defective pixel |
| 2 | Dark area or topographic shadow |
| 3 | Cloud shadow |
| 4 | Vegetation |
| 5 | Not vegetated |
| 6 | Water |
| 7 | Unclassified |
| 8 | Cloud, medium probability |
| 9 | Cloud, high probability |
| 10 | Thin cirrus |
| 11 | Snow or ice |
A common clear-pixel rule keeps classes 4, 5 and 6. Class 7 is a judgement call; Sentinel Hub and Earth Engine treat it as cloud. Sen2Cor draws no buffer around clouds, so growing the cloud and shadow areas by a few pixels helps at the edges.
CLD and SNW give Sen2Cor’s cloud and snow probability per pixel, 0 to 100, if you want your own threshold instead of SCL’s classes.
Lean v1 applies no pixel cloud mask: tiles and previews show every pixel of the scenes they mosaic, and processing.cloudMask is rejected. The unified cloud mask, built from SCL with optional dilation, is planned.
Data availability
Section titled “Data availability”Earth Search rebuilt its Sentinel-2 index as sentinel-2-c1-l2a from ESA’s Collection 1. On 16 October 2026 we compared it with Earth Search’s original sentinel-2-l2a over two test areas, in Borneo (about 117.9 to 118.2 E, 1.5 to 1.7 N) and Sulawesi (about 120.0 to 120.2 E, 3.2 to 3.4 S). Collection 1 was complete for 2020 to 2021 and 2023 to 2026. It was missing these scenes:
| Year | Borneo: missing of original | Sulawesi: missing of original |
|---|---|---|
| 2017 | 31 of 31 | 20 of 21 |
| 2018 | 75 of 115 | 39 of 59 |
| 2019 | 30 of 144 | 15 of 73 |
| 2022 | 132 of 144 | 67 of 73 |
These are gaps observed in the source collection over two small areas, not a global figure. The gaps may be smaller elsewhere, and they may close as reprocessing continues. Before relying on a period, run /v1/search for your area and dates and count what comes back.
Values before 2022. Scenes acquired before baseline 04.00 are ESA’s reprocessed product, with newer atmospheric correction and geometry than the products first published for those dates. Their values can differ from originally processed products, such as Sentinel Hub’s, by up to about 0.02 reflectance. From baseline 04.00 (2022) on, the two match.
Collection-specific parameters
Section titled “Collection-specific parameters”Search
Section titled “Search”Lean v1 search takes the STAC query extension, which Earth Search evaluates. CQL2 filter answers invalid-filter. These properties work in query:
| Property | Example | Notes |
|---|---|---|
eo:cloud_cover |
{"eo:cloud_cover": {"lte": 20}} |
Scene cloud percentage |
platform |
{"platform": {"in": ["sentinel-2b", "sentinel-2c"]}} |
One satellite or several |
grid:code |
{"grid:code": {"eq": "MGRS-31TCF"}} |
One MGRS tile. Earth Search’s Collection 1 items have no s2:mgrs_tile; they carry grid:code and mgrs:utm_zone, mgrs:latitude_band, mgrs:grid_square |
Each item also carries s2:processing_baseline, such as 05.12.
Presets
Section titled “Presets”From GET /v1/presets, the web map’s recipes for this collection:
| Preset | Assets | Stretch |
|---|---|---|
true-color |
visual (the 8-bit TCI image) |
as stored |
false-color |
nir (B08), red, green |
DN 1,000 to 5,000, reflectance 0 to 0.4 |
ndvi |
nir (B08), red |
(b1 - b2) / (b1 + b2 - 2000), −0.2 to 0.8 on rdylgn |
The NDVI expression subtracts the two 1,000 offsets in the denominator; the offsets cancel in the numerator. Default cloud ceiling 30 % (60 % for ndvi), least cloudy scene first, a 30-day window.
Tiles exist from zoom 8 to 15. Outside that range a tile answers 400 zoom-out-of-range.
Examples
Section titled “Examples”Routes marked anonymous need no key. The others send your key in X-API-Key; see Authentication. The examples use the area west of Tarragona, Spain, and August 2026.
Collection document
Section titled “Collection document”Anonymous.
curl -s https://api.satellitedata.net/v1/collections/sentinel-2-l2aThe document lists the bands from schema v1, sdn:aliases, sdn:attribution and sdn:zoom. /v1/collections/S2L2A returns the same document with a Content-Location header.
Search one area and month
Section titled “Search one area and month”curl -s -X POST https://api.satellitedata.net/v1/search \ -H "X-API-Key: $SDN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "collections": ["sentinel-2-l2a"], "bbox": [0.7, 40.7, 0.8, 40.8], "datetime": "2026-08-01T00:00:00Z/2026-08-31T23:59:59Z", "limit": 10, "query": {"eo:cloud_cover": {"lte": 20}} }'import osimport requests
API = "https://api.satellitedata.net/v1"headers = {"X-API-Key": os.environ["SDN_API_KEY"]}
body = { "collections": ["sentinel-2-l2a"], "bbox": [0.7, 40.7, 0.8, 40.8], "datetime": "2026-08-01T00:00:00Z/2026-08-31T23:59:59Z", "limit": 10, "query": {"eo:cloud_cover": {"lte": 20}},}page = requests.post(f"{API}/search", json=body, headers=headers).json()for item in page["features"]: p = item["properties"] print(item["id"], p["datetime"], p["eo:cloud_cover"], p["s2:processing_baseline"])The next link of a POST search has method: POST and a body holding only token; merge it into your body to get the next page. pystac-client does that for you; see Search.
One item
Section titled “One item”Anonymous.
curl -s https://api.satellitedata.net/v1/collections/sentinel-2-l2a/items/S2A_T31TCF_20260829T105044_L2AAsset href values point at the public COGs, which you can read directly without a key.
Preview image
Section titled “Preview image”curl -s -o preview.png \ -H "X-API-Key: $SDN_API_KEY" \ "https://api.satellitedata.net/v1/collections/sentinel-2-l2a/items/S2A_T31TCF_20260829T105044_L2A/preview.png?preset=true-color&max_size=256"preset is true-color, false-color or ndvi; max_size is the long edge in pixels, 64 to 1,024. The preset’s formats in /v1/presets lists the image formats it serves (ndvi has no JPEG).
TileJSON and a tile URL for MapLibre
Section titled “TileJSON and a tile URL for MapLibre”The TileJSON is anonymous; the tiles need the key.
curl -s "https://api.satellitedata.net/v1/tiles/true-color/WebMercatorQuad/tilejson.json?collection=sentinel-2-l2a&datetime=2026-08-01/2026-08-31&maxCloudCoverage=20"{ "tilejson": "3.0.0", "name": "true-color", "attribution": "Contains modified Copernicus Sentinel data 2026", "tiles": ["https://api.satellitedata.net/v1/tiles/true-color/WebMercatorQuad/{z}/{x}/{y}.png?collection=sentinel-2-l2a&datetime=2026-08-01%2F2026-08-31&maxCloudCoverage=20"], "minzoom": 8, "maxzoom": 15}(Shortened.) One tile from that template, at zoom 12 over Tarragona’s hinterland:
curl -s -o tile.png -H "X-API-Key: $SDN_API_KEY" \ "https://api.satellitedata.net/v1/tiles/true-color/WebMercatorQuad/12/2055/1534.png?collection=sentinel-2-l2a&datetime=2026-08-01%2F2026-08-31&maxCloudCoverage=20"In MapLibre, give the TileJSON URL as a raster source and add the key with transformRequest, as in the Quickstart and Map tiles.
True colour as a reflectance GeoTIFF
Section titled “True colour as a reflectance GeoTIFF”A 2 km box from the scenes of 29 August 2026, in UTM zone 31N. REFLECTANCE is the default; it is written out here for clarity.
curl -s -L -X POST https://api.satellitedata.net/v1/process \ -H "X-API-Key: $SDN_API_KEY" \ -H "Content-Type: application/json" \ -o truecolor.tif \ -d '{ "input": { "bounds": {"bbox": [0.73815, 40.71096, 0.76185, 40.72904]}, "data": [{ "type": "sentinel-2-l2a", "dataFilter": {"timeRange": {"from": "2026-08-29T00:00:00Z", "to": "2026-08-29T23:59:59Z"}}, "processing": {"units": "REFLECTANCE"} }] }, "bands": ["red", "green", "blue"], "output": {"width": 200, "height": 150, "crs": "EPSG:32631"} }'import ioimport osimport rasterioimport requests
API = "https://api.satellitedata.net/v1"headers = {"X-API-Key": os.environ["SDN_API_KEY"]}
body = { "input": { "bounds": {"bbox": [0.73815, 40.71096, 0.76185, 40.72904]}, "data": [{ "type": "sentinel-2-l2a", "dataFilter": {"timeRange": {"from": "2026-08-29T00:00:00Z", "to": "2026-08-29T23:59:59Z"}}, "processing": {"units": "REFLECTANCE"}, }], }, "bands": ["red", "green", "blue"], "output": {"width": 200, "height": 150, "crs": "EPSG:32631"},}r = requests.post(f"{API}/process", json=body, headers=headers) # follows a 303 for large outputsr.raise_for_status()with rasterio.open(io.BytesIO(r.content)) as ds: print(ds.descriptions, ds.dtypes[0], ds.crs) # ('red', 'green', 'blue') float32 EPSG:32631NDVI inputs as harmonized integers
Section titled “NDVI inputs as harmonized integers”red and nir08 as INT16, where DN = 10,000 × reflectance on every baseline. sampleType: "INT16" selects units: "DN" with harmonize: "offset_removed".
curl -s -L -X POST https://api.satellitedata.net/v1/process \ -H "X-API-Key: $SDN_API_KEY" \ -H "Content-Type: application/json" \ -o ndvi-inputs.tif \ -d '{ "input": { "bounds": {"bbox": [0.73815, 40.71096, 0.76185, 40.72904]}, "data": [{ "type": "S2L2A", "dataFilter": {"timeRange": {"from": "2026-08-29T00:00:00Z", "to": "2026-08-29T23:59:59Z"}} }] }, "bands": ["red", "nir08"], "output": {"width": 200, "height": 150, "crs": "EPSG:32631", "sampleType": "INT16"} }'Then NDVI is (nir08 - red) / (nir08 + red) on the integers directly, because the offset is already gone. Add "processing": {"units": "DN", "clipNegative": true} to the data source for Sentinel Hub’s harmonized values.
A larger area as a job
Section titled “A larger area as a job”Synchronous process stops at 2,500 pixels a side. For more, send the same body to /v1/process/jobs and poll. This one is a 20 km box at 5 m.
curl -s -i -X POST https://api.satellitedata.net/v1/process/jobs \ -H "X-API-Key: $SDN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "input": { "bounds": {"bbox": [0.63147, 40.62956, 0.86853, 40.81044]}, "data": [{ "type": "sentinel-2-l2a", "dataFilter": {"ids": ["S2A_T31TCF_20260829T105044_L2A"]} }] }, "bands": ["blue", "green", "red", "nir08"], "output": {"width": 4000, "height": 4000, "crs": "EPSG:32631"} }'
# 202 Accepted, with Location: https://api.satellitedata.net/v1/process/jobs/{jobId}curl -s -H "X-API-Key: $SDN_API_KEY" https://api.satellitedata.net/v1/process/jobs/$JOB_IDimport osimport timeimport requests
API = "https://api.satellitedata.net/v1"headers = {"X-API-Key": os.environ["SDN_API_KEY"]}
body = { "input": { "bounds": {"bbox": [0.63147, 40.62956, 0.86853, 40.81044]}, "data": [{"type": "sentinel-2-l2a", "dataFilter": {"ids": ["S2A_T31TCF_20260829T105044_L2A"]}}], }, "bands": ["blue", "green", "red", "nir08"], "output": {"width": 4000, "height": 4000, "crs": "EPSG:32631"},}r = requests.post(f"{API}/process/jobs", json=body, headers=headers)r.raise_for_status()job_url = r.headers["Location"]
while True: job = requests.get(job_url, headers=headers).json() if job["status"] in ("done", "failed"): break time.sleep(5)
if job["status"] == "done": for f in job["files"]: print(f["url"]) # valid for one hour; poll again for fresh linkselse: print(job["error"])A job goes queued, running, then done or failed. See Process jobs for the limits.