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.