Requests and errors
Copy link

Where
Copy link

Pick exactly one. Every list and /cells route takes one of these; two at once is a 400 too_many_locations. Id filters (serves, parent, road, operator) are a where of their own and need none of these. There is no "inside a borough": send your own area as cells, a box or a radius.

ParameterTypeWhat it doesExample
nearlat,lngCentre of a circle. Needs radius_m. Places, stations, open spaces and road entries and exits come nearest first, each with distance_m.near=51.5307,-0.1238
radius_mmetresRadius of the circle around near, up to 50,000.radius_m=300
bboxsouth,west,north,eastA rectangle, the way a map viewport asks. Latitude first.bbox=51.522,-0.140,51.538,-0.110
cellsH3 ids, comma-separatedExactly these hexagons (resolution 9 unless resolution says otherwise), up to 100,000: typically a catchment from the TravelTime API. On /cells routes with group_by=cell and no sieve (within, beyond, min_population), every hexagon sent comes back, in the order sent, with 0, or null and a missing reason, where there is nothing. More than fit in a URL: POST the same fields as JSON. With group_by=all at resolution 9, cells may also be compacted (h3.compact_cells output, any resolution up to 9), up to 200,000 res-9 hexagons, each id counting as the hexagons inside it; the total counts each hexagon once.cells=89195da4d03ffff,89195da4d07ffff
countrycountry idThe whole country, only for searches that come back small: q name search, a country's boundaries on /boundaries, within and beyond on /proximity/cells, POST /custom-scores/cells, /categories.country=gb
atlat,lngThe one hexagon containing this point: on /places, the places in that hexagon; on /cells routes, that hexagon's row. On /boundaries, every boundary containing the point.at=51.5307,-0.1238

Paging
Copy link

Every list returns {results, next_cursor}. Pass next_cursor back to get the next page; null means you have everything. Three hexagons, two a page:

Request

curl -H "X-Application-Id: YOUR_APP_ID" -H "X-Api-Key: YOUR_APP_KEY" \
  "https://data.traveltimeapp.com/v1/boundaries/cells?cells=89195da4d03ffff,89195da4d07ffff,89195da4d8fffff&limit=2"

Response

{
  "resolution": 9,
  "results": [
    {
      "cell": "89195da4d03ffff",
      "boundaries": [
        { "id": "gb-st-pancras-somers-town", "name": "St Pancras & Somers Town", "type": "ward" },
        { "id": "gb-camden", "name": "Camden", "type": "borough" },
        { "id": "gb-greater-london", "name": "Greater London", "type": "region" },
        { "id": "gb-england", "name": "England", "type": "nation" },
        { "id": "gb", "name": "United Kingdom", "type": "country" }
      ]
    },
    "… 1 more on this page"
  ],
  "next_cursor": "eyJxIjoiNDlkNTUzN2QwODAyZDIzNzg1ZTJlMzRmIiwiayI6WzJdLCJmIjoiODk1ZmNhNTAwZDk3ZjNlNjA1MGYzN2M5NzgyNzVhZWUifQ.P4UU8l7JVc4LnrGaMmUt4A"
}

The same request plus that cursor returns the third hexagon and "next_cursor": null.

Request

curl -H "X-Application-Id: YOUR_APP_ID" -H "X-Api-Key: YOUR_APP_KEY" \
  "https://data.traveltimeapp.com/v1/boundaries/cells?cells=89195da4d03ffff,89195da4d07ffff,89195da4d8fffff&limit=2&cursor=$NEXT_CURSOR"
ParameterType · defaultWhat it doesExample
limitinteger · 100Rows per page, up to 10,000 (100 with include=bands, open_spaces, cells or stops). Asking for more is a 400, never a silent cut. It is a page size, not a cap: for the top 5 nearest, use limit=5 and ignore next_cursor.limit=500
cursoropaque string · —The next_cursor from the previous page. Send the same other parameters (or the same POST body) with it. A cursor belongs to one data build: after a data update it returns 410 cursor_expired, so pages never mix two builds.cursor=Q2sxN2hKdnVQ.x9Lw

One exception: an area search of POST /v1/custom-scores/cells has a result window of 10,000 hexagons. When more pass, its pages hold the top 10,000 by points (ties broken by h3), top-level matched counts every hexagon that passes, returned is at most 10,000, and next_cursor is null once the window is used up.

Per-hexagon routes
Copy link

One row per hexagon by default, or totals over what you sent: per boundary or for everything. Each row carries its cell, and a per-hexagon answer its resolution at the top level (not /custom-scores/cells); a totals row carries the boundary {id, name, type} it sums instead, and cell_count. Totals say how each field was added up in a rollup block: every field or block by name, no wildcards, with one of the rules under Totals.

ParameterType · defaultWhat it doesExample
group_bycell | boundary type | all · cellcell: one row per hexagon, up to 100,000. A boundary type (borough, ward…, listed on /catalog/{country}; not National Parks or National Landscapes), on places, demographics, neighbourhood and proximity at resolution 9: your hexagons split by the boundary they fall in, one row each. all: one row for everything you sent, up to 200,000 res-9 hexagons sent as cells (a box or circle stops at 100,000).group_by=borough
resolution6–10 · 9Hexagon size: 9 is about 0.1 km², each step down is 7× bigger. 6–10 on /places/cells and the transit /cells routes; 9 only on /boundaries/cells and /open-spaces/cells; the other /cells routes are 9 and take no resolution. Under any group_by, the resolution of the cells you send; with group_by=all at 9, cells may also be 0–8.resolution=8

Output
Copy link

Same data, different shape.

ParameterType · defaultWhat it doesExample
formatjson | geojson · jsongeojson returns a FeatureCollection: points for places and open spaces, hexagon outlines for /cells rows. Drops straight onto Mapbox, Leaflet or QGIS.format=geojson

Each response that carries sourced fields names its data sources once, in one top-level sources map. A field with no value says why in the row's missing: [{field, reason}], the only gap mechanism: nothing silently disappears.

Long requests
Copy link

Every route that takes cells also takes POST with the same fields as a JSON body, for long lists: up to 100,000 cells, or 200,000 res-9 hexagons' worth for a group_by=all total. To page a POST, send the same body plus cursor. All 233 of Camden's hexagons are too many for a URL, so they go in a POST body; two of them here, the second outside every source.

Request

curl -X POST -H "X-Application-Id: YOUR_APP_ID" -H "X-Api-Key: YOUR_APP_KEY" -H "Content-Type: application/json" \
  -d '{"cells": ["89194ad321bffff", "89195da4117ffff"]}' \
  "https://data.traveltimeapp.com/v1/neighbourhood/cells"

Response

{
  "resolution": 9,
  "results": [
    {
      "cell": "89194ad321bffff",
      "noise": { "max_lden_db": 80.3, "road_lden_db": 64.0 },
      "air_quality": { "no2_ugm3": 34.7, "pm25_ugm3": 11.0 },
      "flood": "…"
    },
    {
      "cell": "89195da4117ffff",
      "noise": null,
      "air_quality": null,
      "missing": [
        { "field": "noise", "reason": "outside_source_coverage" },
        { "field": "air_quality", "reason": "outside_source_coverage" },
        "… 4 more"
      ]
    }
  ],
  "next_cursor": null,
  "sources": { "noise": "defra_noise_2022_r4", "air_quality": "defra_pcm_2024" }
}

Totals
Copy link

Totals follow one rule per kind of field, named in every response's rollup block. Over a box around London, split by borough, places count up and proximity times average by population.

Request

curl -H "X-Application-Id: YOUR_APP_ID" -H "X-Api-Key: YOUR_APP_KEY" \
  "https://data.traveltimeapp.com/v1/places/cells?bbox=51.28,-0.51,51.69,0.33&include=categories&group_by=borough"
curl -H "X-Application-Id: YOUR_APP_ID" -H "X-Api-Key: YOUR_APP_KEY" \
  "https://data.traveltimeapp.com/v1/proximity/cells?bbox=51.28,-0.51,51.69,0.33&group_by=borough"

Response

{
  "resolution": 9,
  "results": [
    {
      "boundary": {
        "id": "gb-barking-and-dagenham",
        "name": "Barking and Dagenham",
        "type": "borough"
      },
      "cell_count": 259,
      "count": 1950,
      "category_counts": { "restaurant": 304, "bus_stop": 281, "hair_beauty": 141, "…": "…" }
    },
    "… 52 more"
  ],
  "next_cursor": null,
  "rollup": { "cell_count": "sum", "count": "sum", "category_counts": "sum" }
}
Kind of fieldRule
Counts, people, m²sum
Lines and open spaces (span many cells)distinct
Shares and rateshousehold_weighted_mean or population_weighted_mean, by their base
dB, times, ratingspopulation_weighted_mean (times over non-null hexagons)
Zonesmax
Shares of a total (covered_share)ratio_of_sums
Land cover sharesarea_weighted_mean
Deprivation decilesper_nation_population_share (never averaged)
open_spaces listsnone

Limits
Copy link

LimitValuePast it
Rows a page, default100Pass next_cursor back.
Rows a page, maximum (limit)10,000400 invalid_limit
Rows a page with include=bands, open_spaces, cells or stops100400 invalid_limit
Cells sent, or in a box or circle (except the 200,000 below)100,000400 too_many_cells
Hexagons in a per-hexagon answer (except /custom-scores/cells)100,000400 too_many_cells: use group_by for totals, or send the list in parts
Res-9 hexagons in a group_by=all total or on /custom-scores/cells200,000400 too_many_cells
Walk and drive times (within, beyond, covered_by)900 s400 invalid_within, invalid_beyond or invalid_covered_by, allowed_max 900
radius_m50,000 m400 invalid_radius_m
resolution on /places/cells and transit /cells6–10, default 9400 invalid_resolution with allowed
Homes on POST /v1/custom-scores1,000400 invalid_homes
Custom-score search window10,000 hexagonsPages stop; next_cursor is null
Requests per application, all Data API routes combined120 requests/minute. Contact us for more429 Too Many Requests

Errors
Copy link

Every error from the routes on the pages from Places to Custom scores has the same shape: a code to branch on, a message to show, the param at fault, an optional allowed list or allowed_max, and a doc_url into this page. HTTP 400 unless the table says otherwise. A slow /places/cells count, aggregation_read_budget_exceeded, has details in place of param and doc_url.

401 and 429 are not in the table. A missing or wrong X-Application-Id or X-Api-Key is 401 Unauthorized from every route. Its body is {"detail": "missing X-Application-Id or X-Api-Key header"} or {"detail": "invalid X-Application-Id or X-Api-Key"}, not the error shape above. Over the rate limit is 429 Too Many Requests; back off and retry.

The two category routes answer errors in an older shape, with no param or doc_url; Catalog and categories has them. So does every route not on the pages from Places to Custom scores; their codes are not on this page.

A resolution the dataset does not have, and a box with south above north:

Request

curl -H "X-Application-Id: YOUR_APP_ID" -H "X-Api-Key: YOUR_APP_KEY" \
  "https://data.traveltimeapp.com/v1/places/cells?bbox=51.522,-0.140,51.538,-0.110&resolution=12"
curl -H "X-Application-Id: YOUR_APP_ID" -H "X-Api-Key: YOUR_APP_KEY" \
  "https://data.traveltimeapp.com/v1/places?bbox=51.538,-0.140,51.522,-0.110"

Response

{
  "error": {
    "code": "invalid_resolution",
    "message": "places supports resolutions 6 to 10; got 12",
    "param": "resolution",
    "allowed": [6, 7, 8, 9, 10],
    "doc_url": "/docs/errors#invalid_resolution"
  }
}

Response

{
  "error": {
    "code": "invalid_bbox",
    "message": "bbox must be south,west,north,east with south<=north and west<=east",
    "param": "bbox",
    "doc_url": "/docs/errors#invalid_bbox"
  }
}
CodeHTTPparamWhen
Where
location_required400country · cells · —No near, bbox, cells or at (param is country where country would also do, none on /boundaries); q with no location and no country, or on /boundaries without country; /boundaries/cells without cells (param is cells).
too_many_locations400the second oneTwo ways of saying where at once.
invalid_radius_m400radius_mradius_m with bbox, cells or at but no near, near without radius_m, or outside 1 to 50,000 (allowed_max 50000).
invalid_cell_id400cellsNot H3 ids.
invalid_cell_resolution400cellsNot at resolution (9 unless sent); with group_by=all, a resolution finer than 9.
invalid_input400cellsAn empty cells list.
invalid_cells400cellsGeohash cells whose precision differs from precision.
too_many_cells400the location's: cells, bbox, near, country, within, beyond or includeMore than 100,000 hexagons (allowed_max 100000), or more than 200,000 res-9 hexagons sent as cells for a group_by=all total or /custom-scores/cells (allowed_max 200000); on /boundaries/{id}, include=cells on a boundary of more than 100,000. On the geohash grid, a box or circle of more than 10,000 geohashes (no allowed_max). Use group_by for totals, or send the list in parts.
invalid_country400countryNo country with that id. GET /v1/boundaries?type=country lists them.
country_not_allowed400countrycountry on a search that does not come back small.
out_of_coverage400bbox · near · at · cells · homesThe location touches no country we serve; is it lng,lat?
mixed_countries400homesHomes in more than one country; send each country's in a request of its own.
Paging
invalid_limit400limitlimit is 1 to 10,000 rows a page (allowed_max 10000), or 1 to 100 with include=bands, open_spaces, cells or stops (allowed_max 100).
invalid_cursor400cursorcursor is not one this API issued, or came with different parameters; pass next_cursor back unchanged with the same request.
cursor_expired410cursorThe data was rebuilt since this cursor was issued; start again without cursor.
Per-hexagon routes
invalid_resolution400resolutionOutside what the dataset has (allowed lists it); /boundaries/cells is resolution 9 only.
invalid_group_by400group_byNot cell, all, or a boundary type the country has (allowed lists them).
invalid_format400formatNot json or geojson; geojson with a group_by other than cell.
invalid_include400includeNot one of this route's includes; on /proximity/cells, include with a group_by other than cell.
covered_by_needs_group_by400covered_bycovered_by reports totals rows; add group_by=all or a boundary type.
Route filters
invalid_category400categoryUnknown category id; /categories has the tree.
grid_not_supported400gridgrid=geohash works with bbox, near and cells on /places and /places/cells.
invalid_precision400precisionGeohash precision is 6 or 7.
invalid_geohash400cellsNot geohashes, with grid=geohash.
mixed_precision400cellsGeohashes of more than one precision.
invalid_mode400modeNot a transit mode; allowed lists them.
invalid_serves400servesNot station ids, stations from two countries, or more than 1,000 (allowed_max 1000).
invalid_parent400parentNo boundary with that id.
invalid_type400typeNot a boundary type the country has; on /roads/access, not entry or exit.
invalid_layer400layerNot one of noise, air_quality, flood, crime, deprivation, broadband.
invalid_class400classNot an open-space class; /catalog lists them.
unknown_check_field400profile.checks.<key>.fieldNot a custom-score field; the message suggests the nearest one.
too_many_results400road · bbox · nearA roads search over 10,000 entries and exits in the location; narrow it.
Ids
invalid_id400id · roadNot an id of this kind (pl_, st_, ln_, os_, rd_, ra_).
not_found404id · countryNo place, station, line, road, entry or exit, open space, boundary or country with this id.
not_available_in_country404—This dataset is not loaded for the country.
Service
cells_unindexed503includeA boundary's hexagons are not indexed yet; retry after the next build.
field_unindexed503profile.checks.<key>.fieldThe field is on /catalog but not indexed for search yet.
aggregation_read_budget_exceeded400—A /places/cells count per hexagon took longer than the read budget allows; ask for fewer cells or a narrower category.
pt_read_budget_exceeded400—A transit /cells answer took longer to count than the read budget allows; send fewer cells.
pt_read_incomplete503—A transit /cells count did not finish; retry.

invalid_<param>: any other malformed parameter is named by its code — invalid_near, invalid_bbox, invalid_at, invalid_beyond, invalid_covered_by (the last two and invalid_within carry allowed_max 900 when a condition is malformed), and invalid_ plus the top-level field for a request-validation failure, with the dotted path as param (invalid_profile, profile.checks.gym.field); malformed JSON is invalid_body. message says why.

A 503 with any other code is the data store, not the request, and carries no param or doc_url. index_unavailable, opensearch_unavailable and opensearch_error clear on retry. opensearch_read_timeout and opensearch_request_rejected repeat until the request asks for less: send fewer cells or narrow the filters. <field>_unindexed (slug_unindexed, breadcrumb_ids_unindexed…) repeats until the country's next build.