Recipe: replicate the Summary plugin from raw API calls
Rebuild the “nearest POI per category, with travel time” widget — using only the REST API — so you control every pixel.
Why bother
The JS Summary plugin is great if its default table layout works for you. It doesn’t if you need:
- A completely different visual layout (cards, hex tiles, your own iconography).
- Server-side rendering — the listing page should ship with the summary HTML for SEO.
- Conditional rendering — only show certain categories, or merge them.
- Integration with a non-HTML output (PDF, e-mail, app).
What you need
- The property’s latitude and longitude.
- A Yatmo license key (header-based; server-side).
- One call to
/summaryper property — cached server-side for 7 days, so subsequent renders are cheap.
Fetch the data
Most integrations will do this server-side and embed the result in the listing page’s HTML.
curl -H 'LicenseKey: YOUR_KEY' \
'https://be.yatmo.com/summary?latitude=50.8520525&longitude=4.3442926&language=EN' \
> summary.json
// Node.js / server-side
const summary = await (await fetch(
`https://be.yatmo.com/summary?latitude=${lat}&longitude=${lng}&language=EN`,
{ headers: { LicenseKey: process.env.YATMO_KEY } }
)).json();
$ch = curl_init("https://be.yatmo.com/summary?latitude=$lat&longitude=$lng&language=EN");
curl_setopt($ch, CURLOPT_HTTPHEADER, ['LicenseKey: ' . getenv('YATMO_KEY')]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$summary = json_decode(curl_exec($ch), true);
curl_close($ch);
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("LicenseKey", Environment.GetEnvironmentVariable("YATMO_KEY"));
var json = await http.GetStringAsync($"https://be.yatmo.com/summary?latitude={lat}&longitude={lng}&language=EN");
var summary = JsonSerializer.Deserialize<JsonElement>(json);
Render it your way
Walk the response and pull out one POI per subcategory. Here’s the shape in plain JS:
// summary.AvailableCategoriesAroundPosition is an array of top-level categories
// (e.g. Children, Transports, Shopping, Tourism). Each has .SubCategories.
// Each subcategory has .Data (the POIs) and each POI has .TravelDataElements
// (one entry per travel mode).
summary.AvailableCategoriesAroundPosition.forEach(category => {
category.SubCategories.forEach(sub => {
const closest = sub.Data[0]; // already sorted by distance
if (!closest) return;
const driving = closest.TravelDataElements
.find(t => t.TravelMode === 1); // 1 = Driving
renderRow({
categoryLabel: sub.Label, // localized
poiName: closest.Name, // POI's own name
distanceLabel: driving.PreciseTravelDistanceLongLabel, // "1.2 km"
timeLabel: driving.TravelTimeShortLabel // "3min"
});
});
});
Travel modes are: 1 = Driving, 2 = Walking, 3 = Bicycling, 4 = Transit. Not every POI has every mode — check HasTravelInformation before reading time/distance.
Going to production
- Cache the response on your side for 24h+. The endpoint is already cached server-side for 7 days, but caching at your end saves a hop.
- Handle the “no neighbourhood” case. Some coordinates (rural roads, water edges) return sparse data. Skip categories with empty
SubCategories. - Pick the right travel mode per category. “Walking time to the bakery” reads better than “driving time to the bakery”. Choose intelligently per category instead of always using mode 1.