# Statistics of a raster per polygon

> Index of the TerraLab docs for agents: [llms.txt](https://terra-lab.ai/docs/llms.txt). Every docs page is also served as markdown: add .md to its URL.

A raster summarised for each polygon of a layer: population, land cover shares, or elevation and slope per district, parcel or catchment.

| | |
| --- | --- |
| REST | `POST https://mcp.terra-lab.ai/v1/tools/zonal_statistics` |
| MCP tool | `zonal_statistics` on `https://mcp.terra-lab.ai/mcp` |
| Group | Imagery and land |

## Request

```bash
curl -X POST https://mcp.terra-lab.ai/v1/tools/zonal_statistics \
  -H "Authorization: Bearer $TERRALAB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"dataset":"population","url":"https://mcp.terra-lab.ai/dl/geojson/TOKEN/divisions.geojson","name_field":"name"}'
```

```python
import os
import requests

response = requests.post(
    "https://mcp.terra-lab.ai/v1/tools/zonal_statistics",
    headers={"Authorization": f"Bearer {os.environ['TERRALAB_API_KEY']}"},
    json={
        "dataset": "population",
        "url": "https://mcp.terra-lab.ai/dl/geojson/TOKEN/divisions.geojson",
        "name_field": "name",
    },
    timeout=120,
)
response.raise_for_status()
print(response.json()["structuredContent"])
```

```javascript
const response = await fetch('https://mcp.terra-lab.ai/v1/tools/zonal_statistics', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TERRALAB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "dataset": "population",
    "url": "https://mcp.terra-lab.ai/dl/geojson/TOKEN/divisions.geojson",
    "name_field": "name"
  }),
});
const data = await response.json();
console.log(data.structuredContent);
```

Other valid request bodies:

```json
{"dataset":"land_cover","geojson":{"type":"Polygon","coordinates":[[[3.86,43.6],[3.9,43.6],[3.9,43.62],[3.86,43.62],[3.86,43.6]]]}}
```

## Parameters

A JSON object in the request body; in an MCP client, the arguments of the tool call.

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `dataset` | string | Yes | "population", "land_cover", "elevation", or a raster id from find_datasets such as "ghs_pop", "worldpop_1km", "esa_worldcover", "clc2018_cog", "ghs_built_h". Up to 80 characters. |
| `url` | string | No | The zones: a public http(s) link to polygons, or a TerraLab download link from an earlier answer. |
| `file` | object | No | The zones as a file uploaded in the conversation (GeoJSON, KML, KMZ, GPX, zipped Shapefile, CSV or WKT). |
| `file.download_url` | string | Yes |  |
| `file.file_id` | string | Yes |  |
| `file.file_name` | string | No |  |
| `file.mime_type` | string | No |  |
| `geojson` | object | No | The zones as a GeoJSON FeatureCollection, Feature or polygon in WGS 84 (longitude, latitude). |
| `name_field` | string | No | The attribute that names each zone in the rows. Left out: name, label or id when present, else the zones are numbered. Up to 80 characters. |
| `variables` | object | No | Values for the dataset's variables, as find_datasets lists them: {"YEAR": "2015"}, {"RETURN_PERIOD": "100"}. |

## Response

`structuredContent` holds the result, `content` the same result as text for a model, and `isError` is `true` when the tool refused the request or failed; then `structuredContent.code`, `message` and `hint` say what to change.

```json
{
  "structuredContent": {
    "sources": [{ "name": "...", "licence": "...", "url": "..." }],
    "downloads": [{ "label": "...", "format": "...", "url": "...", "name": "..." }]
  },
  "content": [{ "type": "text", "text": "..." }],
  "isError": false
}
```

Every result lists its `sources` with their licence. File results come as `downloads`: each link builds its file when opened.

## Errors

A missing key answers `401`, an empty balance `402`, too many calls `429`. A call that fails or finds nothing is not counted. See [Errors and rate limits](https://terra-lab.ai/docs/errors).

## Description for agents

The text the server gives an agent to decide when to call this tool:

> Use this when the user wants zonal statistics, a raster summarised for each polygon of a layer (per district, parcel, catchment, buffer or isochrone): "population of each commune", "land cover shares per parcel", "mean elevation and slope of each catchment", "flood depth per neighbourhood". Give the zones as url (a public link or a TerraLab download link, such as the divisions of extract_open_data or a buffer of process_geometry), file or geojson, and dataset: "population" (GHS-POP 2025, 100 m), "land_cover" (ESA WorldCover, 10 m), "elevation" (Copernicus DEM, with mean slope) or any raster id from find_datasets. Returns one row per zone (population and density; share and hectares per class; or min, max, mean, sum) and the zones with these as attributes (GeoPackage, GeoJSON, CSV), never the pixels. A pixel counts when its centre lies inside the zone, on the dataset's own grid. Limits: 5,000 zones, 16 million pixels over their extent (8 million for 64-bit data such as population); a refusal gives the numbers. Not for one area (estimate_population, extract_dataset with summary), a Sentinel-2 index per zone (get_satellite_index with zones_url) or counting features per polygon (process_geometry). Download links build their file when opened; no file is saved. The user opens the file in QGIS or any GIS: this server cannot load it into the user's own QGIS or any app on their computer. Never pass these links to open_gis_file. TerraLab reads the zones to answer and keeps no copy.


Source: https://terra-lab.ai/docs/api/zonal_statistics · Updated 2026-11-15
