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 minutes | Hour | Day | |
|---|---|---|---|
| Without a key | 20 | 120 | 500 |
| With a key | 60 | 400 | 2,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.
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.
lat lon size_kmParameters
| Parameter | Meaning |
|---|---|
lat, lon | The middle of the square, decimal degrees (lng also accepted) |
size_km | The 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. |
bits | Optional. 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. |
format | Optional. 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. |
compress | Optional. 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. |
download | Optional. 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 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.
bits | Per cell | Packing |
|---|---|---|
4 | Height band 0 to 15 | Two cells a byte, the first in the high four bits |
1 | 1 = trees of 3 m or more, 0 = none | Eight cells a byte, the first in the highest bit |
The height bands, lowest height in each:
| Band | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| From | none | 3 m | 5 m | 7 m | 10 m | 13 m | 16 m | 19 m | 22 m | 26 m | 30 m | 35 m | 40 m | 46 m | 53 m | 61 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:
| Square | Cells | 4-bit | 1-bit |
|---|---|---|---|
| 1 km | 217 × 368 | 39 KB | 10 KB |
| 3 km | 648 × 1,102 | 349 KB | 87 KB |
| 5 km | 1,079 × 1,835 | 967 KB | 242 KB |
| 10 km | 2,157 × 3,669 | 3.8 MB | 967 KB |
| 20 km | 4,313 × 7,337 | 15.1 MB | 3.8 MB |
| 30 km | 6,469 × 11,005 | 33.9 MB | 8.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:
| Square | 4-bit, height bands | 1-bit, tree or not | ||||
|---|---|---|---|---|---|---|
| Raw | zlib | lzma | Raw | zlib | lzma | |
| 1 km | 40 KB | 13 KB | 13 KB | 10 KB | 4 KB | 4 KB |
| 3 km | 352 KB | 90 KB | 82 KB | 88 KB | 28 KB | 23 KB |
| 5 km | 967 KB | ~250 KB | ~230 KB | 242 KB | ~70 KB | ~60 KB |
| 10 km | 3.8 MB | 1.0 MB | 0.9 MB | 967 KB | 268 KB | 217 KB |
| 20 km | 15.1 MB | ~4 MB | ~3.6 MB | 3.8 MB | ~1 MB | ~0.85 MB |
| 30 km | 33.9 MB | ~9 MB | ~8 MB | 8.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.
| Byte | Type | Value |
|---|---|---|
| 0 | char[4] | ACTR |
| 4 | uint8 | Version, 1 |
| 5 | uint8 | Bits per cell, 1 or 4 |
| 6 | uint16 | Cells per degree, 24000 |
| 8 | int32 | North edge in cells: its latitude × 24000 |
| 12 | int32 | West edge in cells: its longitude × 24000 |
| 16 | uint32 | Rows |
| 20 | uint32 | Columns |
| 24 | uint32 | Bytes per row |
| 28 | uint32 | 0, 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.
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.
| Code | Meaning |
|---|---|
412 | A parameter is missing or not valid, the square is more than 30 km, or JSON was asked for over 10 km |
413 | The square has too many cells, which only happens very close to the poles |
429 | Too many requests from your address, or one still being answered |
501 | The 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.



















