Your rules, our data. You define what matters in JSON (checks, how much each counts, which are must-haves) and we work it out for every home or hexagon, returning every value, whether it was met, and your points. Custom scores never decide what a good place is: no personas, no built-in model. A check reads one field of another dataset's row for the hexagon (Proximity, Neighbourhood or Open spaces), so it is as fresh as that dataset; custom_scores.fields on /v1/catalog/{country} lists every field a check can read.
Questions this page answers: Does this home have everything this buyer needs? · Where in the UK passes all of my must-haves? · Which of my client's rules does this listing meet?
Required authentication header. The same application id as the TravelTime API.
Required authentication header.
POST /v1/custom-scoresCheck a list of homes against a profile you define. One row per home, in the order sent. There is no GET: the profile is a JSON body.
A buyer's own list in JSON: what they need, how much each matters, and one must-have. The Euston flat gets 8 of 10 points but is noisy (81.3 dB). The Milton Keynes house gets 2 of 10: two parks of 5 ha or more within 15 minutes, but the nearest supermarket is a 12-minute walk, one gym in 10 minutes and a transit rating of 21. Both pass the must (no flood zone). Every value comes back, so your app can show why.
Request
curl -X POST -H "X-Application-Id: YOUR_APP_ID" -H "X-Api-Key: YOUR_APP_KEY" -H "Content-Type: application/json" \
-d '{
"homes": [
{"id": "listing-1", "lat": 51.5288, "lng": -0.1309},
{"id": "listing-2", "lat": 52.023, "lng": -0.77}
],
"profile": {
"checks": {
"supermarket": {"field": "proximity.walk_s.supermarket", "max": 600, "weight": 3, "label": "Supermarket in 10 min"},
"quiet": {"field": "neighbourhood.noise.max_lden_db", "max": 55, "weight": 2, "label": "Quiet street"},
"gyms": {"field": "proximity.bands.walk.600.category_counts.gym", "min": 3, "weight": 1, "label": "3+ gyms in 10 min"},
"no_flood": {"field": "neighbourhood.flood.zone", "max": 1, "must": true, "label": "Not in a flood zone"},
"transit": {"field": "proximity.ratings.transit", "min": 70, "weight": 2, "label": "Good public transport"},
"big_park": {"field": "proximity.bands.walk.900.over_5ha_count", "min": 1, "weight": 2, "label": "A park of 5+ ha in 15 min"}
}
}
}' \
"https://data.traveltimeapp.com/v1/custom-scores"Shade = checks.supermarket.value2377422 hexagons; a hexagon opens its row.
| Hexagoncell | Checks supermarket metchecks.supermarket.met | Checks supermarket valuechecks.supermarket.value | Checks quiet metchecks.quiet.met | Map |
|---|---|---|---|---|
| 89195da4d03ffff | met | 237 | not met | |
| 89195d88a97ffff | not met | 742 | not met |
All 2 shown
Response
{
"results": [
{
"id": "listing-1",
"cell": "89195da4d03ffff",
"checks": {
"supermarket": {
"met": true,
"value": 237
},
"quiet": {
"met": false,
"value": 81.3
},
"gyms": {
"met": true,
"value": 12
},
"no_flood": {
"met": true,
"value": 1
},
"transit": {
"met": true,
"value": 100
},
"big_park": {
"met": true,
"value": 1
}
},
"musts_met": true,
"score": {
"points": 8,
"out_of": 10,
"share": 0.8
}
},
"… 1 more row"
],
"next_cursor": null
}Leave out the weights and you get just met or not met, with the values. No score at all.
Request
curl -X POST -H "X-Application-Id: YOUR_APP_ID" -H "X-Api-Key: YOUR_APP_KEY" -H "Content-Type: application/json" \
-d '{
"homes": [{"id": "listing-1", "lat": 51.5288, "lng": -0.1309}],
"profile": {
"checks": {
"supermarket": {"field": "proximity.walk_s.supermarket", "max": 600, "label": "Supermarket in 10 min"},
"quiet": {"field": "neighbourhood.noise.max_lden_db", "max": 55, "label": "Quiet street"},
"gyms": {"field": "proximity.bands.walk.600.category_counts.gym", "min": 3, "label": "3+ gyms in 10 min"},
"no_flood": {"field": "neighbourhood.flood.zone", "max": 1, "label": "Not in a flood zone"},
"transit": {"field": "proximity.ratings.transit", "min": 70, "label": "Good public transport"}
}
}
}' \
"https://data.traveltimeapp.com/v1/custom-scores"Shade = checks.supermarket.value2372371 hexagon; it opens its row.
| Hexagoncell | Checks supermarket metchecks.supermarket.met | Checks supermarket valuechecks.supermarket.value | Checks quiet metchecks.quiet.met | Map |
|---|---|---|---|---|
| 89195da4d03ffff | met | 237 | not met |
All 1 shown
Response
{
"results": [
{
"id": "listing-1",
"cell": "89195da4d03ffff",
"checks": {
"supermarket": {
"met": true,
"value": 237
},
"quiet": {
"met": false,
"value": 81.3
},
"gyms": {
"met": true,
"value": 12
},
"no_flood": {
"met": true,
"value": 1
},
"transit": {
"met": true,
"value": 100
}
},
"musts_met": true
}
],
"next_cursor": null
}| Parameter | Type · default | What it does | Example |
|---|---|---|---|
homes | [{id, lat, lng}] · — | 1 to 1,000 homes, all in one country. id is yours and comes back unchanged. | {"id": "listing-1", "lat": 51.5288, "lng": -0.1309} |
profile.checks | {key: check} · — | 1 to 50 checks, keyed by a name of your choice that the row answers under. One profile per request. | "quiet": {"field": "neighbourhood.noise.max_lden_db", "max": 55, "weight": 2} |
Each check:
| Key | Type · default | What it does |
|---|---|---|
field | field · — | Exactly as rows name it, <dataset>.<path in the row>, e.g. proximity.walk_s.supermarket. Anything not in custom_scores.fields on /catalog is a 400. |
min | number · — | Met when the value is at least this. |
max | number · — | Met when the value is at most this. A check needs min, max or both. |
weight | whole number 1–1000 · — | Points when met. Leave every weight out and you get met or not met, with no score. |
must | true | false · false | A dealbreaker: musts_met is false when it fails, and an area search leaves the hexagon out. |
label | text · — | For your UI; not returned. |
Also takes Paging: limit, cursor, in the body. A page is 100 rows by default; limit goes up to 10,000, or 100 when a check reads a proximity.bands. field.
Each row: id, cell (the resolution-9 hexagon holding the home), checks keyed as you keyed them, each {met, value} with the value in the field's own unit (seconds, dB, a count, a share); a null value is never met and carries the reason the field's own row lists it under in missing (no_residents, over_900_s…) when there is one; musts_met; and score {points, out_of, share} when any check has a weight, still returned when a must fails.
| Code | HTTP | When |
|---|---|---|
invalid_homes | 400 | No homes, more than 1,000, or a lat or lng out of range. |
invalid_profile | 400 | The profile breaks a bound in the tables above. |
unknown_check_field | 400 | A field no row names; the message suggests the nearest one when one is close, and allowed lists every field. |
out_of_coverage | 400 | A home is in no country we serve, or in one custom scores are not built for yet; param is homes. |
mixed_countries | 400 | Homes in more than one country: send each country's homes in a request of its own. |
invalid_limit | 400 | limit below 1 or over 10,000, or over 100 when a check reads a proximity.bands. field. |
| Every Paging code | ||
POST /v1/custom-scores/cellsThe same profile as a search. An area search (near, bbox or country) returns only the hexagons where every must holds, most points first, ties broken by h3, and stops at the top 10,000: next_cursor pages through them and is null at the end of the window. With at or cells, every hexagon sent comes back once, in the order sent, musts met or not. In a country custom scores are not built for yet, a search returns no rows (matched 0).
The same rules as a search over the whole country: 326,487 hexagons pass the must, so you get the top 10,000 by points, 100 a page.
Request
curl -X POST -H "X-Application-Id: YOUR_APP_ID" -H "X-Api-Key: YOUR_APP_KEY" -H "Content-Type: application/json" \
-d '{
"country": "gb",
"profile": {
"checks": {
"supermarket": {"field": "proximity.walk_s.supermarket", "max": 600, "weight": 3, "label": "Supermarket in 10 min"},
"quiet": {"field": "neighbourhood.noise.max_lden_db", "max": 55, "weight": 2, "label": "Quiet street"},
"gyms": {"field": "proximity.bands.walk.600.category_counts.gym", "min": 3, "weight": 1, "label": "3+ gyms in 10 min"},
"no_flood": {"field": "neighbourhood.flood.zone", "max": 1, "must": true, "label": "Not in a flood zone"},
"transit": {"field": "proximity.ratings.transit", "min": 70, "weight": 2, "label": "Good public transport"},
"big_park": {"field": "proximity.bands.walk.900.over_5ha_count", "min": 1, "weight": 2, "label": "A park of 5+ ha in 15 min"}
}
}
}' \
"https://data.traveltimeapp.com/v1/custom-scores/cells"Shade = checks.supermarket.value174592100 hexagons; a hexagon opens its row.
| Hexagoncell | Checks supermarket metchecks.supermarket.met | Checks supermarket valuechecks.supermarket.value | Checks quiet metchecks.quiet.met | Map |
|---|---|---|---|---|
| 891874ac393ffff | met | 387 | met | |
| 89194ad1d03ffff | met | 390 | met | |
| 89194e87387ffff | met | 336 | met | |
| 8919588542fffff | met | 386 | met | |
| 8919588c01bffff | met | 216 | met |
5 of the first 100 shown · next_cursor fetches the next page
Response
{
"matched": 326487,
"returned": 10000,
"results": [
{
"cell": "891874ac393ffff",
"checks": {
"supermarket": {
"met": true,
"value": 387
},
"quiet": {
"met": true,
"value": 54.38
},
"gyms": {
"met": true,
"value": 3
},
"no_flood": {
"met": true,
"value": 1
},
"transit": {
"met": true,
"value": 70
},
"big_park": {
"met": true,
"value": 2
}
},
"musts_met": true,
"score": {
"points": 10,
"out_of": 10,
"share": 1
}
},
"… 99 more rows"
],
"next_cursor": "eyJxIjoiZDQyNGVjYWM0NmVmZDQxZmYxMmNiOWE4IiwiayI6WzkuMCwiODkxOTA5YWM0NGJmZmZmIiwxMDBdLCJmIjoiZWMwNmU2MTYwZWE3MDdiZDk2ODllMGZlNTBhY2QwMDkifQ.jhsquKMv7YKFbunVp44kvA"
}| Parameter | Type · default | What it does | Example |
|---|---|---|---|
profile | {checks: …} · — | The same profile as POST /v1/custom-scores, as above. | {"checks": {...}} |
Also takes Where: near, radius_m, bbox, cells, at, country and Paging: limit, cursor, in the body; cells as a JSON list of resolution-9 hexagons, or compacted ids of any resolution up to 9, read as the res-9 hexagons inside them, up to 200,000.
Each row: cell, checks, musts_met and score, as above, except that only a proximity field's null carries a reason. Top level: matched, the hexagons passing every must (with no must, every hexagon we hold data for in the area, or every hexagon sent with at or cells), and returned, the rows in all: the smaller of matched and 10,000 for an area search; the hexagons sent with at or cells.
| Code | HTTP | When |
|---|---|---|
invalid_limit | 400 | limit below 1 or over 10,000; allowed_max 10000. |
invalid_profile | 400 | The profile breaks a bound in the tables above. |
unknown_check_field | 400 | A field no row names; the message suggests the nearest one when one is close, and allowed lists every field. |
field_unindexed | 503 | The field is on /catalog but this build cannot search hexagons by it; POST /v1/custom-scores reads it for a list of homes. |
| Every Where and Paging code | ||
Request
curl -X POST -H "X-Application-Id: YOUR_APP_ID" -H "X-Api-Key: YOUR_APP_KEY" -H "Content-Type: application/json" \
-d '{
"homes": [
{"id": "listing-1", "lat": 51.5288, "lng": -0.1309},
{"id": "listing-2", "lat": 52.023, "lng": -0.77}
],
"profile": {
"checks": {
"supermarket": {"field": "proximity.walk_s.supermarket", "max": 600, "weight": 3, "label": "Supermarket in 10 min"},
"quiet": {"field": "neighbourhood.noise.max_lden_db", "max": 55, "weight": 2, "label": "Quiet street"},
"gyms": {"field": "proximity.bands.walk.600.category_counts.gym", "min": 3, "weight": 1, "label": "3+ gyms in 10 min"},
"no_flood": {"field": "neighbourhood.flood.zone", "max": 1, "must": true, "label": "Not in a flood zone"},
"transit": {"field": "proximity.ratings.transit", "min": 70, "weight": 2, "label": "Good public transport"},
"big_park": {"field": "proximity.bands.walk.900.over_5ha_count", "min": 1, "weight": 2, "label": "A park of 5+ ha in 15 min"}
}
}
}' \
"https://data.traveltimeapp.com/v1/custom-scores"Shade = checks.supermarket.value2377422 hexagons; a hexagon opens its row.
| Hexagoncell | Checks supermarket metchecks.supermarket.met | Checks supermarket valuechecks.supermarket.value | Checks quiet metchecks.quiet.met | Map |
|---|---|---|---|---|
| 89195da4d03ffff | met | 237 | not met | |
| 89195d88a97ffff | not met | 742 | not met |
All 2 shown
Response
{
"results": [
{
"id": "listing-1",
"cell": "89195da4d03ffff",
"checks": {
"supermarket": {
"met": true,
"value": 237
},
"quiet": {
"met": false,
"value": 81.3
},
"gyms": {
"met": true,
"value": 12
},
"no_flood": {
"met": true,
"value": 1
},
"transit": {
"met": true,
"value": 100
},
"big_park": {
"met": true,
"value": 1
}
},
"musts_met": true,
"score": {
"points": 8,
"out_of": 10,
"share": 0.8
}
},
"… 1 more row"
],
"next_cursor": null
}