The Chloris Platform
The Chloris Platform is a cloud-based SaaS platform which empowers decision-makers to gain access to reliable, cost-efficient, and repeatable measurements of carbon stocks stored in woody vegetation and its changes at the global scale. It gives access to our premium on-demand data product of annual above-ground carbon stock and change, and forest cover and change.
The site you are currently viewing documents the premium on-demand dataset, both for users of the web interface and for developers who wish to use the API in their own applications.
For more information, see the Chloris website or try it out now at app.chloris.earth.
How it works
Here is how our premium on-demand product works:
- You provide a polygon of interest (e.g. a forest concession, a protected area, or a country) and a time period of interest (e.g. 2010-2020).
- Optionally, you provide a control polygon (e.g. a neighboring area).
- We use our proprietary machine learning algorithms to analyze satellite imagery from several sources and produce annual maps of forest carbon stock, gains, and losses for your area of interest. This is available from 2000 (23+ years) at 30m resolution or from 2017 at 10m resolution (6+ years).
- We provide you with a report that includes maps of the results. The report for your site is available via API, and interactively at app.chloris.earth, where you can explore the results, download summary data, or print a PDF report. If your licensing includes it, you can also download the geospatial data outputs.
Chloris Platform: Quick Start Guide
To get started with using the Chloris Platform's web interface, visit account and organization overview. We step through how to create an account, create and view sites, and also introduce the suite of tools that allow you to analyze and download your data.
API documentation
Your organization may also access the Chloris Platform programmatically by using the Chloris Platform API. This is the API that powers the web interface, and select functionality is available for you to use in your own applications. For a detailed walk-through, see Getting Started. Alternatively. you can view the API reference. Keep up with all API changes by checking the Changelog.
Chloris Platform: Quick Start Guide
The Chloris Platform is a cloud-based SaaS platform which empowers decision-makers to gain access to reliable, cost-efficient, and repeatable measurements of carbon stocks stored in woody vegetation and its changes at the global scale. It gives access to two datasets: a) a new on-demand operational-scale data product of biomass stock, change and associated uncertainty, and b) a global freely available non-commercial coarser resolution biomass data product that Chloris released in early 2022 (also available through Microsoft's Planetary Computer).
This guide explains the platform's functionality, and walks through the process of uploading a site and the outcome of the analysis.
We officially support the latest versions of Chrome and Firefox. For other browsers, some features may not work as expected.
Creating an account
To access the Chloris data, you need to create an account. The global 5 km data is freely available for all users. For premium users, you will receive an invitation email with specific instructions to access your organization's high resolution biomass data.

If you have any problems related to your account, please contact us at help@chloris.earth.
Organization overview page
If your account already has sites uploaded, you will see them on your organization's overview page. You can choose sites directly on the map at the top of the page, or from the list below.
If you want more information about many sites, the "table" button will take you to a table view where you can sort, filter, and aggregate your sites.
Managing your account
Once your account is created, you may manage your account using the buttons on the top right of your screen.

The first icon allows you to invite users to your organization. The second allows you to manage your organization settings and to change user permissions within the organization. If you click on your name, you can see your own profile information, change units, documentation for the platform and API, and our terms of service and privacy policy. See Units for more information.
Managing users
Within your organization, there are different user access permissions:
- Viewers can access operational-scale data that has already been processed and explore our interactive graphs, maps, and more.
- Managers can submit new sites, manage organization preferences, and invite others to view the organization.
- The Owner can do everything above and promote viewers to manager, they are also included on all email notifications.
If you are an owner or manager and would like to change your users' permissions, you can do so in the "Organization Options" panel, which you can get to using the icon in the top right of the screen. Non-managers/owners cannot see this panel.
Units
You can change the units you would like to explore the data with by opening your preference in the top right of the page. If you have manager or owner's permissions, you can also choose the defaults units for anyone in your organization. Simply go to your "Organization Options" panel in the top right of the page to manage unit options.

Uploading a site
If you have a premium account and have manager or owner’s permissions you can request operational-scale biomass stock and change for your chosen area of interest. To do so, you can either upload a vector file containing a polygon with boundaries of your site of choice, or draw your site’s boundaries using our map tools. If you have not previously uploaded any sites, the options for uploading a new site are the first thing you will see after clicking on your organization. Otherwise, they are on the right side of your organization's home page.
To upload a site, click the "New site" button, which will take you to the site upload page.

Fill in the basic information about your site. You can specify the range of years you would like to have data available. Add tags about your site for easier navigation, sorting and summary analytics. Later, you can customize your tags and toggle the summary analytics option in the Manage Organization tab in the top right of the screen. When you are ready, click Next.
Uploading a file with your site’s boundaries
Upload a file containing the polygon with boundaries of your site. A site can be uploaded with the majority of vector-based spatial data formats, including ESRI shapefile, GeoJSON files, .gpkg, .kml, and more. Our recommended formats are shapefiles and GeoJSON. If you are uploading an ESRI shapefile, please select all the files (including files with extensions .shp, .shx and .dbf) or a zipped shapefile. After a successful upload, you will see your site on a map on the right-hand side. Please make sure the location of your site is correct and click Next.
If your site is not in the correct location, please try re-projecting the file to EPSG 4326 (WGS84) and uploading again. If you are still having problems, please contact us at help@chloris.earth.
Drawing your site’s boundaries by hand
If you don’t have the file containing your site’s boundaries, you can draw it yourself. First, zoom in to where your site is located and click the Polygon tool. Click on the map to draw a polygon, each click adds one vertex. Double click closes the polygon. If you’re unhappy with the shape you just drew you can change it by clicking and dragging the vertices or in the middle of an edge to add a new vertex. Once you’re satisfied with the shape, click Save on top of the map. You can add new polygons, or delete ones you don't want anymore. Once you're happy with the shape, click "Save", confirm the saved shape matches what you drew, then click "Next".

Uploading a control area
An (optional) control area is a secondary site to which your main area of interest will be compared. Adding a control area enables you to observe how the carbon dynamics within your project differ to its surroundings or an area of your choice. You can choose to upload another file with the control area, draw it yourself using our map tools, or auto-generate a buffered area around your site. The upload and drawing process is similar to uploading a site. If you would like to auto generate a buffered control area, click the Auto Generate button.

Site review
Once you upload your site and a control area, you get a chance to review your project and go back and edit if necessary. The summary at the bottom of the page tells you the total site area, which will be counted toward your account limit. When ready, click Submit.
Confirmation
Congratulations! You have just uploaded your first site. The results of the analysis should be available soon (as little as a few hours, but more often several business days). You can close the page and your new site will appear in your list of sites. It will show ‘Analysis in progress’ until the data is ready, and our team has reviewed it for quality assurance purposes. You will get an email notification when it is ready for you to view.
Viewing your sites
Clicking on your site from your organizations home page or the table view will take you to the report page for that site. The report page is divided into several subpages, starting with site summary statistics. You can switch between pages by clicking the arrows at the top of the page.

The first page gives a summary of the site’s carbon dynamics and control area, if provided. Here you can find the information about the biomass stock within your site at the beginning and the end of the time period, overall net, gains and losses.
Graphs
The second page displays carbon dynamics plots. As of 2023, the standard graphs show annual stock, annual change, the distribution of stock values, forest cover, forest cover change, and an annual loss allocation which differentiates losses due to deforestation, degradation, and other changes.

Each graph has a title in the top left, some options in the top right, and a legend at the bottom. Hovering your cursor over the "i" icon next to the title provides details of what the graph is showing. Clicking on the legend elements switches them on and off. Hovering your cursor the graph will reveal the exact values for each data point. Data shown in the plots is also available to download in tabular form by clicking the download button to the top right of each plot to get a .csv file, or by click the name of your site in the top left, then clicking "downloads".
Exploring the maps
The final page of the site report is the map view. The Chloris platform enables you to explore the location of stock density, change, and forest cover within your site.

You can change the background map (or "basemap") by clicking on the last button in the top-left of the map. By clicking on the legend in the top-right you can turn the data layer on and off. Hovering your mouse over the legend and scrolling allows you to change the opacity of the data layer. For annual datasets, the time slider at the bottom enables you to explore the data for each year of the time period. The "home" button takes you back to your site if you get lost while exploring. You can change layers either by clicking the buttons in the top left of the map or using the "layers" menu in the bottom left. Clicking the play button next to the year slider will animate the map through the years (note that requires a fast internet connection).
If you find a point of interest that you would like to save for later or share with someone else, you can copy the permalink from your browser's URL bar. This will take you back to the same view when you paste it into your browser.
The format of the URL when viewing the map page is
.../map/{layer}/{longitude}/{latitude}/{zoom}/{year} where the layer is one of stock, change, or forest, the longitude and latitude are the coordinates of the center of the map, the zoom is the zoom level of the map (between 0 and 20), and the year is the year of the data shown on the map.
Exploring forest cover and change
The Chloris platform also enables you to explore forest cover and change within your site in both charts and maps. Forest cover classifies the land into two categories (forest, and non-forest) based on a dynamic threshold of biomass. Non-forest therefore includes impermanent water.
Collections
This feature is in beta release
Sites can be grouped together into a Collection. Collections can also contain other collections.
Collections are saved to your organization. They offer summary statistics and the ability to explore all member sites at once. Common use cases are to group related sites or represent a very sparse multipolygon by its parts.
Create and explore a collection
To create a collection, click the "Add collection" button on the organization overview page. Search for existing sites and collections by name and click to add. Once saved, view the collection at the top of the site list on the overview page, denoted with a folder icon.

Each collection has an overview page and can be further explored and compared just as you can a single site. There are additional summary charts, and access to the familiar statistics, maps, and visualizations tabs. Access this view by clicking the "View Report" button.



Editing a collection
To edit an existing collection, view the collection to which you want to add sites and click the “Edit” button — you’ll find this option between the Search and Table buttons. Else, use the edit from the site dropdown. Be sure to save your changes.


Beta release notes
Collections are immediately available in the UI when created or edited. However, there may be a slight delay (a couple minutes) before collection summary statistics are updated as collection stats need to be recalculated by our systems. They may not be reflected in the UI until you refresh your browser.
You can add existing collections to another collection to a reasonable extent. The Platform will reject infinite nesting.
Email us at help@chloris.earth if you run into any issues and need assistence. We especially welcome your feedback while the feature is in beta.
Download your Data
Look out for this button across the application: This button indicates that data can be downloaded as a CSV. You can find this button on the site home page, graph, and table views.
This button indicates you can download the data in CSV format. You can find this button on the site home page, graph view, and table views.
Site Downloads
Another way you can download the data shown in the graphs and maps is by clicking the name of your site in the top left, then clicking "downloads". Tabular products are available for all sites for all customers. Geospatial products are subject to additional licensing terms and therefore may not be available for all the sites you have access to. If you want to download data for many sites, see bulk downloading.

Bulk downloading
If you want to download data for many sites at once, you can use the bulk download tool. To access it, click the "Downloads" button on the right of the organization home page.

Select one or more sites from the first list, optionally change the selected products and years of interest, then click "Begin download". Tabular products are available for all sites for all customers. Geospatial products are subject to additional licensing terms and therefore may not be available for all the sites you have access to. All requested files will be downloaded if available. The bulk download interface does not show real-time availability for each selected site, so the final downloads may differ from your selections. While your download is processing, you can continue to use the platform, but navigating away or closing your browser will interrupt the download. For best results, we recommend using the Chrome browser when using this feature.
If you only want to download data for a single site, or to see exactly what data is available before downloading, you can do so from the site report page. See site downloads for more information.
Additional Site Tools
Several additional tools are available when viewing a site report by clicking the name of your site in the top left of the page.

Print site report
You can export results as a printable PDF report by clicking the name of your site in the top left, then clicking "print".

Comparing sites
You can compare your site to other sites in your organization by clicking the name of your site in the top left, then clicking "compare". This will open a search dialog for you to select a second site to compare to.
Below are some examples of what it looks like when you compare two sites. Note that the first site always appears on the left, and the second on the right.

On-the-fly site aggregation
In some cases, you might be interested in summary values for your sites - you can view them in the table by selecting multiple sites while holding the Ctrl button (⌘ on Mac) on your keyboard or by holding shift to select a range of sites. The summary values appear at the top, and clicking "view report" will take you to a report page with more details. This can be slow if you are trying to aggregate more than 50 sites, so as an alternative you may tag all the sites you are interested in using a single tag (such as "project1") and then view the summary values for that tag.

Conclusion
We hope this guide helped you to navigate our application, if you have any questions please feel free to reach out and email us at help@chloris.earth.
Getting started with the Chloris Platform API
The Chloris Platform API is a RESTful API that allows you to interact with the Chloris Platform. It is designed to be used by both humans and machines, but if you are a human you may find it easier to use the Chloris Platform web interface. Note that the API is only available for use with premium organizations, please contact us if you are interested in using the API.
Unless otherwise stated, all requests and responses are in JSON format.
Examples in this documentation show how to interact with the API using both the chloris-app-sdk in Python (recommended) and using the REST API directly in Javascript (using node-fetch).
Getting a test account
Upon request, we can provide you with a test account separate from your main production account to interact with the API. Sites submitted to the test account do not count against your production account's quota and are immediately returned with a sample dataset for testing purposes. Please contact us at help@chloris.earth to request a test account.
Authentication
The Chloris Platform API uses OAuth 2.0 provided by AWS Cognito for authentication. This means that you will need to register an account with the Chloris Platform and then use your Chloris Platform username (email) and password to obtain your API credentials. We provide both a refresh_token (valid for up to 3 years) and an id_token (valid for 3 hours). The refresh_token can then be used to obtain an id_token (via the Oauth2.0 client credentials grant), which can then be used to authenticate all requests to the API. It is best practice to store your credentials securely and not expose them to the public. If your credentials are compromised, you can change your password to revoke them.
Obtaining your credentials
You can view your credentials at any time by logging into the Chloris Platform, opening your profile settings, and clicking "api credentials" at the bottom of the panel.

You can use the id_token directly for authentication, but it is recommended to use the refresh_token to obtain a new id_token when the old one expires. The refresh_token is also useful for long-running scripts or services.
Getting started with chloris-app-sdk (Python)
To get started, you'll need to install the chloris-app-sdk package:
pip install chloris_app_sdk
Then instantiate the client with your credentials. To confirm it is working you can call the list_active_sites function to list the sites in your organization.
from chloris_app_sdk import ChlorisAppClient
# create the client
client = ChlorisAppClient(
organization_id="your-organization-id",
refresh_token="your-refresh-token", # env: CHLORIS_REFRESH_TOKEN
# or
# id_token="your-id-token" # env: CHLORIS_ID_TOKEN
)
reporting_units = client.list_active_sites()
The result is a list of python dicts like this:
[
{
organizationId: "your-organization-id",
reportingUnitId: "123e4567-e89b-12d3-a456-426614174000",
label: "Site 1",
description: "Site 1 description",
createdAt: "2021-01-01T00:00:00Z",
updatedAt: "2021-01-01T00:00:00Z",
tags: ["ARR"],
// stats...
},
// other sites...
]
That's it! If you'd like to submit sites to the Chloris Platform, see Submitting sites.
See the Python API reference for the full list of functions and classes available in the SDK.
Getting started with the REST API (JavaScript)
Below are a series of examples that will walk you through the process of listing the sites in your organization. You can run these examples in a node.js environment by creating a file with the .mjs extension and running it with node <filename>.mjs.
See the API Reference for more details on the API endpoints.
Listing sites
Now that you have authenticated successfully, you can interact with the API. We'll try this out by listing the sites in your organization.
However, first we need to mention something about the Chloris Platform data model: Your organization can contain multiple sites. Individual sites can be organized into collections. Collections are then aggregated versions of the sites they contain. For example, you might have a collection sites in a single project, or a collection for each region. You might even combine collections into collections. So, internally we treat sites and collections as the same thing, called "reporting units". Additionally, when you apply tags to a site, their aggregated results are also available as reporting units. So, in order to list sites, we'll actually be listing all reporting units.
To do this, we'll use the POST https://app.chloris.earth/api/reportingUnit/ endpoint which returns a list of sites in your organization.
// listReportingUnits.mjs
import fetch from "node-fetch";
const idToken = "your-id-token";
const organizationId = "your-organization-id";
async function listReportingUnits() {
const url = "https://app.chloris.earth/api/reportingUnit/";
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + idToken,
},
body: JSON.stringify({
organizationId: organizationId,
}),
});
const json = await response.json();
return json?.reportingUnits || [];
}
const reportingUnits = await listReportingUnits();
The response will look something like this (note we're using JSON5 for readability, but the response is actually normal JSON):
{
reportingUnits: [
{
organizationId: "your-organization-id",
reportingUnitId: "123e4567-e89b-12d3-a456-426614174000",
label: "Site 1",
description: "Site 1 description",
createdAt: "2021-01-01T00:00:00Z",
updatedAt: "2021-01-01T00:00:00Z",
tags: ["ARR"],
// stats...
},
{
organizationId: "your-organization-id",
reportingUnitId: "123e4567-e89b-12d3-a456-426614174001",
label: "Site 2",
description: "Site 2 description",
createdAt: "2021-01-01T00:00:00Z",
updatedAt: "2021-01-01T00:00:00Z",
tags: ["ARR", "ProjectA"],
// stats...
},
{
organizationId: "your-organization-id",
reportingUnitId: "ARR",
branchId: "ARR",
label: "Site 2",
description: "Site 2 description",
createdAt: "2021-01-01T00:00:00Z",
updatedAt: "2021-01-01T00:00:00Z",
},
],
nextToken: "asdf123",
}
Now, this is fine if you only have a few reporting units, but at some point the results from this endpoint will be paginated. To get the next page of results, you can use the nextToken field from the response to make another request. To get all reporting units, you can use the listReportingUnits function like this:
/// ...
async function listReportingUnits() {
const url = "https://app.chloris.earth/api/reportingUnit/";
// this will be used to store the reporting units as they are retrieved
let reportingUnits = [];
// this will be used to store the nextToken after the first page of results
let nextToken = {};
while (true) {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + idToken,
},
body: JSON.stringify({
organizationId: organizationId,
...nextToken,
}),
});
const json = await response.json();
// add the reporting units from this page to the list
reportingUnits = reportingUnits.concat(json?.reportingUnits || []);
// if there is a nextToken, store it and repeat the loop, otherwise break out of the loop
if (json?.nextToken) {
nextToken = { nextToken: json.nextToken };
} else {
break;
}
}
return reportingUnits;
}
const reportingUnits = await listReportingUnits();
As mentioned in the data model, reporting unit entries can be sites, collections, or tags. To filter for just active sites, we have to perform a couple additional steps after retrieving the list of reporting units.
/// ...
async function listReportingUnits() {
// ...
return reportingUnits;
}
function filterActiveSites(reportingUnits) {
// Remove aggregated entries from collections and tags (which have the key `branchId`),
// control sites (which have `hidden: true`), and sites with now > deletedAt
const now = new Date().toISOString(); // e.g. 2021-01-01T00:00:00Z
return reportingUnits.filter((reportingUnit) => !reportingUnit?.branchId && !reportingUnit?.hidden && (!reportingUnit?.deletedAt || reportingUnit.deletedAt < now));
}
const reportingUnits = await listReportingUnits();
const activeSites = filterActiveSites(reportingUnits);
Searching by label
If you only want reporting units whose label contains a particular substring, add a searchLabel field to the request body. The match is a case-sensitive substring on the label field, and the response is sorted by analysisCompletedAt descending (pending sites are returned at the bottom of the list).
// searchReportingUnits.mjs
import fetch from "node-fetch";
const idToken = "your-id-token";
const organizationId = "your-organization-id";
async function searchReportingUnits(label) {
const url = "https://app.chloris.earth/api/reportingUnit/";
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + idToken,
},
body: JSON.stringify({
organizationId: organizationId,
searchLabel: label,
}),
});
const json = await response.json();
return json?.reportingUnits || [];
}
const matchingReportingUnits = await searchReportingUnits("Restoration");
For large organizations, the server scans up to limit * maxPages items per request before returning whichever match the filter. limit defaults to 1000 (capped at 1000) and maxPages defaults to 5 (capped at 50). If the matches you're looking for are spread across more items than fit in a single request, follow the nextToken from the response and pass it in your next request — the same pagination pattern as for the unfiltered list above.
async function searchReportingUnits(label) {
const url = "https://app.chloris.earth/api/reportingUnit/";
let reportingUnits = [];
let nextToken = {};
while (true) {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + idToken,
},
body: JSON.stringify({
organizationId: organizationId,
searchLabel: label,
...nextToken,
}),
});
const json = await response.json();
reportingUnits = reportingUnits.concat(json?.reportingUnits || []);
if (json?.nextToken) {
nextToken = { nextToken: json.nextToken };
} else {
break;
}
}
return reportingUnits;
}
Note that searchLabel cannot be combined with drafts or historical.
The Chloris Platform Data Model
The Chloris Platform data model is organized around a handful of key concepts:
- Everything belongs to an organization
- An organization can contain multiple sites
- Individual sites are defined by geospatial (multi)polygon boundaries
- Sites can be organized using one or more tags
- Sites and aggregated tag summaries are both referred to as "reporting units"
- A site may have a single "control reporting unit" for display purposes
Conceptual entity relationship map
Shown here are the things that make up the data model, and how they relate to each other. Each node represents an entity. Arrows indicate the direction of the relationship. For example, a site belongs to an organization. Solid lines indicate "materialized" elements which are queryable and can be returned in a response. Dashed lines indicate elements which exist conceptually, but require additional processing to materialize. For example, several sites can be tagged "ARR", but the tag is materialized by aggregating the results of all unique sites with the tag and storing the results as a new reporting unit entry with the branchId set to the tag name.
See also Output datasets.
The reporting unit
When requesting reporting units from the API, the reporting unit status and a few key summary statistics. This initial result is referred to the "reporting unit entry".
Each reporting unit may also have the following additional data files associated with it, each located under the dataPath attribute*:
- The boundary file, which the reporting unit entry refers to by
boundaryPath* - The stats file, which contains additional summary statistics, both for the entire period of interest, and annually. The stats file is always located at
:dataPath/stats.json - The layers config file, which contains the configuration for the layers displayed in the Chloris Platform web interface. The layers config file is always located at
:dataPath/layers.json. See Pre-computed webtiles. - The downloads index file, which contains a list of all the downloadable files associated with the reporting unit. The downloads index file is always located at
:dataPath/downloads.json. See Geotiff raster file downloads.
* dataPath and boundaryPath may refer to s3://chloris-app-data/, which is a private bucket. Replace s3://chloris-app-data/ with https://app.chloris.earth/ to download the files.
Submitting sites
For submitting sites through the web interface, see the user guide. This page is for submitting sites via the API.
Using chloris-app-sdk (Python)
You can submit a site in the chloris-app-sdk by using the client.submit_site function. This function will upload the boundary, wait for it to be normalized, and then submit the site for processing.
Here is an example which will upload a new site from a geoJSON file:
from chloris_app_sdk import ChlorisAppClient
# create the client
client = ChlorisAppClient(
organization_id="your-organization-id",
refresh_token="your-refresh-token", # env: CHLORIS_REFRESH_TOKEN
# or
# id_token="your-id-token" # env: CHLORIS_ID_TOKEN
)
# submit the site
reporting_unit = client.submit_site(
label="Site 1",
description="Site 1 description",
tags=["ARR"],
boundary_path="path/to/boundary.geojson",
# or boundary_path="https://example.com/path/to/boundary.geojson",
# optional control site
control_boundary_path="path/to/control.geojson",
# or control_boundary_path="https://example.com/path/to/control.geojson",
# optionally disable email notifications for this site, recommended if using webhooks
# notify=False,
)
# the new reporting unit entry
print(reporting_unit)
# {
# organizationId: "...",
# reportingUnitId: "...",
# createdAt: "...", # now, ISO 8601
# updatedAt: "...", # now, ISO 8601
# controlReportingUnitId: "..."
# }
If you are using webhooks, you will be notified when the site is ready. Otherwise, you can poll the API to check the status of the site:
import time
# ...
reporting_unit_id = reporting_unit["reportingUnitId"]
i = 0
while True:
# get the reporting unit and check if it is ready
reporting_unit = client.get_reporting_unit(reporting_unit_id)
if reporting_unit.get("qualityControlledAt"):
break
# sites are ready in hours to days, so we recommend waiting an initial 15 minutes, then exponentially backing off. This is also why we recommend using webhooks.
time.sleep((60 * 15) * (2 ** i))
i += 1
Here is an example which submits a local shapefile:
from chloris_app_sdk import ChlorisAppClient
# create the client
client = ChlorisAppClient(
organization_id="your-organization-id",
refresh_token="your-refresh-token", # env: CHLORIS_REFRESH_TOKEN
# or
# id_token="your-id-token" # env: CHLORIS_ID_TOKEN
)
# submit the site
site = client.submit_site(
label="Site 1",
description="Site 1 description",
tags=["ARR"],
boundary_path="path/to/boundary.shp",
period_change_start_year=2000,
period_change_end_year=2023, # or None for the most recent year
resolution=30, # or 10 for 10m resolution analysis
forest_baseline_year=2000
)
Using the REST API (JavaScript)
Submitting sites via the API is a two-step process. First, you must upload the boundary (and optionally a control boundary) for normalization. Then, you can submit the site using the normalized boundary(s).
Here is an example of submitting a site using the aws-sdk in JavaScript:
// submitSite.mjs
// 1. use the aws-sdk to get temporary sts credentials
// 2. upload the file to S3
// 3. request it be normalized
// 4. wait for the normalized file to be available,
// 5. submit the reporting unit to the API
// node-specific imports
import fetch from "node-fetch";
import fs from "fs";
import crypto from "crypto";
import { extname, resolve } from "path";
// aws-sdk imports
// install via `npm install @aws-sdk/client-cognito-identity @aws-sdk/credential-provider-cognito-identity @aws-sdk/client-s3 @aws-sdk/credential-provider-cognito-identity`
import { CognitoIdentityClient } from "@aws-sdk/client-cognito-identity";
import { HeadObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { CognitoIdentityProviderClient, InitiateAuthCommand } from "@aws-sdk/client-cognito-identity-provider";
import { fromCognitoIdentityPool } from "@aws-sdk/credential-provider-cognito-identity";
let idToken;
idToken = "your-id-token";
// or
// const refreshToken = "your-refresh-token";
const organizationId = "your-organization-id";
const filePath = resolve("/path/to/local/file.geojson");
const controlFilePath = resolve("/path/to/local/control-file.geojson");
const fileExt = extname(filePath);
const uploadId = crypto.randomUUID();
// Get info about the AWS resources used by the Chloris API
const apiInfo = await (
await fetch("https://app.chloris.earth/api/info", {
method: "GET",
})
).json();
// Create a CognitoIdentityClient
const cognitoClient = new CognitoIdentityClient({
region: apiInfo.awsRegion,
credentials: fromCognitoIdentityPool({
identityPoolId: apiInfo.awsCognitoIdentityPoolId,
logins: {
[`cognito-idp.${apiInfo.awsRegion}.amazonaws.com/${apiInfo.awsUserPoolId}`]: idToken,
},
}),
});
// get temporary credentials
const credentials = await cognitoClient.config.credentials();
const identityId = credentials.identityId;
const uploadKey = `private/${identityId}/apiUploads/${uploadId}${fileExt}`;
const boundaryKey = `protected/${identityId}/uploads/${uploadId}.geojson`;
const boundaryPath = `s3://${apiInfo.awsUserFilesS3Bucket}/${boundaryKey}`;
// Create S3 client with Cognito credentials
const s3 = new S3Client({
region: apiInfo.awsRegion,
credentials: credentials,
});
// Upload the boundary
const s3Upload = new PutObjectCommand({
Bucket: apiInfo.awsUserFilesS3Bucket,
Key: uploadKey,
Body: fs.createReadStream(filePath),
});
await s3.send(s3Upload);
// request the boundary be normalized
const boundaryResponse = await (
await fetch("https://app.chloris.earth/api/boundary", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${idToken}`,
},
body: JSON.stringify({
organizationId,
uploadId,
uploadPath: `s3://${apiInfo.awsUserFilesS3Bucket}/${uploadKey}`,
}),
})
).json();
if (boundaryResponse?.message) {
throw new Error("Failed to submit boundary for normalization: " + boundaryResponse.message);
}
console.log("boundaryResponse: ", boundaryResponse);
await wait_for_boundary_normalization(apiInfo.awsUserFilesS3Bucket, boundaryKey);
// optionally download the normalized boundary to inspect it
// const normalizedBoundary = await s3.send(new GetObjectCommand({
// Bucket: apiInfo.awsUserFilesS3Bucket,
// Key: boundaryKey,
// }));
// const normalizedBoundaryData = await streamToBuffer(normalizedBoundary.Body);
// begin optional control boundary upload
const controlFileExt = extname(controlFilePath);
// generate a separate id for the control boundary upload
const controlUploadId = crypto.randomUUID();
const controlUploadKey = `private/${identityId}/apiUploads/${controlUploadId}${controlFileExt}`;
const controlBoundaryKey = `protected/${identityId}/uploads/${controlUploadId}.geojson`;
const controlBoundaryPath = `s3://${apiInfo.awsUserFilesS3Bucket}/${controlBoundaryKey}`;
// Upload the control boundary
const controlS3Upload = new PutObjectCommand({
Bucket: apiInfo.awsUserFilesS3Bucket,
Key: controlUploadKey,
Body: fs.createReadStream(controlFilePath),
});
await s3.send(controlS3Upload);
// request the control boundary be normalized
const controlBoundaryResponse = await (
await fetch("https://app.chloris.earth/api/boundary", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${idToken}`,
},
body: JSON.stringify({
organizationId,
uploadId: controlUploadId,
uploadPath: `s3://${apiInfo.awsUserFilesS3Bucket}/${controlUploadKey}`,
excludeGeojsonPath: boundaryPath,
}),
})
).json();
if (controlBoundaryResponse?.message) {
throw new Error("Failed to submit control boundary for normalization: " + controlBoundaryResponse.message);
}
await wait_for_boundary_normalization(apiInfo.awsUserFilesS3Bucket, controlBoundaryKey);
// end optional control boundary upload
// Submit the reporting unit
const reportingUnit = await (
await fetch("https://app.chloris.earth/api/reportingUnit", {
method: "PUT",
headers: await get_headers(),
body: JSON.stringify({
organizationId: organizationId,
boundaryPath: boundaryPath,
controlBoundaryPath: controlBoundaryPath,
// dryrun: true, // may be used for testing, deletedAt will be set to 3 hours in the future
}),
})
).json();
if (reportingUnit?.message) {
throw new Error("Failed to submit reporting unit: " + reportingUnit.message);
}
console.log(`Reporting unit submitted successfully: ${reportingUnit.reportingUnitId}`);
/// Helper functions ///
/**
* Get the headers for the API requests, refreshing the idToken if necessary
*
* @returns {Promise<{}>}
*/
async function get_headers() {
// check that the idToken JWT is not expired
const idTokenDecoded = JSON.parse(Buffer.from(idToken.split(".")[1], "base64").toString());
if (idTokenDecoded.exp * 1000 < Date.now()) {
idToken = await refreshTokens(refreshToken, apiInfo);
}
return {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${idToken}`,
};
}
/**
* Wait for the boundary to be normalized (long poll S3 for the boundary to be processed)
* @param {string} bucket
* @param {string} boundaryKey
*
* @returns {Promise<void>}
*/
async function wait_for_boundary_normalization(bucket, boundaryKey) {
// 15 minute timeout with exponential backoff (note that most boundaries will be normalized in a few minutes)
console.log("waiting for normalization: ", boundaryKey);
const timeout = Date.now() + 15 * 60 * 1000;
let i = 0;
while (true) {
try {
const headObjectCommand = new HeadObjectCommand({
Bucket: bucket,
Key: boundaryKey,
});
const headResult = await s3.send(headObjectCommand);
if (headResult?.Metadata?.error) {
throw new Error(headResult.Metadata.error);
}
break;
} catch (e) {
if (e.name !== "NotFound") {
throw e;
}
}
if (Date.now() > timeout) {
throw new Error("Timeout waiting for boundary to be normalized");
}
const delay = 5 * Math.pow(1.5, i);
i++;
await new Promise((resolve) => setTimeout(resolve, delay * 1000));
}
}
/**
* Exchange a refresh token for an idToken
*
* @returns {Promise<string>}
*/
async function refreshTokens() {
if (typeof refreshToken === "undefined") {
throw new Error("idToken is expired and no refresh token is available");
}
try {
const client = new CognitoIdentityProviderClient({
region: apiInfo.awsRegion,
});
const command = new InitiateAuthCommand({
AuthFlow: "REFRESH_TOKEN_AUTH",
ClientId: apiInfo.awsUserPoolWebClientId,
AuthParameters: {
REFRESH_TOKEN: refreshToken,
},
});
const response = await client.send(command);
// Extract and return the ID token from the response
return response.AuthenticationResult?.IdToken;
} catch (error) {
console.error("Error refreshing tokens:", error);
throw error; // Rethrow the error for handling by the caller
}
}
Duplicate boundaries
A boundary can only be live once per organization at a given resolution. PUT /reportingUnit compares the new
site's label, resolution and normalized boundary area against the organization's existing sites (deleted sites
excluded) and returns 409 Conflict when it finds a match, naming the existing site in the message. The same
check runs when a site is approved, so a second copy of an already approved site cannot be approved by mistake.
Submitting the same boundary at both 10 m and 30 m is fine: the two resolutions are separate deliverables. To
replace a boundary, delete the existing site first or ask an administrator, who can bypass the check with
allowDuplicate: true.
Note for clients that retry: a request that is slow to respond has usually still succeeded. Retrying a submission
after a timeout used to create a second copy; it now returns 409, which a retry loop should treat as success.
Upload retention
Boundary uploads are working files, not storage. Two cleanup jobs run against the user files bucket:
- The raw file you upload under
private/{identityId}/apiUploads/(orprivate/{identityId}/uploads/from the web app) is deleted 30 days after upload. It is only read once, when the boundary is normalized. - The normalized boundary under
protected/{identityId}/uploads/is deleted by a weekly job once no site or site version references it, provided the upload itself is older than 30 days. A boundary referenced by a site is kept for as long as the site references it. When a site's analysis completes, the platform keeps its own copy of the boundary and the site stops referencing the upload, so the upload may be removed on the next weekly run regardless of how long ago the site completed.
Submit the site within 30 days of normalizing its boundary, or upload and normalize the boundary again. Do not treat the upload location as storage: keep your own copy of any boundary you may need later. Sites created with dryrun: true do not keep their upload referenced, so their boundaries are removed by the second rule.
Supported file types
When uploading boundaries, GeoJSON is recommended. However, the following file types are also supported:
- GeoJSON (.geojson)
- Zipped Shapefile (.shp.zip)*
- Multi-file Shapefile (.shp, .shx, .dbf, .prj, .cpg)
- GeoPackage (.gpkg)**
- FlatGeobuf (.fgb)
- KML (.kml)***
* Zipped shapefiles may have only contain one shapefile.
** Only the first layer of the GeoPackage will be used.
*** KML files are not recommended, as they often contain invalid geometries (such as unclosed line strings when you would expect a polygon). We recommend using QGIS to convert KML files to GeoJSON before uploading.
When submitting sites to the platform, we recommend using EPSG 4326 (WGS84) as the coordinate reference system (CRS) for your boundary. However, our system can support most CRSs, including UTM zones. Incorrect outputs due to projection issues are solely your responsibility.
Statistical data
This section describes the statistical data that is generated by the Chloris Platform and how to access it.
Units of measurement
The Chloris Platform internally uses the following SI units for most measurements:
- Length: meters (m)
- Area: square kilometers (km²)
- Mass: tonnes (t)
And tonnes / hectare (t/ha) for biomass stock density.
Generally, we recommend rounding statistical data to three significant digits for display purposes.
Common conversions
- 1 km² = 100 ha
- 1 t biomass = 0.5 * (44 / 12) t CO₂e (may vary depending on local biomass composition)
Accessing statistical data
Statistical data produced for each reporting unit is available at :dataPath/stats.json, where dataPath is specified on the reporting unit entry. Some stats are copied to the reporting unit entry itself (such as start/end stock and net change), but where they differ, the stats.json file should be considered the authoritative source.
Values at the root level of the stats.json file are calculated for the entire reporting unit, excluding areas with no data such as permanent water.
The following additional nested statistical product datasets are available for most sites:
forest: Areas which are forest in the selected year.non_forest: Areas which are not forest in the selected year (e.g. impervious surfaces, low biomass crops).forest_area_gain: Areas which are forest in the given year, but not the previous year.forest_area_loss: Areas which are not forest in the given year, but was forest in the previous year.forest_area_degradation: Areas which remained forest in the given year, but experienced statistically significant losses in biomass.forest_degradation: Losses allocated to degradation.forest_biomass_loss: Losses allocated to loss of forest area. Gradual forest loss may be recognized up to 3 years before and 1 year after a forest area loss event.
The contents of the stats.json file follow this schema:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Reporting unit stats created by the Chloris platform. Note that each reporting unit may represent a single site or multiple sites, each composed of one or more polygonal areas.",
"type": "object",
"properties": {
"organizationId": {
"type": "string",
"description": "The organization id of the reporting unit."
},
"reportingUnitId": {
"type": "string",
"description": "The reporting unit id, may be a tag name or a UUID7."
},
"branchId": {
"type": "string",
"description": "If an aggregate branch (i.e. a tag or collection), this is the branch ID",
"example": "12341234-1234-1234-1234-123412341234"
},
"resolution": {
"type": "integer",
"description": "The nominal length of one edge of each pixel in the reporting unit in meters. Typically 10 or 30, which are 100 m2 and 900 m2 pixels, respectively. Note that actual pixel sizes in rasters may vary slightly based on underlying imagery. If the reporting unit is a branch, the maximum value (largest area pixels) of the sites in the branch is used."
},
"count": {
"type": "integer",
"description": "The number of sites in the branch"
},
"areaKm2": {
"type": "number",
"description": "The area of the reporting unit(s) in square kilometers."
},
"reportingUnitIds": {
"type": "array",
"description": "The reporting unit ids of all sites in the branch."
},
"areaKm2DistributionBinWidth": {
"type": "number",
"description": "The bin width for the area distribution in square kilometers."
},
"areaKm2DistributionValues": {
"type": "array",
"description": "The frequency counts for the area distribution. For example, given a bin width of 10, and bin values of [1,2,3], there is 1 site with an area between 0-10 km2, 2 sites with an area between 10-20 km2, and 3 sites with an area between 20-30 km2."
},
"areaKm2Mean": {
"type": "number",
"description": "The mean area of sites in square kilometers."
},
"periodChangeStartYear": {
"type": "integer",
"description": "The start of the period of interest",
"example": 2000
},
"periodChangeEndYear": {
"type": "integer",
"description": "The end of the period of interest (inclusive)",
"example": 2023
},
"periodChangeStartYearStock": {
"type": "number",
"description": "The stock value for the start of the period of interest"
},
"periodChangeStartYearStockError": {
"type": "number",
"description": "The stock error for the start of the period of interest"
},
"periodChangeEndYearStock": {
"type": "number",
"description": "The stock value for the end of the period of interest"
},
"periodChangeEndYearStockError": {
"type": "number",
"description": "The stock error for the end of the period of interest"
},
"periodChange": {
"type": "number",
"description": "The net change in stock over the period of interest"
},
"periodChangeError": {
"type": "number",
"description": "The error in the net change in stock over the period of interest"
},
"periodChangeGain": {
"type": "number",
"description": "The gain in stock over the period of interest"
},
"periodChangeGainError": {
"type": "number",
"description": "The error in the gain in stock over the period of interest"
},
"periodChangeLoss": {
"type": "number",
"description": "The loss in stock over the period of interest"
},
"periodChangeLossError": {
"type": "number",
"description": "The error in the loss in stock over the period of interest"
},
"periodChangeCarryover": {
"type": "number",
"description": "The carryover net change in stock from sites which ended prior to the periodChangeEndYear (tonnes)"
},
"periodChangeCarryoverError": {
"type": "number",
"description": "The uncertainty (based on a 95% CI) in the carryover net change in stock."
},
"periodChangeCarryoverGain": {
"type": "number",
"description": "The carryover gain in stock from sites which ended prior to the periodChangeEndYear (tonnes)"
},
"periodChangeCarryoverGainError": {
"type": "number",
"description": "The uncertainty (based on a 95% CI) in the carryover gain in stock."
},
"periodChangeCarryoverLoss": {
"type": "number",
"description": "The carryover loss in stock from sites which ended prior to the periodChangeEndYear (tonnes)"
},
"periodChangeCarryoverLossError": {
"type": "number",
"description": "The uncertainty (based on a 95% CI) in the carryover loss in stock."
},
"distributionBinWidth": {
"type": "number",
"description": "The bin width for the distribution of stock."
},
"annualYears": {
"type": "array",
"description": "The years for which annual data is available. Index for all other annual* properties"
},
"annualCount": {
"type": "array",
"description": "The count of sites for each year in the branch"
},
"annualAreaKm2": {
"type": "array",
"description": "The area of the site(s) for each year"
},
"annualStock": {
"type": "array",
"description": "The stock value for each year (tonnes)."
},
"annualStockError": {
"type": "array",
"description": "The uncertainty (based on a 95% CI) in stock for each year (tonnes)."
},
"annualStockMin": {
"type": "array",
"description": "The minimum pixel-level stock density for each year (t/ha)."
},
"annualStockMax": {
"type": "array",
"description": "The maximum pixel-level stock density for each year (t/ha)."
},
"annualChange": {
"type": "array",
"description": "The net change for each year vs. the previous year (tonnes). First index will always be null for annualChange* properties."
},
"annualChangeError": {
"type": "array",
"description": "The uncertainty (based on a 95% CI) for the net change (tonnes)."
},
"annualChangeMin": {
"type": "array",
"description": "The minimum pixel-level year-over-year change for each year (t/ha)."
},
"annualChangeMax": {
"type": "array",
"description": "The maximum pixel-level year-over-year change for each year (t/ha)."
},
"annualChangeLoss": {
"type": "array",
"description": "Stock loss for each year vs. the previous year (tonnes)."
},
"annualChangeLossError": {
"type": "array",
"description": "The uncertainty (based on a 95% CI) for the loss in stock (tonnes)."
},
"annualChangeGain": {
"type": "array",
"description": "Stock gain for each year vs. the previous year (tonnes)."
},
"annualChangeGainError": {
"type": "array",
"description": "The uncertainty (based on a 95% CI) for the gain in stock (tonnes)."
},
"annualDistributionMeanValue": {
"type": "number",
"description": "The mean value of stock for each year."
},
"annualDistributionBinValues": {
"type": "array",
"description": "The bin values for the distribution of stock densities for each year.",
"items": {
"description": "The bin values for a single year. So, for example, if the bin width is 10, and the bin values are [300,200,100], there are 300 pixels between 0-10 t/ha, 200 pixels between 10-20 t/ha, and 100 pixels between 20-30 t/ha.",
"type": "array",
"items": {
"type": "number",
"description": "The bin value for a single year for a specific bin. Typically whole numbers, but may be fractional if branch includes mixed 10m and 30m sites."
}
}
}
},
"additionalProperties": {
"type": "object",
"description": "Additional data products, like forest coverage and change, appear as nested stats objects."
}
}
Here is an example of how to retrieve the stats data for a site using the chloris-app-sdk in Python:
# download_stats.py
from chloris_app_sdk import ChlorisAppClient
# create the client
client = ChlorisAppClient(
organization_id="your-organization-id",
refresh_token="your-refresh-token", # env: CHLORIS_REFRESH_TOKEN
# or
# id_token="your-id-token" # env: CHLORIS_ID_TOKEN
)
reporting_unit_id = 'abcd1234'
# get the stats of a reporting unit
reporting_unit_entry = client.get_reporting_unit(reporting_unit_id)
stats = client.get_reporting_unit_stats(reporting_unit_entry)
# print a mini report with data from 2019
print(f"Total area: {reporting_unit_entry['areaKm2']} km²")
print(f"Period start/end: {stats['periodChangeStartYear']} - {stats['periodChangeEndYear']}")
for i in range(len(stats["annualYears"])):
year = stats["annualYears"][i]
if year != 2019:
continue
print(f"Total stock ({year}): {stats['annualStock'][i]} t")
if "forest" in stats:
print(f"Forest area ({year}): {stats['forest']['annualAreaKm2'][i]} km²")
print(f"Forest stock ({year}): {stats['forest']['annualStock'][i]} t")
Here is an example of how to retrieve the stats data for a site in Javascript (node):
// downloadStats.mjs
import fetch from "node-fetch";
const organizationId = "your-organization-id";
const reportingUnitId = "abcd1234";
const reporting_unit_entry_response = await fetch(`https://app.chloris.earth/api/reportingUnit/`, {
headers: {
Authorization: `Bearer ${idToken}`,
},
body: JSON.stringify({
organizationId,
reportingUnitId,
}),
});
const reporting_unit_entry = await reporting_unit_entry_response.json();
const dataPath = reporting_unit_entry.dataPath.replace("s3://chloris-app-data/", "https://app.chloris.earth/");
const url = `${dataPath}/stats.json`;
const stats_response = await fetch(url);
const stats_response_json = await stats_response.json();
console.log(stats_response_json);
Geospatial data
This section describes the geospatial data that is generated by the Chloris Platform and how to access it. Because geospatial data can be confusing, we'll start with a brief introduction to the concepts and terminology. Then cover input datasets, output datasets, and how to access them.
Geospatial data concepts
Geospatial data is any data that has a location component, specifically on Earth. It is separated into two distinct classes: vector and raster. Vector datasets are composed of points (such as longitude and latitude), lines, and polygons. Raster datasets are composed of pixels, possibly with multiple bands, which store more than one value for a single grid cell.
The Chloris Platform consumes vector datasets in the form of site boundaries, and produces raster output datasets. One special thing about geospatial datasets that is not immediately obvious is that every geospatial dataset must have a Coordinate Reference System (CRS), sometimes called a "projection", which describes where the data is located on Earth. The CRS is usually specified in the metadata of the dataset, but certain formats (such as GeoJSON) assume a CRS of WGS84 (EPSG:4326) if one is not specified. Without the correct CRS, the data will not be located in the correct place on Earth.
The Chloris Platform also produces tabular data (i.e. CSV files), but this is not generally considered to be geospatial data. There is one notable exception, which is when the tabular data contains a location component (e.g. longitude and latitude) as we sometimes see with field plots around a central point. Currently, the Chloris Platform does not consume field data, so within the platform interface, you will only see tabular data in the form of graphical tables or CSV files.
Additional beginner geospatial resources:
Input datasets
Site boundaries
Site boundaries are the primary input dataset that the Chloris Platform consumes. Site boundaries are vector datasets that describe the location and shape of a site. The Chloris Platform accepts site boundaries in most single-file formats, plus Shapefiles which have been zipped. Additionally, in the platform interface, you can draw your own site boundaries using the interactive map.
In order to assure the quality of our results, we enforce limits on the minimum size of sites and individual polygons (for statistical accuracy), how sparse the site may be (especially when given multi-polygons), and maximum precision of the vertices. If your site boundaries do not meet these requirements, you will receive a detailed error message explaining why.
Site boundary projection
The Chloris Platform automatically re-projects each site boundary to the most appropriate UTM-based projection based on the center of the bounding box of the boundary. Therefore, you do not typically need to re-project your boundaries prior to submitting them if you are using a common projection. A notable exception to this are LCC projections, which we recommend re-projecting to WGS84 (EPSG:4326) and verifying the results ahead of time. See computing projections / areas for more info.
Output datasets
For each site, the following datasets are produced:
Normalized boundary polygon
For your review and convenience, the Chloris Platform will return the normalized site boundary that was used for analysis with any invalid geometries (i.e. points and lines) removed, and all polygons flattened into a single multi polygon. Curve geometries are not supported in the platform and are approximated using linear segments. After a site has been submitted, you can retrieve the normalized boundary polygon using the boundaryPath field in the reporting unit entry.
Representative point
For map navigation purposes, the Chloris Platform will return a point (X,Y) that is representative of the reporting unit in WGS84 lon/lat when listing reporting units in the fields pointX and pointY. This point is guaranteed to be inside the reporting unit boundary geometry, so it is safe to perform spatial joins with (but may not be the exact centroid). If the same site is submitted multiple times, the representative points will be identical.
GeoTiff raster file downloads
If your licensing agreement includes data download, the data products (listed below) are available for download as GeoTiff files. Annual data products have one file per year. Difference data products are relative to the previous year, so change from 2000 to 2001 is reported in the 2001 file and there is no 2000 change file. Note that "t" refers to metric tonnes and "ha" refers to hectares.
Available datasets:
stock: Annual above-ground biomass stock in the given year (t/ha).change: Annual above-ground biomass change (t/ha/yr).se: Standard error of the annual above-ground biomass stock estimates (t/ha)*.change_se: Standard error of the annual above-ground biomass change estimates (t/ha/yr)*.forest: Area which is forest in the selected year. Boolean (0 or 1) mask.forest_area_gain: Area which is forest in the given year, but not the previous year.forest_area_loss: Area which is not forest in the given year, but was forest in the previous year.forest_area_degradation: Area which remained forest in the given year, but experienced statistically significant losses in biomass.
* Introduced 2024-01-17.
An index of the files available to download is available at {dataPath}/downloads.json where dataPath is specified on the reporting unit entry.
The contents of the downloads index follow this schema:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"downloads": {
"type": "array",
"items": {
"description": "An entry for a single downloadable file.",
"type": "object",
"properties": {
"format": {
"type": "string",
"enum": ["tif", "geojson", "zip"]
},
"date": {
"description": "The date of the data in the file.",
"type": "string"
},
"url": {
"type": "string"
},
"urlFormat": {
"type": "string",
"description": "The key in the urlFormats object which this url conforms to. Optional, as not all urls will conform to a urlFormat (e.g. a project-level summary pdf report)."
},
"sizeMB": {
"description": "The approximate size of the file in megabytes.",
"type": "number"
}
},
"required": ["format", "date", "url"]
}
},
"urlFormats": {
"description": "A map of url formats which may be used to parse variables out of urls.",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"formatVersion": {
"type": "string",
"enum": ["0.1"]
}
},
"required": ["formatVersion", "downloads", "urlFormats"]
}
Example downloads index:
{
"formatVersion": "0.1",
"downloads": [
{
"format": "tif",
"date": "2023-01-01",
"url": "s3://chloris-app-data/data/abc01234/efg56789/downloads/merged_2000_95a.tif",
"urlFormat": "stock"
},
{
"format": "tif",
"date": "2023-01-01",
"url": "s3://chloris-app-data/data/abc01234/efg56789/downloads/merged_2001_ch95a.tif",
"urlFormat": "change"
},
{
"format": "geojson",
"date": "2023-01-01",
"url": "s3://chloris-app-data/data/abc01234/efg56789/downloads/efg56789_boundary.geojson",
"urlFormat": "boundary"
}
],
"urlFormats": {
"stock": "s3://chloris-app-data/data/{organizationId}/{reportingUnitId}/downloads/merged_{year}_95a.tif",
"change": "s3://chloris-app-data/data/{organizationId}/{reportingUnitId}/downloads/merged_{year}_ch95a.tif",
"boundary": "s3://chloris-app-data/data/{organizationId}/{reportingUnitId}/downloads/{reportingUnitId}_boundary.geojson"
}
}
This means you can directly iterate the list for urls, or parse out arbitrary variables for your own processing needs. Note that chloris-app-data is a private bucket accessible only through app.chloris.earth, so you will need to replace the s3://chloris-app-data/ prefix with https://app.chloris.earth/ to download the files.
Here is an example of how to download all the stock rasters for a site using the chloris-app-sdk in Python:
# download_stock_rasters.py
import urllib3
from chloris_app_sdk import ChlorisAppClient
http = urllib3.PoolManager()
# create the client
client = ChlorisAppClient(
organization_id="your-organization-id",
refresh_token="your-refresh-token", # env: CHLORIS_REFRESH_TOKEN
# or
# id_token="your-id-token" # env: CHLORIS_ID_TOKEN
)
reporting_unit_id = 'abcd1234'
# get the reporting unit entry with downloads (if available)
reporting_unit_entry = client.get_reporting_unit(reporting_unit_id, include_stats=False, include_downloads=True)
if not reporting_unit_entry.get('downloads'):
raise Exception('No downloads available for this reporting unit')
downloads_index = reporting_unit_entry['downloads']
# Filter for stock rasters
stock_urls = [d['url'] for d in downloads_index['downloads'] if d['urlFormat'] == 'stock']
print(f'Downloading {len(stock_urls)} stock rasters for reporting unit {reporting_unit_id}')
for url in stock_urls:
r = http.request('GET', url.replace('s3://chloris-app-data/', 'https://app.chloris.earth/'))
fname = url.split('/')[-1]
with open(fname, 'wb') as f:
f.write(r.data)
print(f'Downloaded {fname}')
Here is an example of how to download all the stock rasters for a site in Javascript (node):
// downloadStockRasters.mjs
import fetch from "node-fetch";
const organizationId = "your-organization-id";
const reportingUnitId = "abcd1234";
const reporting_unit_entry_response = await fetch(`https://app.chloris.earth/api/reportingUnit/`, {
headers: {
Authorization: `Bearer ${idToken}`,
},
body: JSON.stringify({
organizationId,
reportingUnitId,
}),
});
const reporting_unit_entry = await reporting_unit_entry_response.json();
const dataPath = reporting_unit_entry.dataPath.replace("s3://chloris-app-data/", "https://app.chloris.earth/");
const url = `${dataPath}/downloads.json`;
const downloads_response = await fetch(url);
const downloads_response_json = await downloads_response.json();
const stockUrls = downloads_response_json.downloads.filter((d) => d.urlFormat === "stock").map((d) => d.url);
for (const url of stockUrls) {
const response = await fetch(url);
const buffer = await response.buffer();
const fname = url.split("/").pop();
await fs.promises.writeFile(fname, buffer);
console.log(`Downloaded ${fname}`);
}
Computing projections / areas
The Chloris Platform uses the UTM-based projections for each site boundary when computing the area of the site. The following sample code computes the UTM zone number and EPSG code for a given geopandas DataFrame in Python:
import math
import geopandas as gpd
def get_utm_zone(lon: float, lat: float) -> int:
""" Get UTM zone number from latitude and longitude """
if 72.0 <= lat < 84.0:
if 0.0 <= lon < 9.0:
return 31
if 9.0 <= lon < 21.0:
return 33
if 21.0 <= lon < 33.0:
return 35
if 33.0 <= lon < 42.0:
return 37
if 56 <= lat < 64.0 and 3 <= lon <= 12:
return 32
return math.floor((lon + 180) / 6) + 1
def get_utm_epsg(lon: float, lat: float) -> int:
""" Find the UTM zone for a given longitude and latitude and return the EPSG code. """
zone = get_utm_zone(lon, lat)
epsg_code = 32600
epsg_code += int(zone)
if lat < 0: # Southern hemisphere
epsg_code += 100
return epsg_code
def get_gdf_area_km2(gdf: gpd.GeoDataFrame) -> float:
""" Calculate the total area of a GeoDataFrame in km2 using the center of its bounding box. """
bbox = gdf.to_crs(epsg=4326).total_bounds
epsg = get_utm_epsg(lat=(bbox[1] + bbox[3]) / 2, lon=(bbox[0] + bbox[2]) / 2)
gdf = gdf.to_crs(epsg=epsg)
# calculate area in km2
gdf["area_km2"] = gdf.area / 1e6
return gdf["area_km2"].sum()
Webhooks
To be notified when sites are ready, you can register a webhook URL for your organization via your organization options panel in the Chloris Platform web interface.
You will be asked to provide an HTTPS publicly-accessible URL for the webhook and a signing secret (a random string of your choosing).
This URL will be called when the site is ready. The webhook will be called with a POST request with the following JSON body:
{
type: "reportingUnit",
data: {
organizationId: "your-organization-id",
reportingUnitId: "your-reporting-unit-id",
analysisCompletedAt: "2024-01-01T00:00:00.000Z",
// ISO 8601
exportCompletedAt: "2024-01-01T00:00:00.000Z",
// ISO 8601, omitted if geospatial outputs are not yet available
},
}
If the request fails, the webhook will be retried once after a random delay of 10-30 seconds. If the request fails again, the webhook will not be retried. In the future, we may add further retry logic, so please let us know if you have a specific use case that requires this.
The analysisCompletedAt timestamp is included to help you determine if the analysis results have been updated since the last time you checked. In the event that a site is reprocessed, the analysisCompletedAt timestamp will be updated. This can happen if the site is reprocessed due to a bug fix or as new data becomes available. If your system is caching the results of the site, you can use this timestamp to determine if you need to update your cache.
If you have licensed geospatial outputs, the webhook will also be invoked a second time with an additional
As of 1.5.0 exportCompletedAt timestamp in the payload. This timestamp indicates when the geospatial outputs are ready to be downloaded. If you are using the webhook to download the geospatial outputs, you can use this timestamp to determine when to download the outputs.exportCompletedAt is deprecated and will be removed in a future version. Downloads are now always available when the webhook is called. For backwards compatibility, the field will continue to be included in webhook calls using the same value as analysisCompletedAt, but will be removed in a future version.
Control sites (or any site marked hidden: true) will not trigger a webhook, but will delay the primary site's webhook notification until the control site is ready.
Because there are several scenarios where the webhook will be called multiple times, you should ensure that your webhook endpoint is idempotent. This means that if the webhook is called multiple times, the final state of the site in your system will be both valid and up to date.
Signature validation
When our system calls your webhook endpoint, we will include a signature in the X-Signature http header. We recommend that you verify this signature to ensure that the request is actually from us.
The X-Signature header is a hash made up of the signing secret you specified when creating the
webhook in your Chloris Platform account plus the request body. To verify each request to your webhook, you should re-create the hash and compare it to the signature in the header.
Here is an example of validating the signature in Node / Javascript:
import crypto from "crypto";
// the signing secret you specified when creating the webhook
const secret = "";
const body = ""; // the raw request body
const hmac = crypto.createHmac("sha256", secret);
const digest = Buffer.from(hmac.update(request.rawBody).digest("hex"), "utf8");
const signature = Buffer.from(request.get("X-Signature") || "", "utf8");
if (!crypto.timingSafeEqual(digest, signature)) {
throw new Error("Invalid signature.");
}
And here is an example of validating the signature in Python:
import hmac
import hashlib
# the signing secret you specified when creating the webhook
secret = ''
# the signature in the X-Signature header
signature = ''
# the raw request body (depending on your http library, you may need to encode this.)
body = ''
digest = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(digest, signature):
raise Exception('Invalid signature.')
Testing your webhook endpoint
Here is a curl command that will simulate a webhook call with the signing secret "secret1234" so you can test your endpoint:
curl -X POST \
-H "Content-Type: application/json" \
-H "X-Signature: 211eee254e5bab5761e82452a9cf37e1396329cadc9957a99ea64197bd1c25a0" \
-d '{"type": "reportingUnit", "data": {"organizationId": "your-organization-id", "reportingUnitId": "your-reporting-unit-id", "analysisCompletedAt": "2024-01-01T00:00:00.000Z"}}' \
http://localhost:8080/webhook
Map integration
The Chloris Platform provides web maps which may be integrated into your web applications. Alternatively, you may choose to export tiff files for use in your own web mapping solutions, but may still want to use Chloris map styling for consistency. This page describes the options available for integrating Chloris maps into your web applications, complete with color maps and legends.
Layers config
Each site may have different layers available, so the Chloris Platform provides a "layers config" file at {dataPath}/layers.json that describes the available layers and their properties. As of version 1.9.0, the layers config files for all sites comply with this JSON schema.
The following layers ("productTypes") are available for most sites:
stockchangecumulative_changeforestnon_forest(only the non-forest pixels from the forest layer)forest_area_gainforest_area_lossforest_area_degradation
For some productTypes, multiple variants (e.g. dynamic-30m) may be made available based on different webtiling methods, analysis resolutions, zoom levels, or other factors. See the layers config file for details.
Here is an example for how to retrieve the layers config for a reporting unit using the chloris-app-sdk in Python:
import urllib3
from chloris_app_sdk import ChlorisAppClient
http = urllib3.PoolManager()
# create the client
client = ChlorisAppClient(
organization_id="your-organization-id",
refresh_token="your-refresh-token", # env: CHLORIS_REFRESH_TOKEN
# or
# id_token="your-id-token" # env: CHLORIS_ID_TOKEN
)
reporting_unit_id = 'abcd1234'
# get the reporting unit entry with downloads (if available)
reporting_unit_entry = client.get_reporting_unit(reporting_unit_id, include_layers_config=True)
if not reporting_unit_entry.get('layers'):
raise Exception('No layers available for this reporting unit')
print(reporting_unit_entry['layersConfig'])
Layer types
Dynamic webtile layers
For sites processed or updated since 1.6.0, the Chloris Platform provides dynamically generated webtiles for all map layers by default. Dynamic webtiles are provided as XYZ tiles via REST API of the general form:
GET https://app.chloris.earth/tiles/cog/tiles/WebMercatorQuad/:z/:x/:y.webp@2x?data_path=:dataPath&product_type=:productType&analysis_completed_at=:analysisCompletedAt&year=:year
Where dataPath and analysisCompletedAt are specified on the reporting unit entry, productType is one of the available layers for the site. The @2x suffix is optional and may be omitted for lower resolution tiles. {z}, {x}, and {y} are the zoom level, and WMTS x and y coordinates of the tile, respectively. Additional parameters can be seen in the Dynamic Tiler API reference.
Example
Here is an example using MapLibre GL JS to display a dynamic webtile layer:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Add a raster tile source</title>
<meta property="og:description" content="Add a third-party raster source to the map." />
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel='stylesheet' href='https://unpkg.com/maplibre-gl@5.0.0/dist/maplibre-gl.css' />
<script src='https://unpkg.com/maplibre-gl@5.0.0/dist/maplibre-gl.js'></script>
<style>
body {
margin: 0;
padding: 0;
}
html, body, #map {
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script type="module">
(async () => {
// parameters retrieved from the reporting unit entry, a sample dataset is show here.
const dataPath = "s3://chloris-app-data/data/0183c887-9933-7751-ab91-4eed6dfd057f/019194dc-a302-7c9d-b708-ebe5e056d592_2024-08-27T17:22Z/";
const analysisCompletedAt = "2024-08-27T21:59Z";
// layer / year we're interested in
const productType = "stock";
const year = 2023;
// get the bounds of the reporting unit layer
const bounds = (await (await fetch(`https://app.chloris.earth/tiles/cog/bounds?data_path=${dataPath}&product_type=${productType}&year=${year}&analysis_completed_at=${analysisCompletedAt}`)).json()).bounds;
const map = new maplibregl.Map({
container: "map", // container id
style: {
"version": 8,
"sources": {
"stock_2023": {
"type": "raster",
"tiles": [
`https://app.chloris.earth/tiles/cog/tiles/WebMercatorQuad/{z}/{x}/{y}.webp?data_path=${dataPath}&product_type=${productType}&year=${year}&analysis_completed_at=${analysisCompletedAt}`
],
"scheme": "xyz",
"minzoom": 0,
"maxzoom": 16,
"tileSize": 512,
"attribution": "AGB stock data from Chloris Geospatial",
bounds
},
"osm": {
"type": "raster",
"tiles": ["https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"],
"tileSize": 256,
"attribution": "© OpenStreetMap Contributors",
"maxzoom": 19
}
},
"layers": [
{
"id": "osm",
"type": "raster",
"source": "osm"
},
{
"id": "stock_2023",
"type": "raster",
"source": "stock_2023",
"minzoom": 0,
"maxzoom": 16
}
]
},
bounds: [[bounds[0], bounds[1]],[bounds[2], bounds[3]]],
});
})();
</script>
</body>
</html>
Pre-computed webtile layers (deprecated)
Deprecation notice: This feature and the following section of our docs is outdated and will be removed in a future version.
Prior to 1.6.0, the Chloris Platform provided pre-computed webtiles for all sites. These are still available for sites that were analyzed prior to 1.6.0, but new sites and updates to existing sites will not have pre-computed webtiles available.
The Chloris Platform produces TMS tiles for each site in the platform. The exact symbology information for each is available at :dataPath/layers.json and tiles themselves are available at :dataPath/:layerName/:year?/:z/:x/:y.webp for authenticated users. If you are used to XYZ tiles, the y coordinate in TMS is flipped (i.e. y = 2^z - 1 - y). In general, zoom levels 0-13 are produced in 512px by 512px resolution tiles (effective 9.555m / pixel on the equator). However, the layers config file(layers.json) contains the exact zoom levels for each layer so there is no guesswork. The :year? parameter is omitted for non-annual products.
Colormaps and legends
Whether you are using our webtiles or your own custom solution, we recommend using the following color maps and legends for consistency when showing Chloris data. These are the same color maps and legends used in the Chloris Platform web interface, with only minor modifications to make it easier for you to integrate them into your own applications. We present them here in TOML format, realizing that you will likely need to translate them into the format used by your mapping library. Other formats are available upon request.
Annual Stock
Stock maps are presented using a continuous color ramp, aligned to values 0 to 650 t/ha, using percentages so the exact values can be adjusted to individual sites. We recommend displaying values exceeding this range as 650 t/ha as they are likely to be outliers.
[2024-04-09_stock_light_650]
comment = "for use up to 650 t/ha. Values should be clamped to 650 t/ha"
clamp_max = 650
continuous = true
continuous_percentages = [0.0, 1e-6, 0.08, 0.17, 0.26, 0.37, 0.46, 0.56, 0.68, 0.83, 0.90, 1.0, ]
colors = [[255, 255, 255, 255, ],
[252, 252, 252, 255, ],
[223, 214, 180, 255, ],
[215, 224, 130, 255, ],
[137, 219, 55, 255, ],
[53, 194, 41, 255, ],
[17, 166, 68, 255, ],
[17, 135, 168, 255, ],
[21, 87, 158, 255, ],
[72, 11, 138, 255, ],
[65, 7, 110, 255, ],
[191, 20, 140, 255, ],
]
Annual stock legend images
The following images may be linked to in your application to provide a legend for the stock map:


Annual change
Change maps are presented using a diverging color ramp using discrete bins. If your mapping library does not support binning, displaying the values as a continuous color ramp is also acceptable, but you may need to define percentage-based values for the color ramp to align with your data. In general, losses may be larger than gains in any given year, as deforestation may happen suddenly, but growth is more gradual.
[2022-11-18_change_light]
comment = "values are in tons, but based on tco2e, as shown in labels"
diverging = true
diverging_symmetric = false
value_mass_units = "t"
value_area_units = "ha"
discrete_bin_values = [
-9999.0,
-190.90909090909090909091,
-109.09090909090909090909,
-54.54545454545454545455,
-27.27272727272727272727,
-5.45454545454545454545,
0.0,
0.54545454545454545455,
2.72727272727272727273,
5.45454545454545454545,
10.90909090909090909091,
21.81818181818181818182,
9999.0,
]
discrete_bin_labels = ["< -350", "-350", "-200", "-100", "-50", "-10", "0", "1", "5", "10", "20", "40", "> 40", ]
colors = [
[74, 16, 11, 255],
[115, 40, 33, 255],
[222, 91, 62, 255],
[227, 163, 79, 255],
[230, 190, 96, 255],
[247, 239, 205, 255],
[255, 255, 255, 255],
[218, 232, 213, 255],
[180, 217, 171, 255],
[133, 184, 121, 255],
[69, 135, 62, 255],
[31, 117, 21, 255],
[25, 79, 68, 255],
]
null_color = [255, 255, 255, 0, ]
Annual change legend images
The following images may be linked to in your application to provide a legend for the change map:


Working with Collections
The Collections feature is in beta release
For creating and editing collections through the web interface, see the user guide. This page is for working with collections via the API.
Using chloris-app-sdk (Python)
You can create or update a collection in the chloris-app-sdk by using the client.put_collection function.
Here is an example which will create a new collection:
from chloris_app_sdk import ChlorisAppClient
# create the client
client = ChlorisAppClient(
organization_id="your-organization-id",
refresh_token="your-refresh-token", # env: CHLORIS_REFRESH_TOKEN
# or
# id_token="your-id-token" # env: CHLORIS_ID_TOKEN
)
# create or update a collection
collection = client.put_collection(
collection_entry={
"organizationId": "your-organization-id",
# "reportingUnitId": "your-collection-reporting-unit-id", # include to edit an existing collection
"label": "your-collection-name",
"description": "your-collection-description"
"reportingUnitIds": ["reportingUnitId1", "reportingUnitId2", ...]
}
)
# the resulting collection entry
print(collection)
# {
# 'organizationId': '...',
# 'reportingUnitId': '...',
# 'collectionId': '...',
# 'branchId': '...',
# 'label': '...',
# 'description': '...',
# 'reportingUnitIds': [...],
# 'updatedAt': '...',
# 'createdAt': '...',
# 'createdBy': '...',
# 'hidden': False
# }
Using the REST API (JavaScript)
Here is an example of creating a collection using the aws-sdk in JavaScript:
// node-specific imports
import fetch from "node-fetch";
const organizationId = "your-organization-id";
// Create a collection
const collection = await (
await fetch("https://app.chloris.earth/api/collection", {
method: "PUT",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${yourApiIdToken}`,
},
body: JSON.stringify({
organizationId: organizationId,
// reportingUnitId: "yourReportingUnitId", // include to edit an existing collection
label: "your-label"
description: "your-description",
reportingUnitIds: [...]
}),
})
).json();
Changelog
All notable changes to the API will be documented here. This project adheres to Semantic Versioning, but is aligned to the platform frontend version, so some versions may be skipped if there are no changes to the API.
The API is actively maintained and developed but does not have a regular release schedule.
Unreleased
Added
PUT /reportingUnitnow returns409 Conflictwhen the organization already has a live site with the same label, resolution and boundary area, both on submission and on approval. The message names the existing site.- Added the administrator-only
allowDuplicateparameter toPUT /reportingUnitto bypass that check.
1.12.0 - June 2, 2026
Added
- Added
searchLabelparameter toPOST /reportingUnit(list) for case-sensitive substring search on thelabelfield - Added
maxPagesparameter toPOST /reportingUnit(list) to control the per-request scan budget whensearchLabelis set. Defaults to 5, capped at 50 (raised from 20 to better support large organizations)
Changed
- Search results from
POST /reportingUnitwithsearchLabelare now sorted byanalysisCompletedAtdescending, with non-completed sites at the bottom
1.10.0 - September 10, 2025
Added
- Beta release of collections, users can add and edit collections
1.9.6 - July 31, 2025
Added
- Added mangrove model option
1.9.2 - May 13, 2025
Changed
- Increase site geometry sparsity check to 1% of bbox and convex hull.
- Remove 500m global product from Platform.
1.9.1 - April 2, 2025
Added
- Added support for 2024 data
1.9.0 - March 1, 2025
Added
Fixed
Changed
- Layer config structure has been improved to support more layers, alternate carbon pools.
Where previously layers.json had 2 levels of nested objects, with the first level having a product type like "stock", "change", etc and the second level having one or more keys to differentiate layer variants (resolutions, tiling methods, and more), the new structure uses an array of objects, with the product type, resolution, and carbon pool as object properties (more info). Moving forward, you should filter over the array to find the layer you need like so:
# ...
reporting_unit_entry = client.get_reporting_unit(reporting_unit_id, include_layers_config=True)
stock_layer = next((layer for layer in reporting_unit_entry["layers"] if layer['product'] == 'stock' and layer['resolution'] == 30 and layer['carbonPool'] == 'agb'), None)
If you would like to defer migrating to the new structure, install chloris-app-sdk version 1.0.16, which has limited forward compatibility with the new structure.
- Fixed issue uploading kmz files
- Fixed incorrect resolution field values for sites and collections
- Fixed forest mask layers being produced with an empty first band.
- Fixed issue where change map legends were flipped.
[v1.8.3] - February 25, 2025
Added
Fixed
Changed
- Changed Python SDK handling of layers config (layers.json) files in preparation for upcoming schema changes. Version
1.0.16of the SDK provides forward-compatibility with the new schema, version 1.1.0 (not yet released) will use the updated schema.
Deprecated
[v1.8.1] - January 22, 2025
Added
Fixed
- Fixed an issue that limited submission of areas larger than 3 Mha in some cases.
Changed
- Improved documentation for integrating dynamic webtiles from the Chloris Platform into web maps, now with legends.
Deprecated
[v1.7.4] - October 30, 2024
Added
- Documentation for dynamic webtiling and layers config.
Fixed
Changed
Deprecated
[v1.7.0] - August 19, 2024
Added
- 10 m resolution analysis can now be requested by passing
resolution: 10when submitting sites. Contact your account representative for more information.
Fixed
- Fixed delayed error message for site submissions with very large areas.
Changed
- Error messages relating to account limits when submitting sites now specify separate limits for 30 m and 10 m resolution analysis.
Deprecated
[v1.6.1] - July 19, 2024
Added
- New dynamic webtiling endpoint for select regions. More information as we expand this feature.
Fixed
Changed
Deprecated
- For regions now using the dynamic webtiling, static webtiles are no longer available for new analyses. Existing analyses will continue to have access to static webtiles.
Security
[v1.5.0] - April 9, 2024
Added
Fixed
Changed
- The source of truth for where data is located for all new reporting units is
reportingUnit.dataPath, instead of the templated format/data/{organizationId}/{reportingUnitId}. This is non-breaking for previously generated data, as all existing data will remain in place.reportingUnit.dataPathis available for all reporting units, and we recommend migrating usage entirely to this.
Deprecated
- Usage of
exportCompletedAtin webhook calls is no longer necessary and will be removed in a future version. Downloads are now always available when the webhook is called. See the webhook documentation for more information.