Use with the TravelTime API
The TravelTime API calculates which H3 cells you can reach within a travel time. The Data API returns the data inside those cells. This guide shows how to connect the two.
What you need
- A TravelTime API application ID and key. Create a TravelTime account, then read the credentials from your account page.
- A Data API key from /signup.
Each API has its own key and its own rate limits. The Data API does not call the TravelTime
API for you. You make both requests and pass the cells across. The one exception is
POST /v1/search/reachable, described at the end of this page.
The TravelTime API also calculates travel times, routes and reachable areas. The public transport dataset in the Data API is built from the same timetable data, and you query it with your Data API key only.
1. Get reachable cells from /v4/h3/fast
POST /v4/h3/fast returns the H3 cells reachable within a travel time. Ask for
resolution 9. Every Data API dataset has resolution 9.
This request returns the cells reachable within 30 minutes of Trafalgar Square by public
transport, in a weekday morning. travel_time is in seconds. At resolution 9 the TravelTime
API allows up to 10,800 seconds, which is 3 hours.
bash
curl -X POST "https://api.traveltimeapp.com/v4/h3/fast" \
-H "Content-Type: application/json" \
-H "X-Application-Id: $TRAVELTIME_APP_ID" \
-H "X-Api-Key: $TRAVELTIME_API_KEY" \
-d '{
"resolution": 9,
"properties": ["mean"],
"arrival_searches": {
"one_to_many": [
{
"id": "public transport from Trafalgar Square",
"coords": { "lat": 51.507609, "lng": -0.128315 },
"transportation": { "type": "public_transport" },
"arrival_time_period": "weekday_morning",
"travel_time": 1800
}
]
}
}'The response has one entry in results per search. Each entry has a cells list. Each
cell has an id and a mean travel time in seconds. The
H3 Fast reference lists every
parameter, including the other transport types.
2. Send the cells to the Data API
Send the id values from step 1 as cells to POST /v1/h3/pois. We recommend the POST
form: a 30-minute search returns thousands of cells, and a GET query string has a length
limit.
This request sends two cells. Replace them with the cells from step 1.
bash
curl -X POST -H "Authorization: Bearer $POI_API_KEY" -H "Content-Type: application/json" \
-d '{"country": "GB", "cells": ["89195da494bffff", "89195da4947ffff"]}' \
"$POI_BASE/v1/h3/pois"The response has one row per cell with the number of places and the top three categories.
json
{
"country": "GB",
"country_resolved": true,
"resolution": 9,
"count": 2,
"limit": 10000,
"truncated": false,
"generated_at": "2026-09-21T10:19:20Z",
"cells": [
{
"h3": "89195da4947ffff",
"count": 109,
"top_categories": ["restaurant", "clothing_store", "bar"]
},
{
"h3": "89195da494bffff",
"count": 20,
"top_categories": ["uncategorised", "landmark", "bus_stop"]
}
]
}For a short list, GET /v1/h3/pois takes the same cells as a comma-separated cells
parameter.
bash
curl -H "Authorization: Bearer $POI_API_KEY" \
"$POI_BASE/v1/h3/pois?country=GB&cells=89195da494bffff,89195da4947ffff"3. Query more datasets for the same cells
Every /v1/h3/* endpoint takes the same cells list. Send the same list to each one and
the answers cover the same area.
| Endpoint | What it returns for each cell |
|---|---|
GET /v1/h3/pois | Number of places and the top three categories |
GET /v1/h3/pois/records | The places themselves |
GET /v1/h3/population | Residents, and census demographics where the country publishes them |
GET /v1/h3/land-cover | Named green spaces and their area |
GET /v1/h3/environment | Noise, air quality, flood risk, deprivation, broadband and crime |
GET /v1/h3/transport/summary | Number of stations and lines, by mode |
GET /v1/h3/transit-coverage | Whether the cell is within a 15-minute walk of a station, and its population |
This request returns the population of the same two cells.
bash
curl -H "Authorization: Bearer $POI_API_KEY" \
"$POI_BASE/v1/h3/population?country=GB&cells=89195da494bffff,89195da4947ffff"All of them return the same envelope: country, resolution, count, limit,
truncated, generated_at and cells. H3 response format
explains each field.
Large areas
POST /v1/h3/population/aggregate totals the population of up to 100,000 cells in one
request, or 10,000 cells with include_per_cell=true. A long catchment can be sent compacted; the
population page gives the rules.
bash
curl -X POST -H "Authorization: Bearer $POI_API_KEY" -H "Content-Type: application/json" \
-d '{"country": "GB", "cells": ["89195da494bffff", "89195da4947ffff"]}' \
"$POI_BASE/v1/h3/population/aggregate"For an administrative area, send division_id instead of cells. The server uses the
area's own cells, so the list never leaves the server. GET /v1/divisions lists the areas.
See Population and areas.
Reachable search in one request
POST /v1/search/reachable does steps 1 and 2 in one call. Send a point, a transport
mode and travel_time_minutes. You get back places ordered by travel time.
transport is walking, cycling, driving or public_transport. travel_time_minutes
is 1 to 180. category_group takes any id from GET /v1/categories.
bash
curl -X POST -H "Authorization: Bearer $POI_API_KEY" -H "Content-Type: application/json" \
-d '{"lat": 51.5074, "lng": -0.1278, "transport": "public_transport", "travel_time_minutes": 30, "category_group": "cafe", "limit": 5}' \
"$POI_BASE/v1/search/reachable"Each place in pois has a travel_time_seconds. cell_count is the number of reachable
cells and matched_count the number of places inside them.
This endpoint calls the TravelTime API with credentials we hold, so it needs a Data API key
with TravelTime access. Without it the response is 403. It is not in the API reference.
Contact us to enable it.
Cells for a map
Send "include_cells": true to get the reachable cells back with the places. cells has
two arrays of the same length: ids holds the cell ids and means the mean travel time to
each cell in seconds, so cells.means[i] belongs to cells.ids[i]. mode is geohash (the
default) or h3, and resolution sets the cell size; leave resolution out for
the default. cells stops at 50,000 cells and cells_truncated is true when it did;
cell_count is always the full total.
json
{
"cell_count": 1843,
"cells": { "ids": ["gcpvj0", "gcpvj1"], "means": [300, 420] },
"cells_truncated": false
}Stream the reply
The cells come back from the TravelTime API in 50 to 300 ms. Reading the places takes
another 0.3 to 1 s. Send Accept: application/x-ndjson to get each part as soon as it is
ready. The response is Content-Type: application/x-ndjson: two lines of JSON, each ending
in \n and flushed on its own, so a map can draw the reachable area before the places
arrive.
The first line has what the cells give: country, mode, resolution, cell_count,
cells (with include_cells), cells_truncated and a timing with the country and
traveltime stages. The second has the rest: matched_count, points, pois and the full
timing. Merged, the two lines are the object the plain JSON reply gives.
bash
curl -N -X POST -H "Authorization: Bearer $POI_API_KEY" -H "Content-Type: application/json" \
-H "Accept: application/x-ndjson" \
-d '{"lat": 51.5074, "lng": -0.1278, "transport": "walking", "travel_time_minutes": 15, "include_cells": true, "limit": 5}' \
"$POI_BASE/v1/search/reachable"-N turns off curl's output buffering so the first line prints when it arrives.
An error before the first line is an ordinary error response with its usual status. An
error after it, when the TravelTime step succeeded and the places read did not, cannot
change the status any more, so it arrives as a final line: {"error": {"code": "...", "message": "..."}}. Check each line for error before reading it as data.
The reply is gzip-compressed like every other and each line is flushed whole, so a
streaming reader needs nothing extra. Read the body with getReader() and split on \n; a
chunk may end in the middle of a line.
ts
const controller = new AbortController() // abort() when the user moves the origin
const response = await fetch(`${POI_BASE}/v1/search/reachable`, {
method: 'POST',
headers: {
Authorization: `Bearer ${POI_API_KEY}`,
'Content-Type': 'application/json',
Accept: 'application/x-ndjson',
},
body: JSON.stringify({ lat, lng, transport, travel_time_minutes, include_cells: true }),
signal: controller.signal,
})
if (!response.ok) throw await response.json() // same statuses as the plain reply
const reader = response.body!.getReader()
const decoder = new TextDecoder()
let buffer = ''
for (;;) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
let newline = buffer.indexOf('\n')
while (newline >= 0) {
const line = JSON.parse(buffer.slice(0, newline))
buffer = buffer.slice(newline + 1)
if (line.error) throw new Error(line.error.message)
if (line.cells) drawCells(line.cells.ids, line.cells.means) // line 1: the reachable area
if (line.pois) showPlaces(line.pois, line.matched_count) // line 2: the places
newline = buffer.indexOf('\n')
}
}