Inland Water 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 2-bit classes 120 an hour without a key, 400 with one

Where the water is over a square around a point, and what kind: every cell of about 10 metres comes back as general water (lakes, reservoirs, ponds, rivers, canals and the sea along the coast), a stream or beck, a ditch or drain, or dry, the same data the Inland Water 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, so a launch site's water can be built into firmware rather than fetched on the day.

GEThttps://www.altimetercloud.com/api/water/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.
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) with a 64 KB dictionary, exactly as the Tree Canopy API gives it.
classesOptional. The classes to keep, as a list such as 1,2; every other class comes back as 0. Left out, all three.
downloadOptional. 1 makes a browser save a format=bin answer as a file, named for the square.

The classes

ClassWhatFrom
0Dry
1General waterESA WorldCover's permanent water; OpenStreetMap's lakes, ponds, reservoirs, marina basins and river areas; rivers and canals mapped as lines; the sea along the coast
2Streams and becksOpenStreetMap waterway=stream
3Ditches and drainsOpenStreetMap waterway=drain and ditch

Any cell with water in it at all takes its class, however little: a stream a metre wide comes back as an unbroken line of cells. That is meant for hazard checks, where a cell with any water in it is a place something can land wet. Where two classes meet in a cell the worse is kept, 1 over 2 over 3, so a stream running into a lake is part of the lake. Many ditches and drains are dry for much of the year, and streams and drains that pass through culverts may show as open where they are not; classes=1,2 leaves ditches and drains out.

The satellite part gives the same quality worldwide; the OpenStreetMap part fills in what the satellite cannot see, such as shaded rivers, crowded marinas and anything narrower than a cell, but only has what has been mapped. The satellite water is as it stood in 2021.

The cells

The grid is the water store's own, so nothing is resampled: each cell is exactly 1/12000 of a degree each way, about 9.3 m north to south and, at UK latitudes, about 5.5 m east to west. Row 0 is the north edge and column 0 the west edge; rows run south and columns east. Each cell is two bits, its class, the cells laid end to end along the row with the first cell in the highest bits, so four cells fill each byte. Every row starts on a whole byte, so row j starts at byte j * row_bytes.

Ground not yet in the database is 0 as well; the JSON answer's coverage_pct says how much of the square the database holds, water_pct how much of it is water of the classes kept, and class_pct how much is each class. Out at sea, beyond the WorldCover tiles, there is no water data at all: the open sea comes from the terrain, ground at or below 0 m.

Sizes

At UK latitudes (54°N), before compression:

SquareCellsFile
1 km108 × 1865.0 KB
3 km324 × 55644 KB
5 km540 × 926122 KB
10 km1,078 × 1,850487 KB
20 km2,156 × 3,7001.9 MB
30 km3,234 × 5,5504.3 MB

Most ground is dry, so compression takes these down a long way: a square with a few lakes and streams in it usually packs to a small fraction of its raw size, and lzma is the smallest.

The binary file

With format=bin: a 32 byte header, little endian, then the rows, north first. It is the same layout as the tree canopy and roads files, so one reader handles all three; only the name, the version and the bits a cell differ.

ByteTypeValue
0char[4]ACWT
4uint8Version, 2
5uint8Bits per cell, 2
6uint16Cells per degree, 12000
8int32North edge in cells: its latitude × 12000
12int32West edge in cells: its longitude × 12000
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 * 12000), column floor(lon * 12000) - west_edge.

Examples

https://www.altimetercloud.com/api/water/?lat=54.35&lon=-2.95&size_km=3
https://www.altimetercloud.com/api/water/?lat=54.35&lon=-2.95&size_km=5&classes=1,2&format=bin&compress=zlib
https://www.altimetercloud.com/api/water/?lat=54.35&lon=-2.95&size_km=10&format=bin&compress=lzma&download=1

The JSON answer

{
  "success": true,
  "lat", "lon", "size_km", "bits",           bits is 2
  "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",
  "water_pct",                               cells with water of the classes kept
  "class_pct": { "1": ..., "2": ... },       each class kept
  "classes": { "1": "general water", ... },
  "kept": [1, 2, 3],                         the classes asked for
  "coverage_pct",                            cells the database holds
  "meaning", "order", "compress", "bytes",
  "data_b64",                                the rows, row 0 first, compressed if asked
  "file_name", "seconds",
  "credits": { "water", "source", "doi", "licence", "full" }
}

Reading the cells

On a device: what water is below me

Give a position and get the class of the cell it is in. 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.

// C
typedef struct __attribute__((packed)) {
  char     magic[4];        // "ACWT" (roads "ACRD", trees "ACTR")
  uint8_t  version;         // 2
  uint8_t  bits;            // 2
  uint16_t cells_per_deg;   // 12000
  int32_t  north_edge;      // latitude of the north edge * 12000
  int32_t  west_edge;       // longitude of the west edge * 12000
  uint32_t rows, cols, row_bytes, reserved;
} GridHeader;

// the class at a position, 0 to 3, or -1 outside the square
int waterClassAt(const GridHeader *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;
  uint8_t b = cells[(uint32_t)j * h->row_bytes + (i >> 2)];
  return (b >> (6 - 2 * (i & 3))) & 3;          // four cells a byte, first in the top bits
}

Water near me

For landing, walk the cells whose middles lie within a radius, working in metres from the cell size: 111320 / 12000 m north to south and that times the cosine of the latitude west to east. The worst class in the circle, the share of cells that are water, and the smallest distance to each class all come from one pass.

double ch = 111320.0 / h->cells_per_deg, cw = ch * cos(lat * M_PI / 180.0);
int ny = (int)(radius_m / ch) + 1, nx = (int)(radius_m / cw) + 1, wet = 0, all = 0;
double nearest[4] = { -1, -1, -1, -1 };             // by class
for (int dy = -ny; dy <= ny; dy++)
  for (int dx = -nx; dx <= nx; dx++) {
    double d = sqrt((dx * cw) * (dx * cw) + (dy * ch) * (dy * ch));
    if (d > radius_m) continue;
    int c = waterClassAt(h, cells, lat + dy / (double)h->cells_per_deg, lon + dx / (double)h->cells_per_deg);
    if (c < 0) continue;
    all++;
    if (c) { wet++; if (nearest[c] < 0 || d < nearest[c]) nearest[c] = d; }
  }

Unpacking on a microcontroller

The compression is the same as the tree canopy files': compress=lzma unpacks with LzmaDecode() from the LZMA SDK, and on an ESP32 compress=zlib needs nothing extra, as miniz's tinfl is in its ROM. See Unpacking on a microcontroller on the Tree Canopy page.

On a computer

# Python
import requests, struct, zlib, numpy as np
r = requests.get('https://www.altimetercloud.com/api/water/',
                 params={'lat': 54.35, 'lon': -2.95, '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])
b = np.unpackbits(np.frombuffer(f[32:], np.uint8).reshape(rows, rb), axis=1)[:, :cols * 2]
water = (b[:, 0::2] << 1) | b[:, 1::2]      # rows x cols, 0 dry, 1 water, 2 stream, 3 ditch
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, JSON was asked for over 10 km, or classes= is not a list of 1 to 3
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 Inland Water Map has a download panel: move the map so the cross sits on your launch site, choose the size and compression, and it saves the file from this API.

Water data credits

© ESA WorldCover project 2021 / Contains modified Copernicus Sentinel data (2021) processed by ESA WorldCover consortium. ESA WorldCover 10 m 2021 v200, Zanaga, D. et al. 2022, doi:10.5281/zenodo.7254221, licensed under CC BY 4.0.

Contains OpenStreetMap data © OpenStreetMap contributors, available under the Open Database Licence (ODbL).

The two are combined here and reduced to a water class a cell, which changes them: this is not the original data of either.

Licence of this data. Because it contains OpenStreetMap data, the data this API gives out is made available under the Open Database License (ODbL) 1.0. You may use it freely, commercially too, as long as you credit it as above and share any database you make from it under the same licence.

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.