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.

GET https://{country}.yatmo.com/enrichment LicenseKey header (backend key)

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:

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”).
One place per category, the same place for every mode
The four modes describe the trip to the same place, so a listing card can say “Primary school Les Tilleuls: 6 min on foot, 2 min by bike” without contradiction.

How the values are computed

The values are computed the same way as the travel times of the Summary and of the map plugin:

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

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.

Listing enrichment
What portals do with these values, and an interactive search built on real responses. Read more on yatmo.com.

Try the filters on enriched listings