The Data API allows anyone to access all public data stored in HESTIA, in a compacted (pivoted) format.
The API does not require creating an account.
You can test the API, and see all endpoints available, on the Swagger docs.
The API is used on the Data Explorer.
You can use the Data API to retrieve aggregated data, in the same way as the Data Explorer.
Every request for aggregated data should name the release it is asking about, with the X-Data-Version header set to that release's date. See Always Send a Release Date below for what happens if you leave it out.
Here are some usage examples:
$ curl -X 'GET' \
'https://api.hestia.earth/settings/dataReleases' \
-H 'accept: application/json' \
v1 release on 2025-05-01:$ curl -X 'GET' \
'https://data.hestia.earth/aggregated-nodes?offset=0&limit=50&sortBy=name&sortOrder=asc' \
-H 'accept: application/json' \
-H 'X-Data-Version: 2025-05-01'
Wheat, grain and Maize, grain, in France and United Kingdom:$ curl -X 'GET' \
'https://data.hestia.earth/aggregated-nodes?filter=regions%3DUnited%20Kingdom%7CFrance%3Bproducts%3DWheat%2C%20grain%7CMaize%2C%20grain&offset=0&limit=50&sortBy=name&sortOrder=asc' \
-H 'accept: application/json' \
-H 'X-Data-Version: 2025-05-01'
Note that the limit for a single request is 50 results. If the count in the response is greater than 50, you can paginate using the offset parameter. Example: limit=50&offset=100 will return the results from 100 to 150.
Send the release as an X-Data-Version request header, on every request, to both api.hestia.earth and data.hestia.earth. The value is the release date, as YYYY-MM-DD — not its version number, so 2025-05-01 and never v1.0:
$ curl -X 'GET' \
'https://api.hestia.earth/impactassessments/grapesFruit-spain-2010-2025-20250206' \
-H 'accept: application/json' \
-H 'X-Data-Version: 2025-05-01'
It is an ordinary header, so it goes wherever your client puts headers — headers={'X-Data-Version': '2025-05-01'} in Python requests, a headers entry in fetch. It applies to POST requests the same way:
$ curl -X 'POST' \
'https://api.hestia.earth/search' \
-H 'Content-Type: application/json' \
-H 'X-Data-Version: 2025-05-01' \
-d '{"query": {"match": {"@type": "ImpactAssessment"}}, "limit": 50}'
/settings/dataReleases lists every release, keyed by the date you send:
{
"2025-05-01": {"name": "v1.0", "enabled": true},
"2026-03-10": {"name": "v1.1", "enabled": true},
"2026-04-30": {"name": "v1.2", "enabled": true}
}
The newest key is not necessarily the one to send: a release is restricted for its first 182 days, and asking for one inside that window answers 401 unless you have Priority Data Access. So pick the newest key whose date is more than 182 days ago — v1.2 above was published on 2026-04-30 and only becomes public on 2026-10-29, which leaves v1.1 as the newest public release until then. Pin that date in your code and change it deliberately, rather than always taking the last key, so a published release does not silently change the numbers your analysis returns.
Leaving the header out does not mean "the current public release", and it does not mean the same thing on every endpoint:
/aggregated-nodes: with no header the API reads the preview — the forthcoming release. Reading aggregated data from the preview requires Priority Data Access, so the request answers 401 even when the same aggregation is freely available in a published release.POST /search and POST /count: with no header these fall back to the most recent release you are allowed to read, so they do return data. Which release that is depends on who is asking, so a query that does not pin the header is not reproducible — two people can run it and get different results.Data Releases explains what a release contains.
Each release is a frozen snapshot, and an aggregation id carries the date it was computed. The same product, country and period is recomputed in each release under a new id, so an id read out of one release does not resolve against another — you get a 404, which is easy to read as missing data.
grapesFruit-spain-2010-2025-20250206 belongs to the 2025-05-01 release, and only to that one:
$ curl -X 'GET' \
'https://api.hestia.earth/impactassessments/grapesFruit-spain-2010-2025-20250206' \
-H 'accept: application/json' \
-H 'X-Data-Version: 2025-05-01'
Ask a later release for the same id and it answers 404, because grapes in Spain over that period was recomputed there under a different id.
To follow one product and country across releases, resolve it through each release's own index rather than reusing an id. hestiaAggregatedData.json in the AWS Open Data bucket maps product to country to year to the aggregation id for that release, and there is one per release.
POST https://api.hestia.earth/search runs the Elasticsearch query you give it and adds nothing of its own. It searches one release — the one named by X-Data-Version, or the most recent one you can read if you leave it out. The index holds every aggregation, including ones that have not been validated for publication, so a query that filters only on aggregated returns aggregations you will not be able to fetch. Ask for validated ones as well:
$ curl -X 'POST' \
'https://api.hestia.earth/search' \
-H 'Content-Type: application/json' \
-d '{"query": {"bool": {"must": [
{"match": {"@type": "ImpactAssessment"}},
{"match": {"aggregated": true}},
{"match": {"aggregatedDataValidated": true}}
]}}, "limit": 50}'
aggregated and aggregatedDataValidated are indexed as text, so they come back in results as the strings "true" and "false" rather than as booleans — compare them as strings if you filter the response yourself.
The Data Explorer applies this filter to its own searches. A hand-written query that omits it returns many more aggregations than the Explorer shows, and the extra ones are the unpublished ones.
Alongside every array it indexes, the search index stores a count of it — emissionsResourceUseCount, productsCount, emissionsCount and so on. These are plain numbers at the top level of the document, which makes them the way to ask what a node holds. The arrays themselves are nested fields, so an exists query against one of them matches nothing.
Some Impact Assessments carry no emissions or resource use, because recalculation produced nothing to record. Filter them out with the count rather than finding them one at a time:
$ curl -X 'POST' \
'https://api.hestia.earth/search' \
-H 'Content-Type: application/json' \
-d '{"query": {"bool": {"must": [
{"match": {"@type": "ImpactAssessment"}},
{"range": {"emissionsResourceUseCount": {"gt": 0}}}
]}}, "limit": 50}'
Invert the range to list the ones that are empty.
There is no direct filter. products is a nested field and products.term.termType is searchable, so you can find Cycles carrying a crop product, but Elasticsearch cannot count nested matches inside a filter — and productsCount counts every product, so a single-crop Cycle that also records a cropResidue product looks the same as an intercrop. Retrieve the products with inner_hits and count the ones whose termType is crop yourself.
Wild-caught is a standardsLabels Term, recorded as a Practice on the Cycle. It is a label rather than a required field, so its absence does not mean farmed — only that nobody recorded it.
Two other signals narrow it. The Site's siteType is decisive in one direction: pond is aquaculture, while sea or ocean, river or stream and lake cover both wild capture and open-water farming. And a Cycle carrying an aquaculture production system among its Practices — Extensive system (aquaculture), Recirculating Aquaculture System and the others of termType system — is farmed.
Fetching an aggregation you are not allowed to read answers 401, not 404. It means "not published to you", not "you are not signed in", and there are two reasons for it:
X-Data-Version header at all.A 404 is the different case: the id genuinely does not exist in the release you asked for, usually because it belongs to another release.