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.
| Parameter | Type | What it does | Example |
|---|---|---|---|
near | lat,lng | Centre 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_m | metres | Radius of the circle around near, up to 50,000. | radius_m=300 |
bbox | south,west,north,east | A rectangle, the way a map viewport asks. Latitude first. | bbox=51.522,-0.140,51.538,-0.110 |
cells | H3 ids, comma-separated | Exactly 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 |
country | country id | The 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 |
at | lat,lng | The 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 |
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"| Parameter | Type · default | What it does | Example |
|---|---|---|---|
limit | integer · 100 | Rows 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 |
cursor | opaque 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.
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.
| Parameter | Type · default | What it does | Example |
|---|---|---|---|
group_by | cell | boundary type | all · cell | cell: 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 |
resolution | 6–10 · 9 | Hexagon 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 |
Same data, different shape.
| Parameter | Type · default | What it does | Example |
|---|---|---|---|
format | json | geojson · json | geojson 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.
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 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 field | Rule |
|---|---|
| Counts, people, m² | sum |
| Lines and open spaces (span many cells) | distinct |
| Shares and rates | household_weighted_mean or population_weighted_mean, by their base |
| dB, times, ratings | population_weighted_mean (times over non-null hexagons) |
| Zones | max |
Shares of a total (covered_share) | ratio_of_sums |
| Land cover shares | area_weighted_mean |
| Deprivation deciles | per_nation_population_share (never averaged) |
open_spaces lists | none |
| Limit | Value | Past it |
|---|---|---|
| Rows a page, default | 100 | Pass next_cursor back. |
Rows a page, maximum (limit) | 10,000 | 400 invalid_limit |
Rows a page with include=bands, open_spaces, cells or stops | 100 | 400 invalid_limit |
| Cells sent, or in a box or circle (except the 200,000 below) | 100,000 | 400 too_many_cells |
| Hexagons in a per-hexagon answer (except /custom-scores/cells) | 100,000 | 400 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/cells | 200,000 | 400 too_many_cells |
Walk and drive times (within, beyond, covered_by) | 900 s | 400 invalid_within, invalid_beyond or invalid_covered_by, allowed_max 900 |
radius_m | 50,000 m | 400 invalid_radius_m |
resolution on /places/cells and transit /cells | 6–10, default 9 | 400 invalid_resolution with allowed |
Homes on POST /v1/custom-scores | 1,000 | 400 invalid_homes |
| Custom-score search window | 10,000 hexagons | Pages stop; next_cursor is null |
| Requests per application, all Data API routes combined | 120 requests/minute. Contact us for more | 429 Too Many Requests |
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"
}
}| Code | HTTP | param | When |
|---|---|---|---|
| Where | |||
location_required | 400 | country · 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_locations | 400 | the second one | Two ways of saying where at once. |
invalid_radius_m | 400 | radius_m | radius_m with bbox, cells or at but no near, near without radius_m, or outside 1 to 50,000 (allowed_max 50000). |
invalid_cell_id | 400 | cells | Not H3 ids. |
invalid_cell_resolution | 400 | cells | Not at resolution (9 unless sent); with group_by=all, a resolution finer than 9. |
invalid_input | 400 | cells | An empty cells list. |
invalid_cells | 400 | cells | Geohash cells whose precision differs from precision. |
too_many_cells | 400 | the location's: cells, bbox, near, country, within, beyond or include | More 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_country | 400 | country | No country with that id. GET /v1/boundaries?type=country lists them. |
country_not_allowed | 400 | country | country on a search that does not come back small. |
out_of_coverage | 400 | bbox · near · at · cells · homes | The location touches no country we serve; is it lng,lat? |
mixed_countries | 400 | homes | Homes in more than one country; send each country's in a request of its own. |
| Paging | |||
invalid_limit | 400 | limit | limit 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_cursor | 400 | cursor | cursor is not one this API issued, or came with different parameters; pass next_cursor back unchanged with the same request. |
cursor_expired | 410 | cursor | The data was rebuilt since this cursor was issued; start again without cursor. |
| Per-hexagon routes | |||
invalid_resolution | 400 | resolution | Outside what the dataset has (allowed lists it); /boundaries/cells is resolution 9 only. |
invalid_group_by | 400 | group_by | Not cell, all, or a boundary type the country has (allowed lists them). |
invalid_format | 400 | format | Not json or geojson; geojson with a group_by other than cell. |
invalid_include | 400 | include | Not one of this route's includes; on /proximity/cells, include with a group_by other than cell. |
covered_by_needs_group_by | 400 | covered_by | covered_by reports totals rows; add group_by=all or a boundary type. |
| Route filters | |||
invalid_category | 400 | category | Unknown category id; /categories has the tree. |
grid_not_supported | 400 | grid | grid=geohash works with bbox, near and cells on /places and /places/cells. |
invalid_precision | 400 | precision | Geohash precision is 6 or 7. |
invalid_geohash | 400 | cells | Not geohashes, with grid=geohash. |
mixed_precision | 400 | cells | Geohashes of more than one precision. |
invalid_mode | 400 | mode | Not a transit mode; allowed lists them. |
invalid_serves | 400 | serves | Not station ids, stations from two countries, or more than 1,000 (allowed_max 1000). |
invalid_parent | 400 | parent | No boundary with that id. |
invalid_type | 400 | type | Not a boundary type the country has; on /roads/access, not entry or exit. |
invalid_layer | 400 | layer | Not one of noise, air_quality, flood, crime, deprivation, broadband. |
invalid_class | 400 | class | Not an open-space class; /catalog lists them. |
unknown_check_field | 400 | profile.checks.<key>.field | Not a custom-score field; the message suggests the nearest one. |
too_many_results | 400 | road · bbox · near | A roads search over 10,000 entries and exits in the location; narrow it. |
| Ids | |||
invalid_id | 400 | id · road | Not an id of this kind (pl_, st_, ln_, os_, rd_, ra_). |
not_found | 404 | id · country | No place, station, line, road, entry or exit, open space, boundary or country with this id. |
not_available_in_country | 404 | — | This dataset is not loaded for the country. |
| Service | |||
cells_unindexed | 503 | include | A boundary's hexagons are not indexed yet; retry after the next build. |
field_unindexed | 503 | profile.checks.<key>.field | The field is on /catalog but not indexed for search yet. |
aggregation_read_budget_exceeded | 400 | — | A /places/cells count per hexagon took longer than the read budget allows; ask for fewer cells or a narrower category. |
pt_read_budget_exceeded | 400 | — | A transit /cells answer took longer to count than the read budget allows; send fewer cells. |
pt_read_incomplete | 503 | — | 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.