Skip to content

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

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.

EndpointWhat it returns for each cell
GET /v1/h3/poisNumber of places and the top three categories
GET /v1/h3/pois/recordsThe places themselves
GET /v1/h3/populationResidents, and census demographics where the country publishes them
GET /v1/h3/land-coverNamed green spaces and their area
GET /v1/h3/environmentNoise, air quality, flood risk, deprivation, broadband and crime
GET /v1/h3/transport/summaryNumber of stations and lines, by mode
GET /v1/h3/transit-coverageWhether 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')
  }
}