Listing enrichment endpoint
Send the coordinates of a listing, get back the nearest school, stop, station and supermarket, each with the real travel distance and time on foot, by bike, by car and by public transport. One flat JSON document per listing, ready to store in your database.
Cached server-side for 7 days per (lat, lng, language).
What it is for
Home seekers search with questions like “a primary school within 10 minutes’ walk” or “a train station I can cycle to”. Answering them on a portal usually means building a geographic pipeline: collecting schools and transit stops, keeping them fresh, running a routing engine, computing travel times for every listing. This endpoint does all of that for you.
Call it once when a listing is created (or when its address changes), store the result next to the listing, and you can:
- Filter: “primary school under 10 min on foot”, “metro under 500 m”, “train station under 15 min by public transport”.
- Rank: sort results by the walking time to the nearest supermarket, or combine several categories into your own score.
- Inform: show “Primary school 6 min on foot” or “Tram stop 350 m” badges on listing cards and pages.
Everything runs in your own database, at your own query speed: no call to Yatmo at search time.
Request
| Name | In | Type | Description |
|---|---|---|---|
| latitude required | query | double | Latitude of the listing, in decimal degrees. Must be inside the country’s borders. |
| longitude required | query | double | Longitude of the listing, in decimal degrees. Must be inside the country’s borders. |
| language | query | string |
Language of the label fields only (default EN). Every other value is identical whatever the language. Must be one of the country’s languages, see Countries & languages.
|
The country comes from the subdomain, as for every endpoint: be.yatmo.com, fr.yatmo.com… See Conventions.
Examples
curl -H 'LicenseKey: YOUR_BACKEND_KEY' \
'https://be.yatmo.com/enrichment?latitude=50.8467&longitude=4.3525&language=FR'
// Server-side (Node.js 18+): the backend key must never reach a browser.
const res = await fetch(
'https://be.yatmo.com/enrichment?latitude=50.8467&longitude=4.3525&language=FR',
{ headers: { LicenseKey: process.env.YATMO_KEY } }
);
const enrichment = await res.json();
for (const category of enrichment.categories) {
const walk = category.nearest?.walking;
console.log(category.key, walk ? `${walk.durationMinutes} min on foot` : 'none nearby');
}
$ch = curl_init('https://be.yatmo.com/enrichment?latitude=50.8467&longitude=4.3525&language=FR');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['LicenseKey: ' . getenv('YATMO_KEY')]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$enrichment = json_decode(curl_exec($ch), true);
curl_close($ch);
foreach ($enrichment['categories'] as $category) {
$walk = $category['nearest']['walking'] ?? null;
echo $category['key'], ': ', $walk ? $walk['durationMinutes'] . ' min on foot' : 'none nearby', PHP_EOL;
}
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("LicenseKey", Environment.GetEnvironmentVariable("YATMO_KEY"));
var json = await http.GetStringAsync(
"https://be.yatmo.com/enrichment?latitude=50.8467&longitude=4.3525&language=FR");
using var doc = JsonDocument.Parse(json);
foreach (var category in doc.RootElement.GetProperty("categories").EnumerateArray())
{
var nearest = category.GetProperty("nearest");
var walk = nearest.ValueKind == JsonValueKind.Null ? default : nearest.GetProperty("walking");
Console.WriteLine($"{category.GetProperty("key")}: {(walk.ValueKind == JsonValueKind.Object ? walk.GetProperty("durationMinutes") + " min on foot" : "none nearby")}");
}
Response shape
A real response for a flat in central Brussels, shortened to three of its categories:
{
"latitude": 50.8467,
"longitude": 4.3525,
"country": "BE",
"language": "EN",
"searchRadiusMeters": 25000,
"categories": [
// ... every category of the country, here 3 of 13
{
"id": 10010002000000,
"key": "Children.ElementarySchool",
"group": "Children",
"label": "Elementary school",
"nearest": {
"name": "Vrije Basisschool Sint-Joris Basisschool",
"latitude": 50.843228,
"longitude": 4.350452,
"straightLineDistanceMeters": 412,
"walking": {
"distanceMeters": 548,
"durationSeconds": 395,
"durationMinutes": 7
},
"bicycling": {
"distanceMeters": 548,
"durationSeconds": 174,
"durationMinutes": 3
},
"driving": {
"distanceMeters": 922,
"durationSeconds": 140,
"durationMinutes": 3
},
"transit": {
"distanceMeters": 548,
"durationSeconds": 395,
"durationMinutes": 7
}
}
},
{
"id": 10020001000000,
"key": "Transports.Metros",
"group": "Transports",
"label": "Metro station",
"nearest": {
"name": "De Brouckere",
"latitude": 50.849821,
"longitude": 4.352486,
"straightLineDistanceMeters": 347,
"walking": {
"distanceMeters": 430,
"durationSeconds": 309,
"durationMinutes": 6
},
"bicycling": {
"distanceMeters": 604,
"durationSeconds": 282,
"durationMinutes": 5
},
"driving": {
"distanceMeters": 2059,
"durationSeconds": 316,
"durationMinutes": 6
},
"transit": {
"distanceMeters": 430,
"durationSeconds": 309,
"durationMinutes": 6
}
}
},
{
"id": 10020008000000,
"key": "Transports.Airports",
"group": "Transports",
"label": "Airport",
"nearest": {
"name": "Brussels Airport",
"latitude": 50.895122,
"longitude": 4.485123,
"straightLineDistanceMeters": 10761,
"walking": {
"distanceMeters": 11988,
"durationSeconds": 8642,
"durationMinutes": 145
},
"bicycling": {
"distanceMeters": 12000,
"durationSeconds": 2829,
"durationMinutes": 48
},
"driving": {
"distanceMeters": 15105,
"durationSeconds": 914,
"durationMinutes": 16
},
"transit": {
"distanceMeters": 10759,
"durationSeconds": 1939,
"durationMinutes": 33
}
}
}
]
}
Top level
| Name | In | Type | Description |
|---|---|---|---|
| latitude, longitude | double | The position you sent. | |
| country, language | string | The country of the subdomain and the language of the labels. | |
| searchRadiusMeters | int | Places further than this, as the crow flies, are not considered (25,000 m). | |
| categories[] | array | One entry per category, always the same list for a given country, whether or not a place was found. Your database schema never has to change from one listing to the next. |
Each category
| Name | In | Type | Description |
|---|---|---|---|
| id | long | Category id. Stable for a country, the same ids as the map plugin’s lifestyle search. | |
| key | string |
Readable key, independent of the language: Transports.Metros, Children.ElementarySchool… Use it (or id) as your column or row key. Keys are defined per country: France has Children.EcoleElementaire and Transports.Rers, Belgium Children.ElementarySchool.
|
|
| group | string |
Children (schools and nurseries), Transports or Shopping.
|
|
| label | string | Singular label in the requested language (“Primary school”, “Metro station”). Display it, never compare it. | |
| nearest | object? |
The nearest place of that category, or null when there is none within the search radius.
|
The nearest place
| Name | In | Type | Description |
|---|---|---|---|
| name | string | Name of the school, stop, station or shop. | |
| latitude, longitude | double | Its position, to draw it on a map or link to directions. | |
| straightLineDistanceMeters | int | Distance as the crow flies, for reference only. | |
| walking, bicycling, driving, transit | object? |
The trip by each travel mode, or null when that mode could not be routed (no public transport serving the area, for example).
|
|
| …distanceMeters | int | Distance along the road, path or transit network, in metres. | |
| …durationSeconds | int | Travel time in seconds. | |
| …durationMinutes | int | The same time in minutes, rounded up: 6 min 10 s is 7. Filter on this one, it reads the way users think (“under 10 minutes”). |
How the values are computed
The values are computed the same way as the travel times of the Summary and of the map plugin:
- Real routes, not straight lines. Distances and times follow the road network for cars, the paths for pedestrians and cyclists, and the timetables for public transport.
- The nearest place is the nearest on foot. The three closest places as the crow flies are routed on foot and the shortest walk wins. The school just across a railway line, with no crossing nearby, does not beat the one down your street.
- Public transport is computed for a departure on a weekday morning, at about 08:00 local time. It includes the walk to the first stop and from the last one. When the place is only a few hundred metres away, the trip is the walk itself.
- Categories are those of the map plugin’s lifestyle search: schools and nurseries, the stops of each public transport mode (metro, tram, bus, train…), shared bikes and cars, charging stations, the airport, and one “Supermarket” entry covering every brand. The exact list depends on the country: call the endpoint once to see it.
Storing it in your database
Two layouts work well. Pick the one that fits how your search is built.
One row per listing and category (easy to query, no schema change when a category is added):
CREATE TABLE listing_proximity (
listing_id BIGINT NOT NULL,
category_key VARCHAR(64) NOT NULL, -- "Transports.Metros"
place_name VARCHAR(255),
walk_minutes INT, -- NULL: nothing within reach
bike_minutes INT,
car_minutes INT,
transit_minutes INT,
walk_meters INT,
PRIMARY KEY (listing_id, category_key)
);
-- Listings with a primary school under 10 minutes on foot
SELECT l.*
FROM listings l
JOIN listing_proximity p ON p.listing_id = l.id
WHERE p.category_key = 'Children.ElementarySchool' AND p.walk_minutes <= 10;
A few flat columns on the listing (fastest filters, for the three or four criteria your search offers): school_walk_min, metro_walk_m, station_transit_min… filled from the matching key. Or keep the whole document in a JSON column and index only what you filter on.
Store null as NULL, not as 0: 0 would mean the place is at the door.
Enriching a whole catalogue
- Call from your server, with your backend key. Keys tied to a domain name (the free plugin keys) are refused with
403. - When to call: when a listing is published and when its address changes. The places around a listing change slowly: refreshing your whole catalogue once a month is plenty.
- Pace: a first call for a position takes about one second, a repeated one is served from the cache. Keep at most 4 requests in flight. Beyond that the API answers
429: wait a few seconds and retry. - Use the listing’s own coordinates. Rounding them, or using the centre of the town, gives the neighbourhood of another place.
For a nightly job, the bulk scoring recipe shows a complete worker loop with throttling and retries.
Errors
| Name | In | Type | Description |
|---|---|---|---|
| 400 | Bad request |
Position outside the country of the subdomain (Geolocation outside BE), or a language the country does not serve.
|
|
| 401 | Unauthorized | Missing or unknown licence key. | |
| 403 | Forbidden | The key is a domain-name key, or does not cover this country. Contact Yatmo for a backend key. | |
| 429 | Too many requests | Too many enrichments in progress. Retry after a few seconds. | |
| 503 | Service unavailable | The routing engine did not answer. Nothing is half computed: retry later, and do not store anything for that listing meanwhile. |
See also the general Errors page.