EO — Tool Reference
Earth observation via Sentinel Hub: satellite scene search, rendered imagery (true color, NDVI, NDWI), and AOI statistics from Sentinel-2. Works with a free Copernicus Data Space account
Your AI assistant discovers and invokes these tools through the
MCP Server's meta tools —
it calls execute_tool with the tool name and arguments below.
# get_statistics read
Compute spectral-index time-series statistics over an AOI (NDVI, NDWI, NBR, NDMI, NDSI, NDRE, NBR2): mean or glint-robust median compositing, optional spatial percentiles, no imagery download.
Full description
Compute a spectral-index time series over an AOI (Statistical API). Returns per-interval mean/min/max/std-dev of one normalized-difference index, as JSON, ideal for questions like "when was this field greenest last year", "how severely did that fire burn", or "how did the reservoir's water extent evolve". Daily intervals return one entry per satellite pass (gaps are normal); monthly intervals are cheaper and smoother. max_cloud_cover filters whole scenes by metadata cloud percentage (where the collection supports scene cloud metadata; otherwise it is ignored and a note may be returned); residual clouds in accepted scenes still skew interval means, and stricter values mean more intervals are omitted from the response entirely, so treat suspicious values (for example negative NDWI over open water) as possible cloud contamination. Intervals start at time_from and step by the interval length; a trailing partial interval is included, shortened to end at time_to (so March 1 - June 30 monthly yields four intervals, the last covering June 1-30). Requested intervals with no usable data are omitted from intervals: monthly responses list them in missing_intervals so the series can be re-aligned, daily responses count them in days_without_acquisition. The area can be inline GeoJSON (`aoi`), a `bbox`, or an `aoi_ref` handle minted by the geo toolkit; refs resolve server-side to the stored full-fidelity geometry, so statistics can run on boundaries too large to pass inline (refs expire after about 48 hours). Indices (all normalized differences in -1..1): ndvi (NIR-red)/(NIR+red) for greenness; ndwi (green-NIR)/(green+NIR) for surface water; nbr (narrow NIR-SWIR2)/(narrow NIR+SWIR2) for burn severity, the standard wildfire index — take the difference of a pre- and post-fire call for dNBR; ndmi (NIR-SWIR1)/(NIR+SWIR1) for vegetation water content, drought and fire risk; ndsi (green-SWIR1)/(green+SWIR1) for snow versus cloud (snow is typically above ~0.42); ndre (narrow NIR-red edge)/(narrow NIR+red edge) for crop stress, which shows earlier than ndvi; nbr2 (SWIR1-SWIR2)/(SWIR1+SWIR2) for post-fire residue and dry biomass. Per-collection support, because bands differ by sensor: ndvi and ndwi run on every supported collection. nbr, ndmi, ndsi and nbr2 need SWIR, which Sentinel-2 (B11 SWIR1, B12 SWIR2) and every supported Landsat sensor have (OLI B06/B07 on landsat-ot-l2; TM and ETM+ B05/B07 on landsat-tm-l2 and landsat-etm-l2 — the SWIR1 number differs between OLI and TM/ETM+), but byoc-<id> PlanetScope does not, so those indices are rejected there. ndre needs a red-edge band, which only Sentinel-2 has; it is rejected for every Landsat collection and PlanetScope with an error naming the supported ones rather than substituted from another band. On Sentinel-2, nbr and ndre use B8A (the 20 m narrow NIR band co-registered with the SWIR and red-edge grid) rather than the 10 m B08, matching USGS/Copernicus practice; ndre uses B05 as its red-edge band, which is a methodological choice — B05 (~705 nm) is the earliest-responding red-edge band and the common default, but B06/B07 would give different absolute values, so do not compare ndre numbers against series computed with a different red-edge band. On Landsat, which has a single NIR band, "narrow NIR" resolves to that band. Resolution: Sentinel-2's SWIR, red-edge and B8A bands are 20 m native, so nbr, ndmi, ndsi, ndre and nbr2 sample at 20 m there (not the 10 m of ndvi/ndwi), and effective_resolution_m reports it; a note says so whenever an index is coarser than the collection headline. On Landsat every optical band an index here uses (visible, NIR, both SWIR) is 30 m, so nothing changes there. Works with Sentinel-2 (10 m, 20 m for the SWIR/red-edge indices above; data from 2015-06), and with a Planet account also the 30 m Landsat archive: landsat-ot-l2 (Landsat 8/9, 2013 onwards), landsat-etm-l2 (Landsat 7, 1999-2024), and landsat-tm-l2 (Landsat 4/5, 1982-2012), plus byoc-<id> PlanetScope collections (~3 m). ETM+ scenes after 2003-05 are SLC-off, missing ~22% of their pixels in stripes that bias the interval statistics, so prefer landsat-tm-l2 for 2003-2011. For sentinel-2-l2a, cloud, cloud-shadow, and cirrus pixels are excluded per pixel by default via the scene classification (SCL) band; set mask_clouds=False to include them, or mask_snow=True to also exclude snow/ice. Other collections have no SCL, so per-pixel masking is unavailable there. Pixels where the index is undefined are counted in no_data_count rather than averaged in. "Undefined" is a denominator (the sum of the index's two bands) that is zero or noise-dominated: both bands zero, opposite- signed bands summing to zero, or any result falling outside -1..1 — reachable because L2A surface reflectance can be slightly negative, and a pixel whose denominator is noise carries no index signal. Such pixels are dropped from mean, median and percentiles alike (and drawn transparent by render_image), so every returned statistic stays in -1..1. Prefer aggregation='median' for multi-scene intervals over water or glint-prone areas: it composites a per-pixel temporal median across the days covered in each interval, so a single contaminated pass (glint, haze) cannot skew the interval the way it skews the default single mosaic. The median population is days with coverage in the interval, and median spans are capped to keep requests within the provider timeout; split longer ranges into multiple calls. Set percentiles (e.g. [10, 50, 90]) to also get spatial percentiles of the index over the AOI per interval, useful for drawdown or fraction-of-water style questions; these describe the spatial distribution and are independent of the temporal aggregation choice. Sampling uses an adaptive grid targeting the index's native resolution (the coarsest band it uses: the collection's native resolution, or 20 m for the Sentinel-2 SWIR/red-edge indices), coarsened as needed to fit a 65,536-cell budget (a 25-interval Shasta-scale run cost 8.3 processing units at that budget; cost scales roughly linearly with cell count per interval). For aggregation='median' the default grid is additionally budgeted so total cells times expected interval count stays under 131,072, since median compositing processes every covered day per interval and larger grids over multiple intervals hit the provider's request timeout. Pass resolution_m for explicit control, allowed up to a 1,048,576-cell hard cap at proportionally higher cost. The response's effective_resolution_m reports the actual meters per cell, the coarser of the two per-axis values after grid clamping.
| Parameter | Type | Required | Description |
|---|---|---|---|
aoi | object | null | No | GeoJSON Polygon/MultiPolygon geometry (WGS84). Accepts the geo toolkit's `geojson` output. Provide exactly one of aoi, bbox, or aoi_ref. |
bbox | array<number> | null | No | [west, south, east, north] WGS84 degrees. Alternative to aoi. |
aoi_ref | string | null | No | Stored AOI handle from the geo toolkit (the `aoi_ref`/`merged_aoi_ref` field of lookup_place, make_aoi, or query_features). Resolves server-side to the full-fidelity geometry, so statistics can run on unsimplified boundaries; refs expire after about 48 hours. |
index | "ndvi" | "ndwi" | "nbr" | "ndmi" | "ndsi" | "ndre" | "nbr2" | Yes | Spectral index, all normalized differences in -1..1. ndvi: vegetation greenness, (NIR-red)/(NIR+red). ndwi: surface water, (green-NIR)/(green+NIR). nbr: burn severity, (narrow NIR-SWIR2)/(narrow NIR+SWIR2) (B8A/B12 on Sentinel-2); difference two dates for dNBR. ndmi: vegetation water content and drought/fire risk, (NIR-SWIR1)/(NIR+SWIR1) (B08/B11). ndsi: snow versus cloud, (green-SWIR1)/(green+SWIR1) (B03/B11); snow is typically above ~0.42. ndre: crop/vegetation stress, earlier than ndvi, (narrow NIR-red edge)/(narrow NIR+red edge) (B8A/B05). nbr2: post-fire residue and dry biomass, (SWIR1-SWIR2)/(SWIR1+SWIR2) (B11/B12). Support: ndvi and ndwi work on every supported collection; nbr, ndmi, ndsi and nbr2 need SWIR, so they work on sentinel-2-l2a, sentinel-2-l1c and all three Landsat collections but NOT on byoc-<id> PlanetScope (4 bands, no SWIR); ndre needs a red-edge band, which only Sentinel-2 has, so it is rejected for Landsat and PlanetScope rather than computed from a substitute band. On Sentinel-2 the SWIR, red-edge and B8A bands are 20 m native, so nbr, ndmi, ndsi, ndre and nbr2 sample at 20 m, not 10 m (see effective_resolution_m). |
collection | string | No | Collection id: sentinel-2-l2a (default) or sentinel-2-l1c; with a Planet account also the Landsat archive back to 1982: landsat-ot-l2 (Landsat 8/9, 2013 onwards), landsat-etm-l2 (Landsat 7, 1999-2024; scenes after 2003-05 are SLC-off, missing ~22% of pixels in stripes, so prefer landsat-tm-l2 for 2003-2011), landsat-tm-l2 (Landsat 4/5, 1982-2012), or byoc-<id> for a PlanetScope subscription collection. Default: "sentinel-2-l2a" |
time_from | string | Yes | Start date (YYYY-MM-DD) or ISO datetime. |
time_to | string | Yes | End date (YYYY-MM-DD) or ISO datetime. |
interval | "P1D" | "P1M" | No | Aggregation interval: P1D (daily) or P1M (monthly, default). Default: "P1M" |
aggregation | "mean" | "median" | No | Temporal compositing within each interval. 'mean' (default) computes stats over a single mosaic per interval. 'median' builds a per-pixel temporal median across the days covered in the interval first, which is robust to single contaminated passes. Prefer 'median' for multi-scene intervals over water, glint-prone areas, and any time series where one bad pass can skew an interval. Default: "mean" |
percentiles | array<number> | null | No | Spatial percentiles of the index over the AOI per interval, e.g. [10, 50, 90]. Each value must be strictly between 0 and 100; at most 5 values. These describe the spatial distribution within an interval and are independent of the temporal aggregation choice. |
max_cloud_cover | number | null | No | Scene-level filter: skips whole scenes whose metadata cloud-cover percent exceeds this value, where the collection supports scene cloud metadata; otherwise ignored (a response note may be added). It does not mask cloudy pixels; clouds in accepted scenes still contribute to the statistics. Low values (5-10) give a cleaner series at the cost of intervals dropped for lack of scenes. |
resolution_m | number | null | No | Target sampling resolution in meters per grid cell. Default is adaptive: the index's native resolution (the collection's, or 20 m for the Sentinel-2 SWIR/red-edge indices), coarsened as needed to stay within a 65,536-cell budget. Values finer than native are clamped to native. Explicit values may use up to 1,048,576 cells; finer grids cost proportionally more processing units per interval. |
mask_clouds | boolean | No | Exclude cloud, cloud-shadow, and cirrus pixels per pixel using the scene classification (SCL) band. sentinel-2-l2a only; other collections have no SCL and ignore this flag. Default: true |
mask_snow | boolean | No | Also exclude snow/ice pixels (SCL class 11). Off by default because snow is a real surface state, not contamination. sentinel-2-l2a only. Default: false |
Parameter schema (JSON)
{
"description": "Parameters for get_statistics.",
"properties": {
"aoi": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "GeoJSON Polygon/MultiPolygon geometry (WGS84). Accepts the geo toolkit's `geojson` output. Provide exactly one of aoi, bbox, or aoi_ref.",
"title": "Aoi"
},
"bbox": {
"anyOf": [
{
"items": {
"type": "number"
},
"maxItems": 4,
"minItems": 4,
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "[west, south, east, north] WGS84 degrees. Alternative to aoi.",
"title": "Bbox"
},
"aoi_ref": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Stored AOI handle from the geo toolkit (the `aoi_ref`/`merged_aoi_ref` field of lookup_place, make_aoi, or query_features). Resolves server-side to the full-fidelity geometry, so statistics can run on unsimplified boundaries; refs expire after about 48 hours.",
"title": "Aoi Ref"
},
"index": {
"description": "Spectral index, all normalized differences in -1..1. ndvi: vegetation greenness, (NIR-red)/(NIR+red). ndwi: surface water, (green-NIR)/(green+NIR). nbr: burn severity, (narrow NIR-SWIR2)/(narrow NIR+SWIR2) (B8A/B12 on Sentinel-2); difference two dates for dNBR. ndmi: vegetation water content and drought/fire risk, (NIR-SWIR1)/(NIR+SWIR1) (B08/B11). ndsi: snow versus cloud, (green-SWIR1)/(green+SWIR1) (B03/B11); snow is typically above ~0.42. ndre: crop/vegetation stress, earlier than ndvi, (narrow NIR-red edge)/(narrow NIR+red edge) (B8A/B05). nbr2: post-fire residue and dry biomass, (SWIR1-SWIR2)/(SWIR1+SWIR2) (B11/B12). Support: ndvi and ndwi work on every supported collection; nbr, ndmi, ndsi and nbr2 need SWIR, so they work on sentinel-2-l2a, sentinel-2-l1c and all three Landsat collections but NOT on byoc-<id> PlanetScope (4 bands, no SWIR); ndre needs a red-edge band, which only Sentinel-2 has, so it is rejected for Landsat and PlanetScope rather than computed from a substitute band. On Sentinel-2 the SWIR, red-edge and B8A bands are 20 m native, so nbr, ndmi, ndsi, ndre and nbr2 sample at 20 m, not 10 m (see effective_resolution_m).",
"enum": [
"ndvi",
"ndwi",
"nbr",
"ndmi",
"ndsi",
"ndre",
"nbr2"
],
"title": "Index",
"type": "string"
},
"collection": {
"default": "sentinel-2-l2a",
"description": "Collection id: sentinel-2-l2a (default) or sentinel-2-l1c; with a Planet account also the Landsat archive back to 1982: landsat-ot-l2 (Landsat 8/9, 2013 onwards), landsat-etm-l2 (Landsat 7, 1999-2024; scenes after 2003-05 are SLC-off, missing ~22% of pixels in stripes, so prefer landsat-tm-l2 for 2003-2011), landsat-tm-l2 (Landsat 4/5, 1982-2012), or byoc-<id> for a PlanetScope subscription collection.",
"title": "Collection",
"type": "string"
},
"time_from": {
"description": "Start date (YYYY-MM-DD) or ISO datetime.",
"title": "Time From",
"type": "string"
},
"time_to": {
"description": "End date (YYYY-MM-DD) or ISO datetime.",
"title": "Time To",
"type": "string"
},
"interval": {
"default": "P1M",
"description": "Aggregation interval: P1D (daily) or P1M (monthly, default).",
"enum": [
"P1D",
"P1M"
],
"title": "Interval",
"type": "string"
},
"aggregation": {
"default": "mean",
"description": "Temporal compositing within each interval. 'mean' (default) computes stats over a single mosaic per interval. 'median' builds a per-pixel temporal median across the days covered in the interval first, which is robust to single contaminated passes. Prefer 'median' for multi-scene intervals over water, glint-prone areas, and any time series where one bad pass can skew an interval.",
"enum": [
"mean",
"median"
],
"title": "Aggregation",
"type": "string"
},
"percentiles": {
"anyOf": [
{
"items": {
"type": "number"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "Spatial percentiles of the index over the AOI per interval, e.g. [10, 50, 90]. Each value must be strictly between 0 and 100; at most 5 values. These describe the spatial distribution within an interval and are independent of the temporal aggregation choice.",
"title": "Percentiles"
},
"max_cloud_cover": {
"anyOf": [
{
"maximum": 100,
"minimum": 0,
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Scene-level filter: skips whole scenes whose metadata cloud-cover percent exceeds this value, where the collection supports scene cloud metadata; otherwise ignored (a response note may be added). It does not mask cloudy pixels; clouds in accepted scenes still contribute to the statistics. Low values (5-10) give a cleaner series at the cost of intervals dropped for lack of scenes.",
"title": "Max Cloud Cover"
},
"resolution_m": {
"anyOf": [
{
"maximum": 10000,
"minimum": 1,
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Target sampling resolution in meters per grid cell. Default is adaptive: the index's native resolution (the collection's, or 20 m for the Sentinel-2 SWIR/red-edge indices), coarsened as needed to stay within a 65,536-cell budget. Values finer than native are clamped to native. Explicit values may use up to 1,048,576 cells; finer grids cost proportionally more processing units per interval.",
"title": "Resolution M"
},
"mask_clouds": {
"default": true,
"description": "Exclude cloud, cloud-shadow, and cirrus pixels per pixel using the scene classification (SCL) band. sentinel-2-l2a only; other collections have no SCL and ignore this flag.",
"title": "Mask Clouds",
"type": "boolean"
},
"mask_snow": {
"default": false,
"description": "Also exclude snow/ice pixels (SCL class 11). Off by default because snow is a real surface state, not contamination. sentinel-2-l2a only.",
"title": "Mask Snow",
"type": "boolean"
}
},
"required": [
"index",
"time_from",
"time_to"
],
"title": "GetStatisticsParams",
"type": "object",
"additionalProperties": false
} Result schema (JSON)
{
"$defs": {
"IntervalStats": {
"description": "Index statistics for one time interval.",
"properties": {
"interval_from": {
"description": "Interval start (UTC).",
"title": "Interval From",
"type": "string"
},
"interval_to": {
"description": "Interval end (UTC).",
"title": "Interval To",
"type": "string"
},
"mean": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Mean index value over the AOI.",
"title": "Mean"
},
"min": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Minimum index value.",
"title": "Min"
},
"max": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Maximum index value.",
"title": "Max"
},
"st_dev": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Standard deviation.",
"title": "St Dev"
},
"sample_count": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Pixels sampled.",
"title": "Sample Count"
},
"no_data_count": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Pixels with no data; includes cloud/snow-masked pixels when per-pixel masking is applied, and pixels where the index is undefined because its denominator (the sum of its two bands) is zero or noise-dominated — both bands zero, bands of opposite sign summing to zero, or a value falling outside -1..1. All are excluded rather than averaged in as a bogus value, so mean/min/max stay within -1..1.",
"title": "No Data Count"
},
"valid_pixel_fraction": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Fraction of sampled pixels with valid data, (sample_count - no_data_count) / sample_count. Low values flag mostly-cloudy intervals when masking is applied.",
"title": "Valid Pixel Fraction"
},
"percentiles": {
"anyOf": [
{
"additionalProperties": {
"type": "number"
},
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "Requested spatial percentiles of the index over the AOI within this interval, keyed 'p10' style. These describe the spatial distribution and are not the temporal median that aggregation='median' composites per pixel.",
"title": "Percentiles"
}
},
"required": [
"interval_from",
"interval_to"
],
"title": "IntervalStats",
"type": "object"
}
},
"description": "Result of get_statistics.",
"properties": {
"success": {
"description": "Whether the operation succeeded",
"title": "Success",
"type": "boolean"
},
"index": {
"description": "Spectral index computed.",
"title": "Index",
"type": "string"
},
"collection": {
"description": "Collection used.",
"title": "Collection",
"type": "string"
},
"interval": {
"description": "Aggregation interval used.",
"title": "Interval",
"type": "string"
},
"aggregation": {
"description": "Temporal aggregation used: 'mean' (single mosaic per interval) or 'median' (per-pixel temporal median composite). Under 'median', the per-interval stats describe the median composite.",
"title": "Aggregation",
"type": "string"
},
"intervals": {
"description": "Per-interval statistics, chronological.",
"items": {
"$ref": "#/$defs/IntervalStats"
},
"title": "Intervals",
"type": "array"
},
"empty_intervals_skipped": {
"default": 0,
"description": "Requested intervals with no usable data, omitted from intervals. For P1M this counts both intervals the provider left out of its response and returned intervals dropped as unusable (fully cloudy/masked). For P1D it counts only returned intervals dropped as unusable; provider-omitted days are counted in days_without_acquisition instead.",
"title": "Empty Intervals Skipped",
"type": "integer"
},
"missing_intervals": {
"description": "Monthly intervals absent from intervals, as '<from>/<to>' strings, so callers can re-align the series without date math. Always empty for P1D (see days_without_acquisition).",
"items": {
"type": "string"
},
"title": "Missing Intervals",
"type": "array"
},
"days_without_acquisition": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "For P1D requests, the number of requested days with no usable data (no satellite pass, or fully cloudy/masked). None for P1M.",
"title": "Days Without Acquisition"
},
"effective_resolution_m": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Actual meters per sampling grid cell; the coarser of the two per-axis values after grid clamping. The target is the index's coarsest band, so Sentinel-2 SWIR/red-edge indices (nbr, ndmi, ndsi, ndre, nbr2) target 20 m rather than 10 m; a very small AOI can still be floored to a finer grid, which the note discloses as a sampling artifact.",
"title": "Effective Resolution M"
},
"grid_width": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Sampling grid width in cells.",
"title": "Grid Width"
},
"grid_height": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Sampling grid height in cells.",
"title": "Grid Height"
},
"cloud_masking_applied": {
"default": false,
"description": "Whether per-pixel SCL masking (clouds and/or snow) was applied to these statistics.",
"title": "Cloud Masking Applied",
"type": "boolean"
},
"processing_units_spent": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Processing units this call consumed, when reported.",
"title": "Processing Units Spent"
},
"note": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Extra guidance, when relevant.",
"title": "Note"
}
},
"required": [
"success",
"index",
"collection",
"interval",
"aggregation"
],
"title": "GetStatisticsResult",
"type": "object"
} # render_image write
Render a satellite image (true color, false color, or an NDVI, NDWI, NBR, NDMI, NDSI, NDRE or NBR2 index ramp) of an AOI and return a displayable URL; compare mode stitches 2-4 labeled time windows into one image, overlays burn boundary outlines, fills, a spotlight, or labels onto it, and composite="best_pixel" builds a cloud-free per-pixel composite over the whole window on Sentinel-2 L2A.
Full description
Render satellite imagery of an AOI to a PNG and return a presigned URL.
Mosaics scenes from the time window (scene-level cloud-cover filter where
the collection supports scene cloud metadata; cloudy pixels in accepted
scenes are not masked; prioritized by mosaicking_order), renders the
chosen visualization, and uploads the PNG to temporary storage. Embed
the returned image_url in a markdown image tag to show it. Default is
Sentinel-2 at 10 m pixels: choose field-scale or larger AOIs (parks,
farms, lakes), not building-scale. With a Planet account, the 30 m
Landsat archive (landsat-ot-l2 for Landsat 8/9 from 2013, landsat-etm-l2
for Landsat 7 over 1999-2024, landsat-tm-l2 for Landsat 4/5 over
1982-2012) and byoc-<id> PlanetScope collections (~3 m, resolves
buildings) are also renderable. ETM+ scenes after 2003-05 are SLC-off
and render with ~22% of pixels missing in stripes, so prefer
landsat-tm-l2 for 2003-2011. The area can be inline GeoJSON (`aoi`), a `bbox`,
or an `aoi_ref` handle minted by the geo toolkit, resolved server-side
to the stored full-fidelity geometry (refs expire after about 48
hours). Use search_catalog first to pick a window
containing a clear scene.
Visualizations: true_color, false_color, and the spectral-index ramps
ndvi (greenness), ndwi (water), nbr (burn severity), ndmi (vegetation
moisture), ndsi (snow), ndre (crop stress) and nbr2 (post-fire residue),
each the same normalized difference get_statistics computes. Support is
per collection, because the bands differ: true_color, false_color, ndvi
and ndwi work everywhere; nbr, ndmi, ndsi and nbr2 need SWIR, present on
Sentinel-2 (B11/B12) and on all three Landsat sensors (OLI B06/B07;
TM/ETM+ B05/B07) but not on byoc-<id> PlanetScope; ndre needs a red-edge
band that only Sentinel-2 carries, so it is rejected elsewhere
with an error naming the supported collections rather than rendered from
a substitute band. ndre uses B05 (~705 nm) as its red-edge band — a
methodological choice, matching get_statistics: B06/B07 would shift the
absolute values, so ndre renders are not comparable against series built
on a different red-edge band. On Sentinel-2, nbr/ndmi/ndsi/ndre/nbr2 use 20 m bands
(and B8A, the 20 m narrow NIR, for nbr and ndre), so their native
resolution is 20 m/px, not 10 — resolution="native" sizes to 20 m/px for
them and a note says so. In an index ramp, a pixel whose index is
undefined — a zero or noise-dominated denominator, i.e. both bands zero,
opposite-signed bands summing to zero, or a value outside the ramp's
-1..1 domain — renders transparent, the same as a no-data pixel, rather
than being painted in a terminal colour of the ramp. Band composites
(SWIR or agriculture false colour) are deliberately not offered here.
Sizing: omit width for the 512px default, pass width for an explicit
pixel count, or pass resolution="native" to auto-size the width to the
native ground resolution of the chosen visualization (one output pixel
per source pixel) without doing the latitude/GSD math yourself — capped at 1536px,
with achieved_meters_per_px and a note when the AOI is too large to
render pixel-exact. resolution also accepts a meters-per-pixel number.
width and resolution are mutually exclusive.
Pass compare={panels: [{time_from, time_to, label?}, ...]} (2-4 panels)
instead of time_from/time_to for a before/after comparison: every panel
renders the same AOI at identical dimensions, and image_url becomes one
stitched PNG with a labeled band per panel (2 panels side by side, 3-4
in a 2x2 grid). The result lists panels in temporal order with per-panel
URLs, and the top-level time_from/time_to are null. Per-panel width is
capped at 1024px in compare mode. A comparison never returns partial
results: if any panel fails to render or decode, the whole call fails, and
if too little request time remains to upload every panel plus the stitched
canvas, the call fails before uploading any of them. A failure that happens
PART WAY through the uploads still returns no comparison, but images
already written may remain in storage as unreferenced objects; no URL for
them is returned and they expire with the storage lifecycle.
Tone: true_color and false_color are normalized to the same numeric
reflectance domain (0..1) per collection, gamma-encoded, then
contrast-stretched to the 2nd-98th percentile of that scene's own valid
pixels (nodata, AOI cut-outs and Landsat fill are excluded). That is a
scaling step, not an atmospheric one: sentinel-2-l2a, the Landsat C2 L2
collections and the byoc-* PlanetScope collections carry SURFACE
reflectance, while sentinel-2-l1c carries TOP-OF-ATMOSPHERE reflectance and
still looks hazier for that reason. Every collection therefore gets the
same tone treatment in the same numeric domain, so a Landsat render is no
longer dark relative to Sentinel-2 — but the stretch is per-scene, so brightness is NOT
comparable across separate calls. Within one compare call the panels ARE
tone-matched (one jointly computed stretch for all panels), which preserves
their relative rendered brightness; a difference that remains may still be
acquisition conditions (cloud, haze, sun angle, atmosphere) rather than
surface change. Contrast
amplification is capped, so a genuinely flat scene stays flat rather than
having noise blown up into texture. The index ramps are never stretched:
their colours encode index classes. Pass brightness (0.25-4) for a bounded gamma
on top, for publication imagery. The stretch (and with it brightness) is
skipped whenever it cannot be completed within the wall clock the request
has left (too little time to start, or an overrun of the bound it runs
under), and when the render has no valid pixels to stretch; a note says
which, and in compare mode says the panels are then not tone-matched. An
provider payload that is not a PNG of exactly the requested dimensions is
refused before anything decodes or uploads it, whatever the time budget and
whether or not the stretch runs — so the width and height reported here are
always the actual dimensions of the image behind image_url. A payload whose
header is well-formed but whose image data is truncated or forged is
refused too, on every path: either some step decodes it (stretch, overlay
burn, compare stitch) or it is verified before upload. A success response
never carries an undecodable image.
Compositing: composite="scene" (the default) mosaics whole SCENES by
mosaicking_order, so a pixel takes whatever the winning scene has there —
cloud included, which is why a wide window covers the AOI but collects
cloud while a narrow one is clean but incomplete. composite="best_pixel"
instead selects PER PIXEL over every acquisition in the window: it requests
the whole stack with ORBIT mosaicking (one mosaic per covered day), drops
the observations Sentinel-2's SCL band classifies as cloud, cloud shadow or
cirrus (add mask_snow=True for snow/ice), and paints each pixel from the
MOST RECENT survivor. Pixels with no surviving observation anywhere in the
window stay transparent, exactly as a no-data pixel does, and this tool
reports no coverage fraction, so completeness has to be judged from the
image. best_pixel needs a per-pixel quality band and is therefore
sentinel-2-l2a only; L1C, Landsat and PlanetScope are refused with an error
naming the supported collection rather than silently falling back to scene
mosaicking. mosaicking_order is not sent in this mode — there is no scene
ordering left to apply — and mask_snow is rejected in scene mode rather
than ignored. There is exactly ONE reducer (most recent survivor) because
it is the only one whose result at a pixel is a real observation on a real
date; a per-pixel median would report a colour nothing ever measured.
"scene" stays the DEFAULT because a composite depicts no single moment: it
is assembled from many dates, so it is right for "show me this place
cloud-free" and wrong for "what did this look like on the 17th". Every
best_pixel response says so in a note and reports
composite_earliest_observation / composite_latest_observation — the extremes
of the acquisition dates the catalog enumerated for the window, which BOUND
the dates that contributed rather than naming them (a date rejected as
cloudy at every pixel contributed nothing, and a rendered image carries
colour, not provenance). Cost scales with acquisition density, not with the
calendar: it is estimated from a free catalog count before the billed
request, reported as estimated_processing_units, and enforced against the
WHOLE REQUEST — the output grid is coarsened to stay inside a 300-unit
budget (a note gives the arithmetic) and the request is refused outright if
even the smallest grid would exceed 1000 units. In compare mode every panel
is composited the same way onto ONE shared grid, so those two limits apply
to the panels' combined cost rather than to each panel; the note says the
panels are composites, and each panel reports its own window's acquisition
COUNT and earliest/latest date bounds — never which date a given pixel came
from. A window the catalog proves EMPTY gets no billed Process request —
the free catalog requests that proved it empty are still made — so its
imagery is rendered here rather than bought: every composited satellite
pixel transparent, at no cost, and a note says so. Overlays are burned onto
that render like any other, so an empty window with overlays comes back as
the graphics alone over transparency, not as an empty file.
Pass overlays=[{...}] to burn vector graphics onto the render: a boundary
outline (stroke), a translucent fill, a spotlight that dims everything
outside a shape to highlight it, and/or a centroid label. Each overlay's
geometry is an aoi_ref (preferred — pass the geo toolkit's aoi_ref rather
than inline geometry for anything bigger than a sketch) or a small inline
geojson Polygon/MultiPolygon; up to 16 overlays. In compare mode every
panel (and the stitched image) gets the overlays. Overlay graphics are
clipped to the rendered extent — for an overlay whose bounding box lies
outside it the fill adds nothing and the shape lights no part of the
spotlight mask (a wide stroke may still paint at the edge), and a note says
which one. Spotlighting itself still runs: if no spotlit shape reaches the
extent the mask is empty and the whole image comes back dimmed, and a note
says that too. Whenever overlays are requested and resolved the
result carries an OpenStreetMap ODbL attribution string. For a
pure outline map with no satellite base, geo-render_outline is the right
tool once it lands. | Parameter | Type | Required | Description |
|---|---|---|---|
aoi | object | null | No | GeoJSON Polygon/MultiPolygon geometry (WGS84). Accepts the geo toolkit's `geojson` output; pixels outside it render transparent. Provide exactly one of aoi, bbox, or aoi_ref. |
bbox | array<number> | null | No | [west, south, east, north] WGS84 degrees. Alternative to aoi. |
aoi_ref | string | null | No | Stored AOI handle from the geo toolkit (the `aoi_ref`/`merged_aoi_ref` field of lookup_place, make_aoi, or query_features). Resolves server-side to the full-fidelity geometry; pixels outside it render transparent. Refs expire after about 48 hours. |
visualization | "true_color" | "false_color" | "ndvi" | "ndwi" | "nbr" | "ndmi" | "ndsi" | "ndre" | "nbr2" | No | Rendering: true_color (natural), false_color (vegetation in red), or a spectral-index ramp — ndvi greenness (NIR-red)/(NIR+red), ndwi water (green-NIR)/(green+NIR), nbr burn severity (narrow NIR-SWIR2)/(narrow NIR+SWIR2), ndmi vegetation moisture (NIR-SWIR1)/(NIR+SWIR1), ndsi snow (green-SWIR1)/(green+SWIR1), ndre crop stress (narrow NIR-red edge)/(narrow NIR+red edge), nbr2 post-fire residue (SWIR1-SWIR2)/(SWIR1+SWIR2). Support: true_color, false_color, ndvi and ndwi work on every supported collection; nbr, ndmi, ndsi and nbr2 need SWIR, so they work on Sentinel-2 and all three Landsat collections but not on byoc-<id> PlanetScope; ndre needs a red-edge band and is Sentinel-2 only. On Sentinel-2 those SWIR/red-edge ramps are 20 m native, so resolution="native" sizes to 20 m/px for them, not 10. Default: "true_color" |
collection | string | No | Collection id: sentinel-2-l2a (default, 10 m) or sentinel-2-l1c; with a Planet account also the 30 m Landsat archive, landsat-ot-l2 (Landsat 8/9, 2013 onwards), landsat-etm-l2 (Landsat 7, 1999-2024; scenes after 2003-05 are SLC-off and render with ~22% striped gaps, so prefer landsat-tm-l2 for 2003-2011) and landsat-tm-l2 (Landsat 4/5, 1982-2012), or byoc-<id> for a PlanetScope subscription collection (~3 m, sharpest). Default: "sentinel-2-l2a" |
time_from | string | null | No | Start date (YYYY-MM-DD) or ISO datetime. Required unless compare is provided. |
time_to | string | null | No | End date (YYYY-MM-DD) or ISO datetime. Required unless compare is provided. |
compare | CompareSpec | null | No | Render 2-4 time windows of the same AOI as pixel-aligned, labeled panels in ONE call (before/after comparisons). All panels share the AOI, visualization, collection, cloud filter, and dimensions; per-panel width is capped at 1024px in compare mode. For true_color and false_color the panels are tone-matched: one stretch, computed jointly over all the panels' pixels, is applied to every panel, which preserves their relative rendered brightness instead of auto-normalizing each panel separately — a remaining brightness difference may still come from acquisition conditions (cloud, haze, sun angle, atmosphere, mosaicking) rather than from a change on the ground. Tone-matching holds WITHIN one call only — panels from separate render_image calls are stretched independently and are not comparable by brightness — and a note says so on the rare call where the stretch does not run (it cannot be completed within the wall clock the request has left, or no panel has any valid pixels). More panels also need more request time — each one is uploaded separately, alongside the stitched canvas — and if there is too little left for all of them the call fails before uploading any. A partial comparison is never returned; a failure part way through the uploads can leave already-written images as unreferenced objects that expire with the storage lifecycle, and no URL for them is returned. Mutually exclusive with time_from/time_to. This is the comparison presentation path (an interactive wipe slider on MCP Apps hosts); google_earth_engine-render_image windows is for multi-frame series over Earth Engine datasets, not comparison presentation. |
overlays | array<OverlaySpec> | null | No | Burn boundary outlines, translucent fills, a spotlight dim, or centroid labels onto the render (highlight a county on a state, an outline on a site, a burn perimeter). Each overlay's geometry is an aoi_ref (preferred — pass the geo toolkit's aoi_ref, not inline geometry, for anything bigger than a sketch) or small inline geojson; up to 16. In compare mode every panel gets the overlays. Graphics are clipped to the rendered extent: for an overlay whose bounding box lies outside it the fill adds nothing and the shape lights no part of a spotlight mask — spotlighting still runs, so an off-extent spotlight DIMS the whole image (a wide stroke may still paint at the edge). A note reports it. For a pure outline map with no satellite base, use geo-render_outline instead once it is available. |
max_cloud_cover | number | No | Scene-level filter: only mosaics scenes whose metadata cloud-cover percent is at or below this value, where the collection supports scene cloud metadata; otherwise ignored (a response note may be added). It does not mask cloudy pixels; accepted scenes can still show clouds. Prefer 10-30; a value of 0 requires perfectly cloud-free scenes and usually produces an empty image. Default: 30 |
mosaicking_order | "mostRecent" | "leastCC" | "leastRecent" | No | Scene priority when several match: leastCC (least cloudy, default), mostRecent, or leastRecent. Not used when composite="best_pixel": that mode selects per pixel rather than per scene, so no scene ordering is sent at all (the response note says so). Default: "leastCC" |
composite | "scene" | "best_pixel" | No | How several acquisitions in the window are combined. "scene" (default) mosaics whole scenes by mosaicking_order — a pixel takes whatever the winning scene has there, cloud included — so the render can still be attributed to the acquisition it came from, which is why it stays the default: it is the only mode that can answer "what did this look like on the 17th". "best_pixel" instead composites PER PIXEL over every acquisition in the window: observations Sentinel-2's SCL band calls cloud, cloud shadow or cirrus (plus snow/ice with mask_snow) are dropped, and the most recent survivor wins the pixel. That gives complete AND clean coverage where no single date does, at the price of an image assembled from many dates that depicts no single moment — a note says so and reports HOW MANY acquisition dates the window offered plus the earliest and latest of them as bounds — not which date each pixel came from, which a rendered image cannot carry. best_pixel needs a per-pixel quality band, so it is sentinel-2-l2a only, and it costs substantially more: cost scales with how many acquisitions fall in the window, so it is estimated from the catalog and capped before the billed request (see estimated_processing_units), and the output grid is coarsened, or the request refused, when the estimate is over budget. The cap is on the WHOLE request: compare panels share one pixel grid, so their combined cost is what is coarsened or refused. A window with no acquisition at all costs no processing units: the free catalog requests still run, but no billed Process request is sent for it, and every composited satellite pixel comes back transparent — any overlays you asked for are still burned on and stay visible. Default: "scene" |
mask_snow | boolean | No | best_pixel only: also drop observations classified as snow/ice (SCL 11), on top of cloud, cloud shadow and cirrus. Off by default because snow is a real surface state, not contamination. Rejected with composite="scene", which has no per-pixel masking at all — rather than accepted and silently ignored. Default: false |
width | integer | null | No | Output width in pixels (64-1536); defaults to 512 when omitted. Height follows the AOI aspect ratio. Width beyond the AOI's native pixel count at the resolution of the chosen visualization (10 m Sentinel-2 — 20 m for its SWIR/red-edge index ramps nbr, ndmi, ndsi, ndre and nbr2 — 30 m Landsat, ~3 m PlanetScope) adds no detail; the server caps excessive values. Mutually exclusive with `resolution`. |
brightness | number | null | No | Optional tone adjustment for true_color/false_color renders (0.25-4.0, default 1 = no adjustment), for publication imagery. Applied as a gamma on top of the automatic per-scene 2-98% percentile stretch: values above 1 lift the midtones without clipping highlights, below 1 deepen them. Rejected for every index ramp — ndvi/ndwi/nbr/ndmi/ndsi/ndre/nbr2 — except the no-op 1.0; their colours encode index values and are never tone-adjusted. Not applied when the stretch does not run: it cannot be completed within the wall clock the request has left, or the render has no valid pixels to stretch (the response note says so in every case). |
resolution | string | number | null | No | Auto-size the output width from the AOI's ground extent instead of passing `width` yourself. "native" sizes to the native ground resolution of the chosen visualization (10 m Sentinel-2, but 20 m for its SWIR/red-edge index ramps nbr, ndmi, ndsi, ndre and nbr2; 30 m Landsat, ~3 m PlanetScope) so one output pixel equals one source pixel — the pixel-exact render, still capped at 1536px. A number sets an explicit target of that many meters per pixel (clamped no finer than native, since finer adds no detail). When the cap is hit, the achieved m/px is coarser than requested and a note says so; use a smaller AOI for a true native render. Mutually exclusive with `width`; leave both unset for the 512px default. |
Parameter schema (JSON)
{
"$defs": {
"ComparePanel": {
"description": "One time window of a compare-mode render.",
"properties": {
"time_from": {
"description": "Panel window start (YYYY-MM-DD) or ISO datetime.",
"title": "Time From",
"type": "string"
},
"time_to": {
"description": "Panel window end (YYYY-MM-DD) or ISO datetime.",
"title": "Time To",
"type": "string"
},
"label": {
"anyOf": [
{
"maxLength": 60,
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Panel label burned into the stitched image and used as the panel title; defaults to the panel's time window (e.g. '2019-07-01 – 2019-07-31'). Burned text covers Latin (incl. umlauts/accents), Greek, and Cyrillic; CJK is not yet supported in the PNG and renders as tofu.",
"title": "Label"
}
},
"required": [
"time_from",
"time_to"
],
"title": "ComparePanel",
"type": "object",
"additionalProperties": false
},
"CompareSpec": {
"description": "Compare-mode request: 2-4 time windows of the same AOI.",
"properties": {
"panels": {
"description": "2-4 time windows rendered as pixel-aligned panels of the same AOI and stitched into one labeled comparison PNG.",
"items": {
"$ref": "#/$defs/ComparePanel"
},
"maxItems": 4,
"minItems": 2,
"title": "Panels",
"type": "array"
}
},
"required": [
"panels"
],
"title": "CompareSpec",
"type": "object",
"additionalProperties": false
},
"OverlaySpec": {
"description": "One boundary/fill/spotlight/label overlay burned onto the render.\n\nGraphics are CLIPPED to the rendered extent (the image covers the AOI, and\nnothing outside it exists to draw on). For an overlay whose BOUNDING BOX\nlies outside it, the fill adds nothing and the shape contributes no lit\narea to the spotlight mask — but spotlighting still runs, so a spotlit\nshape that reaches nothing DARKENS the image rather than leaving it alone.\nA wide stroke can still paint at the edge. A note reports all of this. A\nshape whose box overlaps the extent is drawn as-is and gets no note,\nwhether or not any of it lands on visible pixels.\n\nEvery field is Optional with defaults applied in code (the MCP transport\nmaterializes schema defaults, so conditional logic compares against None,\nnever model_fields_set — same reasoning as _width_xor_resolution).",
"properties": {
"aoi_ref": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Stored AOI handle for this overlay's geometry (the geo toolkit's `aoi_ref`/`merged_aoi_ref`). Preferred for anything bigger than a sketch — resolved server-side, never through model context. Provide exactly one of aoi_ref or geojson.",
"title": "Aoi Ref"
},
"geojson": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "Inline GeoJSON Polygon/MultiPolygon for a small shape (<= 2000 vertices). For larger boundaries pass an aoi_ref. Provide exactly one of aoi_ref or geojson.",
"title": "Geojson"
},
"stroke": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Outline color as hex (#RGB, #RRGGBB, or #RRGGBBAA); defaults to a high-contrast red. Set stroke_width to 0 for no outline.",
"title": "Stroke"
},
"stroke_width": {
"anyOf": [
{
"maximum": 12,
"minimum": 0,
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Outline width in pixels (0-12); 0 draws no outline. Defaults to 2.",
"title": "Stroke Width"
},
"fill": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Fill color as hex; 6 digits are opaque, pass 8 digits (#RRGGBBAA) for a translucent wash. No fill by default.",
"title": "Fill"
},
"spotlight": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"default": null,
"description": "When true, dim everything OUTSIDE this shape (to ~40% brightness) to spotlight it. Multiple spotlight overlays union. If every spotlit shape's bounding box lies outside the rendered extent, nothing is lit, the image may come back fully dimmed, and a note says so. Default false.",
"title": "Spotlight"
},
"label": {
"anyOf": [
{
"maxLength": 60,
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Short text burned once, centered on the centroid of the shape's largest ring (white with a dark halo) — for a concave shape that anchor can lie outside the shape, and if the anchor falls outside the canvas the text is clipped (possibly still partly visible) and a note says so. Max 60 characters. No label by default. Burned text covers Latin (incl. umlauts/accents), Greek, and Cyrillic; CJK is not yet supported in the PNG and renders as tofu.",
"title": "Label"
}
},
"title": "OverlaySpec",
"type": "object",
"additionalProperties": false
}
},
"description": "Parameters for render_image.",
"properties": {
"aoi": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "GeoJSON Polygon/MultiPolygon geometry (WGS84). Accepts the geo toolkit's `geojson` output; pixels outside it render transparent. Provide exactly one of aoi, bbox, or aoi_ref.",
"title": "Aoi"
},
"bbox": {
"anyOf": [
{
"items": {
"type": "number"
},
"maxItems": 4,
"minItems": 4,
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "[west, south, east, north] WGS84 degrees. Alternative to aoi.",
"title": "Bbox"
},
"aoi_ref": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Stored AOI handle from the geo toolkit (the `aoi_ref`/`merged_aoi_ref` field of lookup_place, make_aoi, or query_features). Resolves server-side to the full-fidelity geometry; pixels outside it render transparent. Refs expire after about 48 hours.",
"title": "Aoi Ref"
},
"visualization": {
"default": "true_color",
"description": "Rendering: true_color (natural), false_color (vegetation in red), or a spectral-index ramp — ndvi greenness (NIR-red)/(NIR+red), ndwi water (green-NIR)/(green+NIR), nbr burn severity (narrow NIR-SWIR2)/(narrow NIR+SWIR2), ndmi vegetation moisture (NIR-SWIR1)/(NIR+SWIR1), ndsi snow (green-SWIR1)/(green+SWIR1), ndre crop stress (narrow NIR-red edge)/(narrow NIR+red edge), nbr2 post-fire residue (SWIR1-SWIR2)/(SWIR1+SWIR2). Support: true_color, false_color, ndvi and ndwi work on every supported collection; nbr, ndmi, ndsi and nbr2 need SWIR, so they work on Sentinel-2 and all three Landsat collections but not on byoc-<id> PlanetScope; ndre needs a red-edge band and is Sentinel-2 only. On Sentinel-2 those SWIR/red-edge ramps are 20 m native, so resolution=\"native\" sizes to 20 m/px for them, not 10.",
"enum": [
"true_color",
"false_color",
"ndvi",
"ndwi",
"nbr",
"ndmi",
"ndsi",
"ndre",
"nbr2"
],
"title": "Visualization",
"type": "string"
},
"collection": {
"default": "sentinel-2-l2a",
"description": "Collection id: sentinel-2-l2a (default, 10 m) or sentinel-2-l1c; with a Planet account also the 30 m Landsat archive, landsat-ot-l2 (Landsat 8/9, 2013 onwards), landsat-etm-l2 (Landsat 7, 1999-2024; scenes after 2003-05 are SLC-off and render with ~22% striped gaps, so prefer landsat-tm-l2 for 2003-2011) and landsat-tm-l2 (Landsat 4/5, 1982-2012), or byoc-<id> for a PlanetScope subscription collection (~3 m, sharpest).",
"title": "Collection",
"type": "string"
},
"time_from": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Start date (YYYY-MM-DD) or ISO datetime. Required unless compare is provided.",
"title": "Time From"
},
"time_to": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "End date (YYYY-MM-DD) or ISO datetime. Required unless compare is provided.",
"title": "Time To"
},
"compare": {
"anyOf": [
{
"$ref": "#/$defs/CompareSpec"
},
{
"type": "null"
}
],
"default": null,
"description": "Render 2-4 time windows of the same AOI as pixel-aligned, labeled panels in ONE call (before/after comparisons). All panels share the AOI, visualization, collection, cloud filter, and dimensions; per-panel width is capped at 1024px in compare mode. For true_color and false_color the panels are tone-matched: one stretch, computed jointly over all the panels' pixels, is applied to every panel, which preserves their relative rendered brightness instead of auto-normalizing each panel separately — a remaining brightness difference may still come from acquisition conditions (cloud, haze, sun angle, atmosphere, mosaicking) rather than from a change on the ground. Tone-matching holds WITHIN one call only — panels from separate render_image calls are stretched independently and are not comparable by brightness — and a note says so on the rare call where the stretch does not run (it cannot be completed within the wall clock the request has left, or no panel has any valid pixels). More panels also need more request time — each one is uploaded separately, alongside the stitched canvas — and if there is too little left for all of them the call fails before uploading any. A partial comparison is never returned; a failure part way through the uploads can leave already-written images as unreferenced objects that expire with the storage lifecycle, and no URL for them is returned. Mutually exclusive with time_from/time_to. This is the comparison presentation path (an interactive wipe slider on MCP Apps hosts); google_earth_engine-render_image windows is for multi-frame series over Earth Engine datasets, not comparison presentation."
},
"overlays": {
"anyOf": [
{
"items": {
"$ref": "#/$defs/OverlaySpec"
},
"maxItems": 16,
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "Burn boundary outlines, translucent fills, a spotlight dim, or centroid labels onto the render (highlight a county on a state, an outline on a site, a burn perimeter). Each overlay's geometry is an aoi_ref (preferred — pass the geo toolkit's aoi_ref, not inline geometry, for anything bigger than a sketch) or small inline geojson; up to 16. In compare mode every panel gets the overlays. Graphics are clipped to the rendered extent: for an overlay whose bounding box lies outside it the fill adds nothing and the shape lights no part of a spotlight mask — spotlighting still runs, so an off-extent spotlight DIMS the whole image (a wide stroke may still paint at the edge). A note reports it. For a pure outline map with no satellite base, use geo-render_outline instead once it is available.",
"title": "Overlays"
},
"max_cloud_cover": {
"default": 30,
"description": "Scene-level filter: only mosaics scenes whose metadata cloud-cover percent is at or below this value, where the collection supports scene cloud metadata; otherwise ignored (a response note may be added). It does not mask cloudy pixels; accepted scenes can still show clouds. Prefer 10-30; a value of 0 requires perfectly cloud-free scenes and usually produces an empty image.",
"maximum": 100,
"minimum": 0,
"title": "Max Cloud Cover",
"type": "number"
},
"mosaicking_order": {
"default": "leastCC",
"description": "Scene priority when several match: leastCC (least cloudy, default), mostRecent, or leastRecent. Not used when composite=\"best_pixel\": that mode selects per pixel rather than per scene, so no scene ordering is sent at all (the response note says so).",
"enum": [
"mostRecent",
"leastCC",
"leastRecent"
],
"title": "Mosaicking Order",
"type": "string"
},
"composite": {
"default": "scene",
"description": "How several acquisitions in the window are combined. \"scene\" (default) mosaics whole scenes by mosaicking_order — a pixel takes whatever the winning scene has there, cloud included — so the render can still be attributed to the acquisition it came from, which is why it stays the default: it is the only mode that can answer \"what did this look like on the 17th\". \"best_pixel\" instead composites PER PIXEL over every acquisition in the window: observations Sentinel-2's SCL band calls cloud, cloud shadow or cirrus (plus snow/ice with mask_snow) are dropped, and the most recent survivor wins the pixel. That gives complete AND clean coverage where no single date does, at the price of an image assembled from many dates that depicts no single moment — a note says so and reports HOW MANY acquisition dates the window offered plus the earliest and latest of them as bounds — not which date each pixel came from, which a rendered image cannot carry. best_pixel needs a per-pixel quality band, so it is sentinel-2-l2a only, and it costs substantially more: cost scales with how many acquisitions fall in the window, so it is estimated from the catalog and capped before the billed request (see estimated_processing_units), and the output grid is coarsened, or the request refused, when the estimate is over budget. The cap is on the WHOLE request: compare panels share one pixel grid, so their combined cost is what is coarsened or refused. A window with no acquisition at all costs no processing units: the free catalog requests still run, but no billed Process request is sent for it, and every composited satellite pixel comes back transparent — any overlays you asked for are still burned on and stay visible.",
"enum": [
"scene",
"best_pixel"
],
"title": "Composite",
"type": "string"
},
"mask_snow": {
"default": false,
"description": "best_pixel only: also drop observations classified as snow/ice (SCL 11), on top of cloud, cloud shadow and cirrus. Off by default because snow is a real surface state, not contamination. Rejected with composite=\"scene\", which has no per-pixel masking at all — rather than accepted and silently ignored.",
"title": "Mask Snow",
"type": "boolean"
},
"width": {
"anyOf": [
{
"maximum": 1536,
"minimum": 64,
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Output width in pixels (64-1536); defaults to 512 when omitted. Height follows the AOI aspect ratio. Width beyond the AOI's native pixel count at the resolution of the chosen visualization (10 m Sentinel-2 — 20 m for its SWIR/red-edge index ramps nbr, ndmi, ndsi, ndre and nbr2 — 30 m Landsat, ~3 m PlanetScope) adds no detail; the server caps excessive values. Mutually exclusive with `resolution`.",
"title": "Width"
},
"brightness": {
"anyOf": [
{
"maximum": 4,
"minimum": 0.25,
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional tone adjustment for true_color/false_color renders (0.25-4.0, default 1 = no adjustment), for publication imagery. Applied as a gamma on top of the automatic per-scene 2-98% percentile stretch: values above 1 lift the midtones without clipping highlights, below 1 deepen them. Rejected for every index ramp — ndvi/ndwi/nbr/ndmi/ndsi/ndre/nbr2 — except the no-op 1.0; their colours encode index values and are never tone-adjusted. Not applied when the stretch does not run: it cannot be completed within the wall clock the request has left, or the render has no valid pixels to stretch (the response note says so in every case).",
"title": "Brightness"
},
"resolution": {
"anyOf": [
{
"const": "native",
"type": "string"
},
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Auto-size the output width from the AOI's ground extent instead of passing `width` yourself. \"native\" sizes to the native ground resolution of the chosen visualization (10 m Sentinel-2, but 20 m for its SWIR/red-edge index ramps nbr, ndmi, ndsi, ndre and nbr2; 30 m Landsat, ~3 m PlanetScope) so one output pixel equals one source pixel — the pixel-exact render, still capped at 1536px. A number sets an explicit target of that many meters per pixel (clamped no finer than native, since finer adds no detail). When the cap is hit, the achieved m/px is coarser than requested and a note says so; use a smaller AOI for a true native render. Mutually exclusive with `width`; leave both unset for the 512px default.",
"title": "Resolution"
}
},
"title": "RenderImageParams",
"type": "object",
"additionalProperties": false
} Result schema (JSON)
{
"$defs": {
"PanelResult": {
"description": "Outcome for one panel of a compare-mode render.",
"properties": {
"label": {
"description": "Panel label (also burned into the stitched image).",
"title": "Label",
"type": "string"
},
"time_from": {
"description": "Start of this panel's mosaicking window.",
"title": "Time From",
"type": "string"
},
"time_to": {
"description": "End of this panel's mosaicking window.",
"title": "Time To",
"type": "string"
},
"image_url": {
"description": "Short PNG URL for this panel alone (no label band). Valid for about a day, then the image is deleted.",
"title": "Image Url",
"type": "string"
},
"width": {
"description": "Panel width in pixels (shared by all panels).",
"title": "Width",
"type": "integer"
},
"height": {
"description": "Panel height in pixels (shared by all panels).",
"title": "Height",
"type": "integer"
},
"processing_units_spent": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Processing units this panel consumed, when reported.",
"title": "Processing Units Spent"
},
"note": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Per-panel guidance (blank-image detection).",
"title": "Note"
}
},
"required": [
"label",
"time_from",
"time_to",
"image_url",
"width",
"height"
],
"title": "PanelResult",
"type": "object"
}
},
"description": "Result of render_image.",
"properties": {
"success": {
"description": "Whether the operation succeeded",
"title": "Success",
"type": "boolean"
},
"image_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Short PNG URL — embed as a markdown image or share as a link. The stitched multi-panel image in compare mode. Valid for about a day, then the image is deleted.",
"title": "Image Url"
},
"expires_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "ISO-8601 UTC time after which the image is deleted.",
"title": "Expires At"
},
"width": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Rendered width in pixels; the stitched canvas width in compare mode (per-panel dimensions are on panels[]).",
"title": "Width"
},
"height": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Rendered height in pixels; the stitched canvas height in compare mode (per-panel dimensions are on panels[]).",
"title": "Height"
},
"visualization": {
"description": "Visualization that was rendered.",
"title": "Visualization",
"type": "string"
},
"collection": {
"description": "Collection used.",
"title": "Collection",
"type": "string"
},
"time_from": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Start of the mosaicking window; null in compare mode (each panel carries its own window).",
"title": "Time From"
},
"time_to": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "End of the mosaicking window; null in compare mode (each panel carries its own window).",
"title": "Time To"
},
"processing_units_spent": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Processing units this call consumed, when reported. In compare mode, the sum across panels, or null when any panel did not report a value (see panels[].processing_units_spent).",
"title": "Processing Units Spent"
},
"note": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Extra guidance, when relevant.",
"title": "Note"
},
"attribution": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Data-source attribution, set whenever overlays were requested and resolved — whether or not any of their pixels landed inside the extent: overlay geometry may include OpenStreetMap data, which ODbL requires be attributed, and over-attribution is the safe side. Null when no overlays were requested.",
"title": "Attribution"
},
"achieved_meters_per_px": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Ground resolution of the output in meters per pixel; set only when `resolution` was used. Equals the requested/native target unless the AOI hit a dimension limit: coarser when it rode the max-pixel cap, finer when it rode the minimum render size (see note). The native target is the chosen visualization's, so it is 20 m for the Sentinel-2 SWIR/red-edge index ramps.",
"title": "Achieved Meters Per Px"
},
"composite": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "The compositing mode that produced this image: \"scene\" (whole scenes mosaicked by mosaicking_order) or \"best_pixel\" (per-pixel selection over every acquisition in the window). Echoes the request, and is set on every successful render.",
"title": "Composite"
},
"composite_acquisition_dates": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "best_pixel only: how many distinct UTC acquisition dates the catalog enumerated for the composited window(s) — the population the per-pixel reducer chose from, and what the cost estimate was sized on. In compare mode this is the number of distinct dates summed over the panels' windows, so overlapping panel windows count a shared date once per panel — and that sum is what the budget and the ceiling are enforced against, because the panels share one pixel grid. 0 means no acquisition matched at all, and then no billed Process request was sent (the free catalog requests that established it were): every composited pixel is transparent and estimated_processing_units is 0. Null in scene mode.",
"title": "Composite Acquisition Dates"
},
"composite_earliest_observation": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "best_pixel only: the earliest UTC acquisition date (YYYY-MM-DD) the composite could draw on, from the catalog enumeration of the actual acquisitions rather than from the requested window. With composite_latest_observation it BOUNDS the contributing dates — every rendered pixel comes from a date in this range — but does not name them: an acquisition rejected as cloudy at every pixel contributed nothing, and a 4-channel render carries colour, not provenance. Null in scene mode and when no acquisition matched the window at all.",
"title": "Composite Earliest Observation"
},
"composite_latest_observation": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "best_pixel only: the latest UTC acquisition date (YYYY-MM-DD) the composite could draw on. See composite_earliest_observation for why this bounds the contributing dates rather than naming them. Null in scene mode and when no acquisition matched the window.",
"title": "Composite Latest Observation"
},
"estimated_processing_units": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "best_pixel only: the cost estimated for the WHOLE REQUEST from the catalog count BEFORE the billed request, and what the grid was coarsened (or the request refused) against — (width*height / 512^2) * (4/3) * composite_acquisition_dates, over the COMPOSITED grid (panels[].width/height in compare mode, not the stitched canvas), which the panels share, so their dates are summed across them. 0 when the catalog found no acquisition in any window, because no billed request is then sent. processing_units_spent is what the provider actually charged. Null in scene mode, where no estimate is made.",
"title": "Estimated Processing Units"
},
"comparison": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"default": null,
"description": "True when this is a compare-mode render (see panels).",
"title": "Comparison"
},
"panels": {
"anyOf": [
{
"items": {
"$ref": "#/$defs/PanelResult"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "Compare mode only: one entry per rendered panel, in temporal order (sorted by time_from).",
"title": "Panels"
},
"visual": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "Standardized visual payload for widget-capable clients ({kind: compare, items: [{url, title, description?}], stitched_url}).",
"title": "Visual"
}
},
"required": [
"success",
"visualization",
"collection"
],
"title": "RenderImageResult",
"type": "object"
} # search_catalog read
Search the Sentinel Hub catalog for satellite scenes over an area (inline aoi, bbox, or a geo aoi_ref handle): acquisition times, cloud cover, and footprints.
Full description
Search satellite scenes (STAC catalog) over an area: aoi, bbox, or aoi_ref. Returns scene ids, acquisition times, cloud cover, and footprints for the requested collection and time range. Use it to find good (e.g. least cloudy) dates before calling render_image, or to check data availability before get_statistics. The area can be inline GeoJSON (`aoi`), a `bbox`, or an `aoi_ref` handle minted by the geo toolkit, which resolves server-side to the stored full-fidelity geometry (refs expire after about 48 hours). Free CDSE accounts cover the Sentinel missions; Landsat/MODIS/commercial collections need Planet credentials with the endpoint field set to 'planet'.
| Parameter | Type | Required | Description |
|---|---|---|---|
aoi | object | null | No | GeoJSON Polygon/MultiPolygon geometry (WGS84 lon/lat) to search over. Accepts the `geojson`/`merged_geojson` output of the geo toolkit. Provide exactly one of aoi, bbox, or aoi_ref. |
bbox | array<number> | null | No | [west, south, east, north] in WGS84 degrees. Alternative to aoi. |
aoi_ref | string | null | No | Stored AOI handle from the geo toolkit (the `aoi_ref`/`merged_aoi_ref` field of lookup_place, make_aoi, or query_features). Resolves server-side to the full-fidelity geometry, so no GeoJSON passes through the conversation; refs expire after about 48 hours. |
collection | string | No | Data collection id, e.g. sentinel-2-l2a (default), sentinel-2-l1c, sentinel-1-grd. Planet-endpoint accounts can also use the Landsat archive: landsat-ot-l2 (Landsat 8/9, 2013 onwards), landsat-etm-l2 (Landsat 7, 1999-2024; scenes after 2003-05 are SLC-off, missing ~22% of pixels in stripes, so prefer landsat-tm-l2 for 2003-2011), landsat-tm-l2 (Landsat 4/5, 1982-2012), or byoc-<id> for a PlanetScope subscription collection. Default: "sentinel-2-l2a" |
time_from | string | Yes | Start date (YYYY-MM-DD) or ISO datetime. |
time_to | string | Yes | End date (YYYY-MM-DD) or ISO datetime. |
max_cloud_cover | number | null | No | Maximum scene-level metadata cloud cover percent. Filters whole scenes by metadata, not pixels. Applied only where the collection supports scene cloud metadata (Sentinel-2, Landsat); for collections without it (for example byoc-* PlanetScope) the filter is skipped and a response note reports that max_cloud_cover was ignored, rather than matching zero scenes. |
limit | integer | No | Maximum number of scenes to return (1-50). Default: 10 |
Parameter schema (JSON)
{
"description": "Parameters for search_catalog.",
"properties": {
"aoi": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "GeoJSON Polygon/MultiPolygon geometry (WGS84 lon/lat) to search over. Accepts the `geojson`/`merged_geojson` output of the geo toolkit. Provide exactly one of aoi, bbox, or aoi_ref.",
"title": "Aoi"
},
"bbox": {
"anyOf": [
{
"items": {
"type": "number"
},
"maxItems": 4,
"minItems": 4,
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "[west, south, east, north] in WGS84 degrees. Alternative to aoi.",
"title": "Bbox"
},
"aoi_ref": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Stored AOI handle from the geo toolkit (the `aoi_ref`/`merged_aoi_ref` field of lookup_place, make_aoi, or query_features). Resolves server-side to the full-fidelity geometry, so no GeoJSON passes through the conversation; refs expire after about 48 hours.",
"title": "Aoi Ref"
},
"collection": {
"default": "sentinel-2-l2a",
"description": "Data collection id, e.g. sentinel-2-l2a (default), sentinel-2-l1c, sentinel-1-grd. Planet-endpoint accounts can also use the Landsat archive: landsat-ot-l2 (Landsat 8/9, 2013 onwards), landsat-etm-l2 (Landsat 7, 1999-2024; scenes after 2003-05 are SLC-off, missing ~22% of pixels in stripes, so prefer landsat-tm-l2 for 2003-2011), landsat-tm-l2 (Landsat 4/5, 1982-2012), or byoc-<id> for a PlanetScope subscription collection.",
"title": "Collection",
"type": "string"
},
"time_from": {
"description": "Start date (YYYY-MM-DD) or ISO datetime.",
"title": "Time From",
"type": "string"
},
"time_to": {
"description": "End date (YYYY-MM-DD) or ISO datetime.",
"title": "Time To",
"type": "string"
},
"max_cloud_cover": {
"anyOf": [
{
"maximum": 100,
"minimum": 0,
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Maximum scene-level metadata cloud cover percent. Filters whole scenes by metadata, not pixels. Applied only where the collection supports scene cloud metadata (Sentinel-2, Landsat); for collections without it (for example byoc-* PlanetScope) the filter is skipped and a response note reports that max_cloud_cover was ignored, rather than matching zero scenes.",
"title": "Max Cloud Cover"
},
"limit": {
"default": 10,
"description": "Maximum number of scenes to return (1-50).",
"maximum": 50,
"minimum": 1,
"title": "Limit",
"type": "integer"
}
},
"required": [
"time_from",
"time_to"
],
"title": "SearchCatalogParams",
"type": "object",
"additionalProperties": false
} Result schema (JSON)
{
"$defs": {
"SceneInfo": {
"description": "One catalog scene.",
"properties": {
"id": {
"description": "Scene/item id (usable for traceability).",
"title": "Id",
"type": "string"
},
"datetime": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Acquisition time (UTC).",
"title": "Datetime"
},
"cloud_cover": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Scene cloud cover percent, when the collection reports it.",
"title": "Cloud Cover"
},
"bbox": {
"anyOf": [
{
"items": {
"type": "number"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "Scene footprint bbox [west, south, east, north].",
"title": "Bbox"
},
"thumbnail_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Low-res quicklook URL, when the collection provides one.",
"title": "Thumbnail Url"
}
},
"required": [
"id"
],
"title": "SceneInfo",
"type": "object"
}
},
"description": "Result of search_catalog.",
"properties": {
"success": {
"description": "Whether the operation succeeded",
"title": "Success",
"type": "boolean"
},
"collection": {
"description": "Collection that was searched.",
"title": "Collection",
"type": "string"
},
"scene_count": {
"description": "Number of scenes returned.",
"title": "Scene Count",
"type": "integer"
},
"scenes": {
"description": "Matching scenes.",
"items": {
"$ref": "#/$defs/SceneInfo"
},
"title": "Scenes",
"type": "array"
},
"truncated": {
"default": false,
"description": "True when more scenes matched than `limit` returned.",
"title": "Truncated",
"type": "boolean"
},
"note": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Extra guidance, when relevant.",
"title": "Note"
}
},
"required": [
"success",
"collection",
"scene_count"
],
"title": "SearchCatalogResult",
"type": "object"
} # temporal_map write
Date per-pixel change across a time window in one pass: when each pixel crossed an index threshold (or hit its extreme), as a date histogram and changed area, plus — best-effort, null with the reason in `note` — a colour-mapped map image and a reusable outline of the changed area.
Full description
Date WHEN each pixel changed, across a whole window, in one provider call. Where get_statistics answers "how did the average of this AOI move over time", this answers "which pixels changed, and when" — a per-pixel date raster. Typical uses: dating a burn scar (index=nbr, statistic=first_below, threshold about -0.10, months restricted to the fire season), mapping the advancing front of a quarry or a reservoir drawdown (statistic=last_above/last_below), or timing a disturbance without picking a threshold at all (statistic=date_of_min with min_change). The response carries a date histogram, the changed area, a map image when one could be produced, and — the point of the tool — `changed_aoi_ref`, an AOI handle for the changed area itself, which can be fed straight back to get_statistics so the follow-up series is computed over the pixels that actually changed rather than diluted across the whole AOI. HOW A PIXEL IS DATED. Every date is the date of a VALID OBSERVATION at that pixel. Valid means: the pixel was in a scene, the scene-classification band did not call it cloud, cloud shadow or cirrus (nor snow, if mask_snow is set), and the index value is finite and inside -1..1. Invalid observations are skipped entirely — they never date a crossing, never extend or break a persistence run, never enter the extrema set or the median baseline, and are not counted in valid_observations_*. Because the fold only ever sees observations, the real change lies between a pixel's reported date and its previous valid observation; valid_observations_min/median are how you size that gap, and they are per-pixel rather than a catalog cadence because masking removes observations unevenly. THE SIX STATISTICS. first_below / first_above date onset: the first observation of the earliest run of `persistence` consecutive valid observations past `threshold`, never overwritten by anything later. last_below / last_above date a completed transition: the LAST observation past the threshold — but cleared to "no date" if the pixel was still past it at its final valid observation, because a pixel that never came back has no completed transition. So above,below,below dates that "above"; above,below, above and above,above are undated, and undated is indistinguishable from "never crossed" by design. date_of_min / date_of_max date the extreme value, but only where it differs from that pixel's own median baseline by at least `min_change`, which is required and has no default: with a zero magnitude test every observed pixel would be "changed" and the changed area would describe the whole AOI. For date_of_min/date_of_max, `threshold` is only an additional gate. Across all six, a pixel counts as changed if and only if it got a date, and changed_area_km2, changed_pixel_fraction, the histogram and changed_aoi_ref all mean exactly that — so for the extrema statistics "changed" means "moved by at least min_change", not "crossed something". SEASONALITY IS NOT OPTIONAL FOR VEGETATION INDICES. A vegetation-sensitive index (ndvi, nbr, ndre, ndmi) over an AOI outside the tropics, across more than about 14 months, with no `months` filter, is REFUSED: deciduous leaf-off crosses any sensible threshold every autumn, so the tool would confidently date normal phenology as change. The error names the fix — pass `months` (e.g. [6,7,8,9] for a northern growing season), which also cuts the mosaic count — see RESOLUTION AND COST for what that does and does not do to the bill. Passing all twelve months is the explicit opt-in. COLLECTION. sentinel-2-l2a only in this version. Per-pixel masking is what makes a date a change date rather than a cloud date, and only L2A carries the scene-classification band, so L1C and the Landsat collections are rejected by name rather than served unmasked. Landsat support is a documented follow-up. RESOLUTION AND COST. The grid targets the index's native ground sample distance — 20 m, not 10 m, for the Sentinel-2 SWIR, red-edge and narrow-NIR indices (nbr, ndmi, ndsi, ndre, nbr2) — capped at 1536 cells per axis in this version, and then coarsened as far as the cost budget requires. effective_resolution_m reports what was used and auto_coarsened says whether it is coarser than native; a coarsened date map is still the right answer, but a caller who assumes 10 m and got 40 m will over-trust the outline. The fold reads every acquisition in the window rather than one mosaic per interval, so for the same AOI and window it typically costs 3-10x a monthly get_statistics series — one mosaic per covered day instead of one per month. That multiplier IS the acquisition density over your window, not a constant: covered_days against the number of months is the ratio for your request. WHAT THE COST IS LINEAR IN, EXACTLY, and this is the one paragraph worth reading twice. `PU = (cells / 512^2) * (bands/3) * mosaics`, so the bill is linear in the RETAINED MOSAIC COUNT at a fixed grid, and linear in the CELL COUNT at a fixed mosaic count. It is not linear in anything you actually pass, and two mechanisms stand between your parameters and those two numbers: * **A parameter's magnitude does not map to a mosaic count.** `months` and a shorter `time_from`/`time_to` both remove acquisitions, but acquisition density is not uniform across the calendar or across the window — orbit coverage, archive gaps and `max_cloud_cover` all vary — so keeping half the months, or halving the span, can retain almost none of the acquisitions or almost all of them. `covered_days` (or `covered_days_upper_bound`) and `catalog_features_matched` report what was actually retained. * **The grid is re-solved against whatever mosaic count survives.** The permitted pixel count is the budget divided by the mosaic count, so removing mosaics RAISES the pixels the budget buys. While `auto_coarsened` is true the saving is spent on a finer grid and `estimated_processing_units` stays near 300; a smaller AOI that still exceeds the permitted count likewise buys resolution rather than savings. Only once the grid is no longer the binding constraint does anything that removes mosaics or cells show up in the bill. So there is no lever whose effect on the bill can be predicted from its own size. Filtering and shrinking reliably improve the ANSWER (a finer grid, or a season that means something); whether they also reduce the bill is what `estimated_processing_units`, `auto_coarsened` and `effective_resolution_m` are there to tell you, and `processing_units_spent` is authoritative after the fact. A request whose cost cannot be brought under 1000 units even at the coarsest grid this tool will use is refused — a fail-closed backstop rather than a limit callers meet, because reaching it at that grid would take hundreds of thousands of mosaics and the catalog enumeration cap refuses far earlier. The acquisitions are counted for free from the catalog first, and if that count cannot be established the fold is not started at all rather than billed blind. A window the catalog reports NO acquisitions for is refused there too, rather than billed for a map that could only come back empty — and so is a window whose acquisitions were all enumerated and every one of them fell outside `months`. That cheap upper bound is the catalog's own match count, and not every catalog deployment reports one: where none is reported the acquisitions are ALWAYS enumerated instead (equally free, just slower), which makes the covered-day count exact rather than bounded. When the cheap upper bound WAS taken, no acquisition was enumerated and the surviving day count is unknown, so only a wholly empty window is caught there. WHAT DEGRADES AND WHAT FAILS. The statistics are the result; the map and the outline are best-effort. The promise is about the OPTIONAL phases only, and it is absolute for them: once the fold's processing units are spent, no failure of any optional phase throws the result away. If the colormap or its upload cannot finish in the wall clock left, cannot be started at all, or fails outright, image_url is null and `note` says why and whether anything happened (including the case where an upload was abandoned in flight, so whether the object landed is unknown); if the outline cannot be built, reduced, or stored — or if outlining fails unexpectedly before it gets that far — changed_aoi_ref is null and `note` says which of those it was. The INLINE fallback is narrower than that, and deliberately so: an outline can only be returned inline if one was actually produced. So changed_aoi_geojson is offered exactly when the vectorizer finished and the STORE step is what failed — the write was skipped for lack of wall clock, or it was attempted and failed — and it fits the inline vertex budget. On every other null-ref path there is no geometry in hand to return: no pixel changed, nothing survived the speck drop and the clip, the shape could not be reduced to the vertex budget, the vectorizer raised, or the whole mint phase was never started / timed out / was abandoned — and an abandoned worker's result never comes back to be returned, even if it eventually finishes. `note` names which of those happened. The two are mutually exclusive — a successful changed_aoi_ref always comes with changed_aoi_geojson null, because the handle exists precisely to keep bulk geometry out of the conversation, so read the outline through the ref and expect the inline copy only as the store-failure fallback. Failures BEFORE the fold — an unresolvable ref, an uncountable catalog, a window with no acquisitions at all, an enumerated window with none in the selected `months`, a cost that cannot be brought under budget, no wall clock left — return an error and cost nothing. The MANDATORY decode is the exception, and the only one. It sits between the fold and every optional phase, there is no answer at all without it, so it FAILS THE CALL — after the processing units were spent — on any of six things: payload validation (bytes that are not a PNG of exactly the requested grid), an empty AOI grid (no cell at least half inside the AOI at the resolution used), no wall clock left for it (none at admission, or overrunning what it had), its worker slot still busy with an earlier decode, a submission failure, or the decode itself raising. Each error says plainly that the imagery had already been paid for. So "nothing after the fold throws the statistics away" holds for everything after the decode, not for the decode itself. changed_aoi_ref expires after about 48 hours. changed_aoi_area_km2 is measured from equal-area pixel counts — each grid row's exact spherical cell area times that row's changed-cell count — not by integrating the simplified outline, so it is comparable with changed_area_km2 by construction. It is a SUBSET of changed_area_km2: the cell set is intersected back with the native changed mask, so the only thing that removes cells is the outline's component filter (specks dropped on the working grid). Widening a thin feature to one working cell (changed_aoi_min_feature_width_m) is a tolerance of the outline and adds no area, and clipping the outline to your AOI happens after the measurement and removes none.
| Parameter | Type | Required | Description |
|---|---|---|---|
aoi | object | null | No | GeoJSON Polygon/MultiPolygon geometry (WGS84). Accepts the geo toolkit's `geojson` output. Provide exactly one of aoi, bbox, or aoi_ref. |
bbox | array<number> | null | No | [west, south, east, north] WGS84 degrees. Alternative to aoi. |
aoi_ref | string | null | No | Stored AOI handle from the geo toolkit, or a `changed_aoi_ref` from an earlier temporal_map call. Resolves server-side to the full-fidelity geometry; refs expire after about 48 hours. |
index | "ndvi" | "ndwi" | "nbr" | "ndmi" | "ndsi" | "ndre" | "nbr2" | Yes | Spectral index, all normalized differences in -1..1, and the same seven get_statistics computes. ndvi: vegetation greenness. ndwi: surface water. nbr: burn severity (the standard wildfire index). ndmi: vegetation water content. ndsi: snow versus cloud. ndre: crop/vegetation stress. nbr2: post-fire residue and dry biomass. On Sentinel-2 the SWIR, red-edge and narrow-NIR bands are 20 m native, so nbr, ndmi, ndsi, ndre and nbr2 sample at 20 m rather than 10 m (see effective_resolution_m). |
collection | string | No | Collection id. sentinel-2-l2a only in this version, and the restriction is deliberate: every date returned here is the date of a valid observation, and validity is the per-pixel scene classification (SCL) test, which only L2A carries. On a collection without SCL an unmasked cloud would date as a change, so those collections are rejected rather than served with a caveat. Landsat support is a documented follow-up. Default: "sentinel-2-l2a" |
time_from | string | Yes | Start date (YYYY-MM-DD) or ISO datetime. |
time_to | string | Yes | End date (YYYY-MM-DD) or ISO datetime. |
statistic | "first_below" | "first_above" | "last_below" | "last_above" | "date_of_min" | "date_of_max" | Yes | What date each pixel gets, computed over that pixel's own ascending sequence of VALID observations. first_below / first_above: the date of the first observation of the earliest run of `persistence` consecutive valid observations on the far side of `threshold` — onset dating, never overwritten later. last_below / last_above: the date of the LAST observation on the far side of `threshold`, cleared to 'no date' if the pixel was still there at its final valid observation (because then it never came back, so there is no completed transition to date) — so above,below,below dates that 'above', while above,below,above and above,above are undated. date_of_min / date_of_max: the date of the pixel's extreme value, but only where that extreme differs from the pixel's own median baseline by at least `min_change`; `threshold` is an optional extra gate there and never a substitute for the magnitude test. For all six, a pixel counts as changed if and only if it got a date, and changed_area_km2 / changed_aoi_ref / the histogram all mean exactly that. |
threshold | number | null | No | The index value the test compares against, within -1..1. REQUIRED for first_below, first_above, last_below and last_above — it is their test. For date_of_min/date_of_max it is optional and acts only as an extra gate on the extreme value (the change test there is `min_change`); it is not accepted as a substitute for it. |
min_change | number | null | No | How far, in index units, a pixel's extreme value must differ from that pixel's own median baseline before it counts as changed. REQUIRED for date_of_min and date_of_max, with no default: at zero every pixel with a valid observation would be dated and the 'changed' area would describe the whole AOI. Typical values are 0.1-0.3. Rejected for the four crossing statistics, whose test is `threshold`. |
persistence | integer | null | No | How many CONSECUTIVE VALID OBSERVATIONS must satisfy the test before a crossing is dated — observations, not days or weeks, so a cloud gap cannot break a run and a cloudy scene cannot fake one. Applies to first_below and first_above only, where it defaults to 2 (one anomalous observation cannot date a change; the reported date is still the FIRST observation of the run, so requiring persistence does not push the answer later). Maximum 20. REJECTED for last_below, last_above, date_of_min and date_of_max rather than accepted and ignored: a last_* answer is defined by the final qualifying observation and an extremum is a single observation. |
months | array<integer> | null | No | Calendar months (1-12) the fold may look at; observations in other months are dropped before they are fetched. What that does to the bill is NOT predictable from how many months you keep. Cost is linear in the RETAINED MOSAIC COUNT at a fixed grid, and a month selection's effect on that count is not proportional to its span: acquisition density varies month to month with orbit coverage, archive gaps and (where you set one) max_cloud_cover, so half the months can retain almost none of the acquisitions or almost all of them. The grid is not fixed either — while auto_coarsened is true the saving is spent on a finer grid and estimated_processing_units stays near the budget. Read estimated_processing_units, auto_coarsened and effective_resolution_m for what actually happened. Dates can then only fall in the months you allowed. REQUIRED (with an error naming the fix) for the one configuration where an unfiltered fold is reliably wrong: a vegetation-sensitive index (ndvi, nbr, ndre, ndmi) over an AOI outside the tropics across more than ~14 months, where deciduous leaf-off crosses any sensible threshold every autumn. Passing all twelve months is the explicit opt-in and applies no filter. |
max_cloud_cover | number | null | No | Scene-level filter: whole scenes whose metadata cloud-cover percent exceeds this value are excluded from the fold, and from the acquisition count the cost is estimated from. It is not a substitute for the per-pixel SCL masking, which is always applied here; its value is dropping scenes that would have been masked away anyway. Like `months` it removes mosaics, and like `months` its effect on the bill is not predictable from its own magnitude: how many acquisitions sit above any given cloud percentage depends on the window and the AOI, and while auto_coarsened is true the saving is spent on a finer grid rather than a smaller bill. estimated_processing_units is what actually happened. |
mask_snow | boolean | No | Also treat snow/ice pixels (SCL class 11) as invalid observations. Off by default because snow is a real surface state rather than contamination — but over a winter window an ndvi or ndwi crossing may well be snow, so set it when that would be a false positive. Cloud, cloud-shadow and cirrus pixels are ALWAYS excluded here and there is no flag to include them: a date derived from unmasked cloud is a cloud date, not a change date. Default: false |
resolution_m | number | null | No | Target metres per grid cell. The default is the index's native resolution, capped at 1536 cells per axis and then coarsened as far as the processing-unit budget requires; values finer than native are clamped to native. A coarser explicit value lowers the TARGET cell count, and cost is linear in the cell count at a fixed mosaic count — but it only reaches the bill if your value is coarser than the grid the budget was already going to force, and it cannot make an otherwise-refused request admissible: the budget coarsens on its own, and the refusal that callers actually meet is on the acquisition count, which this does not touch. effective_resolution_m always reports what was actually used, and auto_coarsened says whether the budget was the binding limit. |
Parameter schema (JSON)
{
"description": "Parameters for temporal_map.",
"properties": {
"aoi": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "GeoJSON Polygon/MultiPolygon geometry (WGS84). Accepts the geo toolkit's `geojson` output. Provide exactly one of aoi, bbox, or aoi_ref.",
"title": "Aoi"
},
"bbox": {
"anyOf": [
{
"items": {
"type": "number"
},
"maxItems": 4,
"minItems": 4,
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "[west, south, east, north] WGS84 degrees. Alternative to aoi.",
"title": "Bbox"
},
"aoi_ref": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Stored AOI handle from the geo toolkit, or a `changed_aoi_ref` from an earlier temporal_map call. Resolves server-side to the full-fidelity geometry; refs expire after about 48 hours.",
"title": "Aoi Ref"
},
"index": {
"description": "Spectral index, all normalized differences in -1..1, and the same seven get_statistics computes. ndvi: vegetation greenness. ndwi: surface water. nbr: burn severity (the standard wildfire index). ndmi: vegetation water content. ndsi: snow versus cloud. ndre: crop/vegetation stress. nbr2: post-fire residue and dry biomass. On Sentinel-2 the SWIR, red-edge and narrow-NIR bands are 20 m native, so nbr, ndmi, ndsi, ndre and nbr2 sample at 20 m rather than 10 m (see effective_resolution_m).",
"enum": [
"ndvi",
"ndwi",
"nbr",
"ndmi",
"ndsi",
"ndre",
"nbr2"
],
"title": "Index",
"type": "string"
},
"collection": {
"default": "sentinel-2-l2a",
"description": "Collection id. sentinel-2-l2a only in this version, and the restriction is deliberate: every date returned here is the date of a valid observation, and validity is the per-pixel scene classification (SCL) test, which only L2A carries. On a collection without SCL an unmasked cloud would date as a change, so those collections are rejected rather than served with a caveat. Landsat support is a documented follow-up.",
"title": "Collection",
"type": "string"
},
"time_from": {
"description": "Start date (YYYY-MM-DD) or ISO datetime.",
"title": "Time From",
"type": "string"
},
"time_to": {
"description": "End date (YYYY-MM-DD) or ISO datetime.",
"title": "Time To",
"type": "string"
},
"statistic": {
"description": "What date each pixel gets, computed over that pixel's own ascending sequence of VALID observations. first_below / first_above: the date of the first observation of the earliest run of `persistence` consecutive valid observations on the far side of `threshold` — onset dating, never overwritten later. last_below / last_above: the date of the LAST observation on the far side of `threshold`, cleared to 'no date' if the pixel was still there at its final valid observation (because then it never came back, so there is no completed transition to date) — so above,below,below dates that 'above', while above,below,above and above,above are undated. date_of_min / date_of_max: the date of the pixel's extreme value, but only where that extreme differs from the pixel's own median baseline by at least `min_change`; `threshold` is an optional extra gate there and never a substitute for the magnitude test. For all six, a pixel counts as changed if and only if it got a date, and changed_area_km2 / changed_aoi_ref / the histogram all mean exactly that.",
"enum": [
"first_below",
"first_above",
"last_below",
"last_above",
"date_of_min",
"date_of_max"
],
"title": "Statistic",
"type": "string"
},
"threshold": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "The index value the test compares against, within -1..1. REQUIRED for first_below, first_above, last_below and last_above — it is their test. For date_of_min/date_of_max it is optional and acts only as an extra gate on the extreme value (the change test there is `min_change`); it is not accepted as a substitute for it.",
"title": "Threshold"
},
"min_change": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "How far, in index units, a pixel's extreme value must differ from that pixel's own median baseline before it counts as changed. REQUIRED for date_of_min and date_of_max, with no default: at zero every pixel with a valid observation would be dated and the 'changed' area would describe the whole AOI. Typical values are 0.1-0.3. Rejected for the four crossing statistics, whose test is `threshold`.",
"title": "Min Change"
},
"persistence": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "How many CONSECUTIVE VALID OBSERVATIONS must satisfy the test before a crossing is dated — observations, not days or weeks, so a cloud gap cannot break a run and a cloudy scene cannot fake one. Applies to first_below and first_above only, where it defaults to 2 (one anomalous observation cannot date a change; the reported date is still the FIRST observation of the run, so requiring persistence does not push the answer later). Maximum 20. REJECTED for last_below, last_above, date_of_min and date_of_max rather than accepted and ignored: a last_* answer is defined by the final qualifying observation and an extremum is a single observation.",
"title": "Persistence"
},
"months": {
"anyOf": [
{
"items": {
"type": "integer"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "Calendar months (1-12) the fold may look at; observations in other months are dropped before they are fetched. What that does to the bill is NOT predictable from how many months you keep. Cost is linear in the RETAINED MOSAIC COUNT at a fixed grid, and a month selection's effect on that count is not proportional to its span: acquisition density varies month to month with orbit coverage, archive gaps and (where you set one) max_cloud_cover, so half the months can retain almost none of the acquisitions or almost all of them. The grid is not fixed either — while auto_coarsened is true the saving is spent on a finer grid and estimated_processing_units stays near the budget. Read estimated_processing_units, auto_coarsened and effective_resolution_m for what actually happened. Dates can then only fall in the months you allowed. REQUIRED (with an error naming the fix) for the one configuration where an unfiltered fold is reliably wrong: a vegetation-sensitive index (ndvi, nbr, ndre, ndmi) over an AOI outside the tropics across more than ~14 months, where deciduous leaf-off crosses any sensible threshold every autumn. Passing all twelve months is the explicit opt-in and applies no filter.",
"title": "Months"
},
"max_cloud_cover": {
"anyOf": [
{
"maximum": 100,
"minimum": 0,
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Scene-level filter: whole scenes whose metadata cloud-cover percent exceeds this value are excluded from the fold, and from the acquisition count the cost is estimated from. It is not a substitute for the per-pixel SCL masking, which is always applied here; its value is dropping scenes that would have been masked away anyway. Like `months` it removes mosaics, and like `months` its effect on the bill is not predictable from its own magnitude: how many acquisitions sit above any given cloud percentage depends on the window and the AOI, and while auto_coarsened is true the saving is spent on a finer grid rather than a smaller bill. estimated_processing_units is what actually happened.",
"title": "Max Cloud Cover"
},
"mask_snow": {
"default": false,
"description": "Also treat snow/ice pixels (SCL class 11) as invalid observations. Off by default because snow is a real surface state rather than contamination — but over a winter window an ndvi or ndwi crossing may well be snow, so set it when that would be a false positive. Cloud, cloud-shadow and cirrus pixels are ALWAYS excluded here and there is no flag to include them: a date derived from unmasked cloud is a cloud date, not a change date.",
"title": "Mask Snow",
"type": "boolean"
},
"resolution_m": {
"anyOf": [
{
"maximum": 10000,
"minimum": 1,
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Target metres per grid cell. The default is the index's native resolution, capped at 1536 cells per axis and then coarsened as far as the processing-unit budget requires; values finer than native are clamped to native. A coarser explicit value lowers the TARGET cell count, and cost is linear in the cell count at a fixed mosaic count — but it only reaches the bill if your value is coarser than the grid the budget was already going to force, and it cannot make an otherwise-refused request admissible: the budget coarsens on its own, and the refusal that callers actually meet is on the acquisition count, which this does not touch. effective_resolution_m always reports what was actually used, and auto_coarsened says whether the budget was the binding limit.",
"title": "Resolution M"
}
},
"required": [
"index",
"time_from",
"time_to",
"statistic"
],
"title": "TemporalMapParams",
"type": "object",
"additionalProperties": false
} Result schema (JSON)
{
"$defs": {
"TemporalDateBin": {
"description": "One bin of the change-date histogram.",
"properties": {
"start_date": {
"description": "First UTC date in the bin.",
"title": "Start Date",
"type": "string"
},
"end_date": {
"description": "Last UTC date in the bin (inclusive).",
"title": "End Date",
"type": "string"
},
"pixel_count": {
"description": "Changed pixels dated inside the bin.",
"title": "Pixel Count",
"type": "integer"
},
"area_km2": {
"description": "Area of those pixels, summed from each row's exact spherical cell area rather than from one average cell size.",
"title": "Area Km2",
"type": "number"
}
},
"required": [
"start_date",
"end_date",
"pixel_count",
"area_km2"
],
"title": "TemporalDateBin",
"type": "object"
}
},
"description": "Result of temporal_map.",
"properties": {
"success": {
"description": "Whether the operation succeeded",
"title": "Success",
"type": "boolean"
},
"index": {
"description": "Spectral index folded.",
"title": "Index",
"type": "string"
},
"collection": {
"description": "Collection used.",
"title": "Collection",
"type": "string"
},
"statistic": {
"description": "Dating rule applied (see the parameter).",
"title": "Statistic",
"type": "string"
},
"threshold": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Threshold used, when the statistic uses one.",
"title": "Threshold"
},
"min_change": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Magnitude test used, for the extrema statistics.",
"title": "Min Change"
},
"persistence": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Consecutive valid observations required, for first_below/first_above. Null for every other statistic, where the parameter does not apply and is rejected.",
"title": "Persistence"
},
"months_applied": {
"anyOf": [
{
"items": {
"type": "integer"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "The calendar months the fold was allowed to see, deduplicated and sorted; dates can only fall in these months. Null means `months` was OMITTED. An explicit [1,2,3,4,5,6,7,8,9,10,11,12] is echoed back in full rather than as null, because passing all twelve is the deliberate all-months opt-in and that is a different fact from not having passed the parameter — even though no month filter is sent to the fold in either case, keeping every month being a tautology.",
"title": "Months Applied"
},
"image_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "The colour-mapped date map with a legend: a ramp over the OBSERVED date range, plus distinct flat colours for 'observed but not changed', 'covered but no valid observation' and 'no data'. Valid for about a day, then deleted. Null — with the reason in `note` — on EVERY way either of its two phases can fail: the colormap having no wall clock left, its worker slot busy, its submission refused, or the encode raising; and the upload having no honest budget, its slot busy, never being queued (a definite nothing-written), being abandoned in flight so that whether the object reached storage is unknown, or failing for an unexpected reason. The statistics below are complete in every one of those cases: they are the result, and the map is best-effort, deliberately unlike render_image where the image IS the product and a failed upload fails the call.",
"title": "Image Url"
},
"expires_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "ISO-8601 UTC time after which the image is deleted. Null exactly when image_url is — it describes an object, so it is only present when one was uploaded.",
"title": "Expires At"
},
"histogram": {
"description": "Changed-pixel counts and areas per date interval, chronological, with ZERO-COUNT BINS INCLUDED so the series is alignable and gaps are visible. Bins are equal-width in days over the observed date range (at most 24 of them); a single-date result yields one bin, and a result with no changed pixels yields an empty list.",
"items": {
"$ref": "#/$defs/TemporalDateBin"
},
"title": "Histogram",
"type": "array"
},
"changed_pixel_count": {
"default": 0,
"description": "Grid cells that got a date, inside the AOI and covered.",
"title": "Changed Pixel Count",
"type": "integer"
},
"changed_area_km2": {
"default": 0,
"description": "Area of the changed cells: the sum over grid rows of each row's exact spherical lat/lon cell area times that row's changed-cell count. No polygon outline is integrated anywhere. Envelope: a spherical Earth, ~0.3-0.5% against WGS84, far below the quantization of the grid itself.",
"title": "Changed Area Km2",
"type": "number"
},
"changed_pixel_fraction": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "changed_pixel_count divided by the number of in-AOI cells that at least one scene COVERED — not by the whole AOI, so a partly uncovered AOI does not flatter the fraction. Null when nothing was covered (see aoi_covered_fraction).",
"title": "Changed Pixel Fraction"
},
"earliest_change_date": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Earliest date any pixel was given, UTC. Null when no pixel changed. It is the date of an OBSERVATION: the underlying change lies between it and that pixel's previous valid observation.",
"title": "Earliest Change Date"
},
"latest_change_date": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Latest date any pixel was given, UTC; null if none changed.",
"title": "Latest Change Date"
},
"median_change_date": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Median over the CHANGED PIXELS themselves (one vote each), so it does not depend on how the histogram bins were cut.",
"title": "Median Change Date"
},
"valid_observations_min": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Fewest valid observations any covered in-AOI pixel had. Saturates at 255, which is the widest a single 8-bit channel can count, so a reported 255 means 'at least 255'. Null when nothing was covered. A min of 0 or 1 means some pixels' dates rest on almost no data.",
"title": "Valid Observations Min"
},
"valid_observations_median": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Median valid-observation count over covered in-AOI pixels, with the same 255 saturation. This is the honest handle on per-pixel date uncertainty: masking removes observations unevenly, so a catalog-level cadence would understate the real gaps.",
"title": "Valid Observations Median"
},
"aoi_covered_fraction": {
"default": 0,
"description": "In-AOI cells at least one scene covered, over all in-AOI cells. Below 1.0 part of the AOI was never seen by any scene, so no number in this response describes it (and the map, when one is returned, shows it as 'no data').",
"title": "Aoi Covered Fraction",
"type": "number"
},
"aoi_grid_fraction": {
"default": 0,
"description": "In-AOI cells over all grid cells: how much of the rendered rectangle the AOI polygon actually occupies.",
"title": "Aoi Grid Fraction",
"type": "number"
},
"covered_days": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Exact number of distinct UTC days the fold will composite, after `months` and `max_cloud_cover` — the population ORBIT mosaicking hands the fold, which flattens same-day acquisitions. Present whenever the acquisitions were enumerated — because the catalog's match count was not low enough to cap the cost on its own, or because the catalog reported no match count at all; null when that cheap upper bound was already comfortable, in which case covered_days_upper_bound carries it instead.",
"title": "Covered Days"
},
"covered_days_upper_bound": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "The catalog match count used as an UPPER bound on covered days (one STAC item per acquisition, so a tiled AOI over-counts a single covered day). Present exactly when covered_days is null.",
"title": "Covered Days Upper Bound"
},
"catalog_features_matched": {
"default": 0,
"description": "Acquisitions in this query's window, always the best-observed figure: the catalog's own match count when that count alone settled the cost (covered_days null), and otherwise the distinct acquisitions this tool actually enumerated from the catalog's pages — which is what it reports both when the deployment gives no count at all and when the pages carried more than the count claimed. One STAC item per acquisition, so a tiled AOI counts a single covered day more than once — see covered_days.",
"title": "Catalog Features Matched",
"type": "integer"
},
"estimated_processing_units": {
"default": 0,
"description": "Cost estimated BEFORE the call from the provider's own formula (grid cells / 512² × bands/3 × mosaics), floored at the provider's 0.005 minimum, so a very small grid reports that floor rather than the raw product. Never null — the cost cap is enforced against it, so a request whose cost cannot be established is refused instead of folded. Expect it to sit NEAR the budget rather than to fall when you add a filter: while auto_coarsened is true, removing mosaics buys a finer grid instead of a smaller bill. See pu_estimate_is_upper_bound.",
"title": "Estimated Processing Units",
"type": "number"
},
"pu_estimate_is_upper_bound": {
"default": false,
"description": "True when the estimate was computed from the catalog match count rather than from enumerated covered days, in which case the real cost can only be lower. processing_units_spent is authoritative.",
"title": "Pu Estimate Is Upper Bound",
"type": "boolean"
},
"processing_units_spent": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "The provider's own figure for this call, from its response header. Authoritative where present; null when the header was absent.",
"title": "Processing Units Spent"
},
"effective_resolution_m": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Metres per grid cell actually used — the coarser of the two per-axis values — after the per-axis cap and any cost coarsening. This, not the index's native resolution, is the resolution of every date and area in this response.",
"title": "Effective Resolution M"
},
"grid_width": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Fold grid width in cells.",
"title": "Grid Width"
},
"grid_height": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Fold grid height in cells.",
"title": "Grid Height"
},
"auto_coarsened": {
"default": false,
"description": "True when the grid is coarser than the resolution this call TARGETED — the index's native resolution, or your resolution_m when you passed one — because the 1536-cell per-axis working cap bit, or the cost budget required it, or both. The dates, the areas and any outline that was produced are correspondingly coarser, and `note` says which limit bound and what the grid ended up being.",
"title": "Auto Coarsened",
"type": "boolean"
},
"changed_aoi_ref": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Handle for the outline of the changed area, usable as `aoi_ref` in this toolkit and in geo, valid for about 48 hours. OPTIONAL ON EVERY PATH, with the reason in `note`: no pixel changed, the outline was empty after clipping to the source AOI, it could not be reduced to a storable vertex budget, outlining failed unexpectedly before it got that far (reported as its own cause, not as a statement about your changed cells), the phase could not be started or ran out of wall clock, or the write failed or was abandoned in flight — in those last two cases it is a single attempt that can fail after the object landed, so whether anything reached the store is unknown and no handle is returned either way. Every other case is a definite nothing-written. changed_aoi_geojson stands in for it ONLY when an outline was actually produced and the store step is what failed (skipped or attempted-and-failed) and it fits the inline vertex budget; on the paths where no outline was produced at all there is nothing to return inline.",
"title": "Changed Aoi Ref"
},
"changed_aoi_area_km2": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Area of the NATIVE changed cells the outline was built from, measured the same way as changed_area_km2 (exact per-row spherical cell areas) — not the planimetric area inside the simplified outline, and not the area that outline encloses. It is always a SUBSET of changed_area_km2, and the only thing that removes cells is component filtering: connected components too small to keep on the working (possibly downsampled) grid are dropped, and the surviving cell set is intersected back with the native changed mask. So widening a thin feature to one working cell adds nothing here — that widening is a stated tolerance of the OUTLINE (changed_aoi_min_feature_width_m), never area — and clipping the outline to your AOI removes nothing here either, because the measurement is taken before the clip.",
"title": "Changed Aoi Area Km2"
},
"changed_aoi_vertex_count": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Coordinate positions in the stored/returned outline.",
"title": "Changed Aoi Vertex Count"
},
"changed_aoi_part_count": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Disjoint polygons in the outline.",
"title": "Changed Aoi Part Count"
},
"changed_aoi_min_feature_width_m": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Width of one working cell of the outline, in metres, at the AOI's mid-latitude: the tolerance by which a thin feature may have been widened, since a cell is kept if ANY of it changed (which preserves one-pixel-wide fronts that a majority rule would erase). Approximate by construction — it is a tolerance statement, not a measurement.",
"title": "Changed Aoi Min Feature Width M"
},
"changed_aoi_geojson": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "The outline inline, returned only when the vectorizer produced one AND it fits 2000 coordinate positions. It closes the workflow when the outline was built but could not be STORED — not on the paths where no outline exists (no pixel changed, nothing survived the speck drop and the clip, the shape could not reach the vertex budget, the vectorizer raised, or the mint phase was never started, timed out or was abandoned): there is nothing to return inline there, and `note` says so. NEVER present alongside changed_aoi_ref: AT MOST ONE of the two is returned, because the handle exists to keep bulk geometry out of the conversation — when a ref was minted this field is null and you resolve the outline through the ref. At most, not exactly: the paths listed above return neither, since there was no outline to store or to inline.",
"title": "Changed Aoi Geojson"
},
"note": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Everything the numbers above do not say on their own: coarsening, sparse or absent coverage, saturated observation counts, and the reason for every degraded field. Read it whenever image_url or changed_aoi_ref is null.",
"title": "Note"
}
},
"required": [
"success",
"index",
"collection",
"statistic"
],
"title": "TemporalMapResult",
"type": "object"
}