Tree Canopy API

Access & limitsPublic, no key needed · Heavy▾

Open to everyone. A free API key from your profile raises the limits: send it in an X-API-Key header, or as api_key= in the address.

5 minutesHourDay
Without a key20120500
With a key604002,000

Each 100 square km of area counts as one request, so a 10 km square is 1 and a 30 km square is 9.

Log in to create a free key on your profile page.

Every answer carries X-RateLimit-Remaining and X-RateLimit-Reset. Over a limit, the answer is error 429 with retry_after in seconds. Refused requests do not count.

GET No key needed JSON or binary file Up to 30 km square 1-bit or 4-bit cells 120 an hour without a key, 400 with one

Where the trees are, and how tall, over a square around a point: every cell of the canopy store, about 3 by 5 metres, the same data the Tree Canopy Height Map draws. Ask for it as JSON for a web page or script, or as a small binary file that a flight computer or ground station can carry and read with a few lines of code, so a launch site's trees can be built into firmware rather than fetched on the day.

GEThttps://www.altimetercloud.com/api/trees/lat lon size_km

Parameters

ParameterMeaning
lat, lonThe middle of the square, decimal degrees (lng also accepted)
size_kmThe square's side in km, more than 0 and up to 30. Left out, 1. Squares over 10 km come as format=bin only, and take longer to prepare: up to half a minute for 30 km.
bitsOptional. 4 (the default): each cell is its height band, 0 to 15. 1: each cell is 1 for trees of 3 m or more and 0 for none, a quarter of the size.
formatOptional. json (the default, for squares up to 10 km): the cells as base64 inside a JSON answer with everything needed to read them. bin: a 32 byte header and the cells, as a file, for any size.
compressOptional. none (the default), zlib or lzma. With format=bin the whole file is compressed; with json the cells are, before the base64. lzma is LZMA-alone (.lzma), made for small devices: see LZMA below.
downloadOptional. 1 makes a browser save a format=bin answer as a file, named for the square.

The cells

A 1 km square near Windermere on the Tree Canopy Height Map, beside the same square drawn straight from its 4-bit download

A 1 km square near Windermere: on the left as the Tree Canopy Height Map shows it, on the right drawn straight from a 4-bit download of it, each cell coloured by its band and black where there are no trees.

The grid is the canopy store's own, so nothing is resampled: each cell is exactly 1/24000 of a degree each way, about 4.6 m north to south and, at UK latitudes, about 2.7 m east to west. Row 0 is the north edge and column 0 the west edge; rows run south and columns east. Every row starts on a whole byte, so row j starts at byte j * row_bytes.

bitsPer cellPacking
4Height band 0 to 15Two cells a byte, the first in the high four bits
11 = trees of 3 m or more, 0 = noneEight cells a byte, the first in the highest bit

The height bands, lowest height in each:

Band0123456789101112131415
Fromnone3 m5 m7 m10 m13 m16 m19 m22 m26 m30 m35 m40 m46 m53 m61 m

The heights are the trees as they stood when the satellite imagery was taken: between 2017 and 2020, mostly 2018 to 2020, for the Meta data, and 2020 for the ETH fill-in. Woodland felled or planted since then will not show yet.

Band 0 is open ground, water, and anything under 3 m, including scrub and hedges. Ground not yet in the database is 0 as well; the JSON answer's sources.none counts those cells.

Sizes

At UK latitudes, before compression:

SquareCells4-bit1-bit
1 km217 × 36839 KB10 KB
3 km648 × 1,102349 KB87 KB
5 km1,079 × 1,835967 KB242 KB
10 km2,157 × 3,6693.8 MB967 KB
20 km4,313 × 7,33715.1 MB3.8 MB
30 km6,469 × 11,00533.9 MB8.5 MB

Compression takes these down a long way. File sizes near Windermere in the Lake District (54°N), over mixed woodland, fells and fields; the 1, 3 and 10 km rows are real downloads, and the rows marked ~ are estimated from them:

Square4-bit, height bands1-bit, tree or not
RawzliblzmaRawzliblzma
1 km40 KB13 KB13 KB10 KB4 KB4 KB
3 km352 KB90 KB82 KB88 KB28 KB23 KB
5 km967 KB~250 KB~230 KB242 KB~70 KB~60 KB
10 km3.8 MB1.0 MB0.9 MB967 KB268 KB217 KB
20 km15.1 MB~4 MB~3.6 MB3.8 MB~1 MB~0.85 MB
30 km33.9 MB~9 MB~8 MB8.5 MB~2.4 MB~1.9 MB

Two things move these. Further from the poles a square needs fewer columns, so the raw files shrink: a 30 km 4-bit square at 45°N is 28.5 MB against 33.9 MB here. And dense woodland packs less well than open ground: a real 30 km 4-bit square in the French Alps near Aix-les-Bains, mostly forested, came to 9.5 MB with lzma, about a third of its raw size, where the Windermere squares came down to about a quarter. lzma is always the smallest, by about a tenth for 4-bit and a fifth for 1-bit.

LZMA

compress=lzma gives an LZMA-alone stream, the .lzma format, not .xz: a 13 byte header (one properties byte, 0x5D for lc=3 lp=0 pb=2; a 4 byte dictionary size, 65536; the 8 byte uncompressed length, little endian) and then the compressed data. The 64 KB dictionary keeps the memory a decoder needs small enough for a microcontroller such as an ESP32, and the length is always the true one, so a decoder such as the LZMA SDK's LzmaDec knows exactly how much to expect. On a computer, xz --format=lzma -d or Python's lzma module with FORMAT_ALONE unpacks it.

The binary file

With format=bin: a 32 byte header, little endian, then the rows, north first.

ByteTypeValue
0char[4]ACTR
4uint8Version, 1
5uint8Bits per cell, 1 or 4
6uint16Cells per degree, 24000
8int32North edge in cells: its latitude × 24000
12int32West edge in cells: its longitude × 24000
16uint32Rows
20uint32Columns
24uint32Bytes per row
28uint320, reserved

The edges are whole numbers of cells, so a position maps to its cell exactly: row north_edge - 1 - floor(lat * 24000), column floor(lon * 24000) - west_edge.

Examples

https://www.altimetercloud.com/api/trees/?lat=54.35&lon=-3.03&size_km=3
https://www.altimetercloud.com/api/trees/?lat=54.35&lon=-3.03&size_km=5&bits=1&format=bin&compress=zlib
https://www.altimetercloud.com/api/trees/?lat=54.35&lon=-3.03&size_km=10&format=bin&compress=lzma&download=1

The JSON answer

{
  "success": true,
  "lat", "lon", "size_km", "bits",
  "rows", "cols", "cells", "row_bytes",      the grid, as above
  "cells_per_degree", "north_edge_cell", "west_edge_cell",
  "north", "south", "west", "east",          the square's edges, degrees
  "cell_deg", "cell_m_north_south", "cell_m_east_west",
  "wooded_pct",                              cells with trees of 3 m or more
  "coverage_pct",                            cells the database holds
  "sources": { "meta_chmv2", "eth_fine", "eth_medium", "eth_coarse", "none" },
  "band_min_m": [ 0, 3, 5, ... ],            bits=4
  "meaning", "order", "compress", "bytes",
  "data_b64",                                the rows, row 0 first, compressed if asked
  "file_name", "seconds",
  "credits": { "canopy", "source", "source_eth", "licence", "full" }
}

Reading the cells

On a device: what's below me

Give a position and get the cell it is in: every position falls in exactly one cell, so this is the ground directly underneath. The file sits in flash or on a card, and cells is the bytes straight after the 32 byte header. Use doubles for the position: a float's 24 bits leave only about an eighth of a cell to spare.

// C
typedef struct __attribute__((packed)) {
  char     magic[4];        // "ACTR"
  uint8_t  version;         // 1
  uint8_t  bits;            // 1 or 4
  uint16_t cells_per_deg;   // 24000
  int32_t  north_edge;      // latitude of the north edge * 24000
  int32_t  west_edge;       // longitude of the west edge * 24000
  uint32_t rows, cols, row_bytes, reserved;
} TreeHeader;

// the band (0 to 15), or 1/0 for tree or not, at a position; -1 outside the square
int treeAt(const TreeHeader *h, const uint8_t *cells, double lat, double lon) {
  int32_t j = h->north_edge - 1 - (int32_t)floor(lat * h->cells_per_deg);
  int32_t i = (int32_t)floor(lon * h->cells_per_deg) - h->west_edge;
  if (j < 0 || i < 0 || j >= (int32_t)h->rows || i >= (int32_t)h->cols) return -1;
  const uint8_t *row = cells + (uint32_t)j * h->row_bytes;
  if (h->bits == 4) { uint8_t b = row[i >> 1]; return (i & 1) ? (b & 15) : (b >> 4); }
  return (row[i >> 3] >> (7 - (i & 7))) & 1;
}

// a band's height range in metres; max_m is 0 for band 15 (61 m and over) and band 0
static const uint8_t BAND_MIN_M[16] = { 0, 3, 5, 7, 10, 13, 16, 19, 22, 26, 30, 35, 40, 46, 53, 61 };
void bandMetres(int band, int *min_m, int *max_m) {
  band &= 15;
  *min_m = BAND_MIN_M[band];
  *max_m = (band > 0 && band < 15) ? BAND_MIN_M[band + 1] : 0;
}

Trees near me

For landing: every cell whose middle is within a radius of the position, how many hold trees, how far away the nearest is and the tallest among them. The full version is treesWithin() in the reader download below; it works in metres from the cell size, which is 111320 / 24000 m north to south and that times the cosine of the latitude west to east.

TreeNear t = treesWithin(h, cells, lat, lon, 50.0f);   // within 50 m
// t.wooded     cells with trees of 3 m or more
// t.cells      cells in the circle (fewer near the square's edge)
// t.nearest_m  to the nearest wooded cell, -1 if none
// t.tallest    the highest band in the circle

Trees and the ground

The heights here are the trees' heights above the ground they stand on, not above sea level or above your launch pad. So the file tells you there are trees below you and how tall they are, but not how far below you their tops are: over a valley or a hillside, that depends on the ground as much as the trees. For that, combine it with the ground heights from the Terrain Elevation API, or the Terrain Grid API for a whole square to keep alongside the tree file on a device. An altimeter measures height above the pad, and the pad's ground height turns that into height above sea level; the tree tops below you are the ground height there plus the tree height.

// the pad's ground height and the ground height below you, both from the terrain APIs
double rocket_asl = pad_ground_m + height_above_pad_m;

int lo, hi;
bandMetres(treeAt(h, cells, lat, lon), &lo, &hi);
if (hi == 0 && lo > 0) hi = 70;                  // band 15 is 61 m and over: pick your own top
double canopy_asl = ground_below_m + hi;          // the top of the band, to be on the safe side
double clearance  = rocket_asl - canopy_asl;      // metres between you and the tree tops

Unpacking on a microcontroller

A compress=lzma file unpacks with LzmaDecode() from the LZMA SDK by Igor Pavlov: two C files, LzmaDec.c and LzmaDec.h, public domain, and the decoder the Altimeter Cloud Jupiter carries. Unpacked in one go straight into the final buffer, that buffer serves as the dictionary, so beyond the file itself it needs only about 16 KB while it works. On an ESP32, compress=zlib needs nothing extra: miniz's tinfl is already in its ROM. xz-embedded is not the one to use: it reads .xz, not .lzma.

Tree reader and LZMA decoderzip, packed 26/09/2026

In the zip: tree_reader.h and tree_reader.c with all of the above plus treeUnpackLzma(), and the LZMA SDK decoder files the Jupiter is built with. For Arduino, copy them into the sketch folder and add #include "tree_reader.h"; the LZMA part builds itself only when the SDK files are there. They were current on that date; for anything newer, check the LZMA SDK page.

On a computer

# Python
import requests, struct, zlib, numpy as np
r = requests.get('https://www.altimetercloud.com/api/trees/',
                 params={'lat': 54.35, 'lon': -3.03, 'size_km': 3, 'format': 'bin', 'compress': 'zlib'})
f = zlib.decompress(r.content)            # or lzma.decompress(r.content, format=lzma.FORMAT_ALONE)
magic, ver, bits, cpd, north, west, rows, cols, rb, _ = struct.unpack('<4sBBHiiIIII', f[:32])
g = np.frombuffer(f[32:], np.uint8).reshape(rows, rb)
if bits == 4:
    cells = np.empty((rows, rb * 2), np.uint8); cells[:, 0::2] = g >> 4; cells[:, 1::2] = g & 15
else:
    cells = np.unpackbits(g, axis=1)
cells = cells[:, :cols]                       # row 0 north, column 0 west
lat_of_row = lambda j: (north - j - 0.5) / cpd
lon_of_col = lambda i: (west + i + 0.5) / cpd

Errors

Errors come back as JSON with "success": false, a code and a message.

CodeMeaning
412A parameter is missing or not valid, the square is more than 30 km, or JSON was asked for over 10 km
413The square has too many cells, which only happens very close to the poles
429Too many requests from your address, or one still being answered
501The compression asked for is not available just now

Try it

The Tree Canopy Height Map has a download panel: move the map so the cross sits on your launch site, choose the size, cells and compression, and it saves the file from this API.

Canopy data credits

Canopy heights: Meta and World Resources Institute (WRI) 2026, Version 2 High Resolution Canopy Height Maps (CHMv2), source imagery for CHM © 2016 Vantor, licensed under CC BY 4.0. Where that is not yet in the database: ETH Global Canopy Height 2020, Lang, Jetz, Schindler and Wegner, ETH Zurich, CC BY 4.0. Both are reduced here to the height bands above, which changes them: they are not the original data.

Crediting AltimeterCloud

Free to use. If you use this API, or anything made from the answers, in a commercial product or service, credit AltimeterCloud with a link to www.altimetercloud.com at the point of use: on the screen, page or printout where the data appears, for example “Data from AltimeterCloud.com”. Any credits the data's own sources ask for, listed on this page, apply as well.