A hiking loop on a map in five minutes
Ask the API for an 8 km hiking loop from Zermatt, read what comes back, and draw it on a map with one HTML file. You don't need an account: without a key you get 500 credits a day.
1. Ask for a loop 1 min
Send a start point as
[lon, lat], a distance and a profile (hike,runorbike). The loop follows signed trails.# Without a key, leave out the Authorization line (500 credits a day per address). curl --fail-with-body 'https://api.trailsplits.com/v1/roundtrips' \ -H "Authorization: Bearer $TRAILSPLITS_API_KEY" \ -H 'Content-Type: application/json' \ --data '{"start":[7.7491,46.0207],"distance_km":12,"profile":"hike"}'2. Read the answer 1 min
You get the best loop with its distance, climb, walking time, surfaces, hazards and its line; add
"alternatives": 3for three more. When the best loop misses the distance,statussaysroughandreasonsays why. When there is no loop at all, the answer is422 no_loop.{ "schema": "roundtrips/1", "status": "complete", "loops": [ { "rank": 0, "status": "complete", // or "rough": off the distance by over 10 %, or 20 % walked twice "distance_m": 8011, "ascent_m": 233, "time": { "model": "trailsplits-hike/2", "minutes": 135 }, "surfaces": […], "hazards": […], "difficulty": {…}, "shape": "wd{wvAu}}wMgAR…" // the line, polyline6 (lat,lon at 1e-6) } ], "engine": { "tiles": { "release": "osm_weekly_…" } } }The line is encoded as polyline6. The file in step 3 decodes it in fifteen lines.
3. Draw it on a map 2 min
Save this as
loop-map.htmland open it in a browser. It loads MapLibre and the TrailSplits map style, asks for the loop, and draws it in red. No build step, no key.Download loop-map.html · Open it here
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>A hiking loop from the TrailSplits API</title> <link rel="stylesheet" href="https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.css"> <script src="https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.js"></script> <script src="https://unpkg.com/pmtiles@4/dist/pmtiles.js"></script> <style>body { margin: 0; font: 15px system-ui, sans-serif; } #map { height: 80vh; } #info { margin: 8px 12px; }</style> </head> <body> <p id="info">Asking for a loop…</p> <div id="map"></div> <script> const API = 'https://api.trailsplits.com'; const KEY = ''; // optional: a browser key (ts_pub_…) for this page's origin, from trailsplits.com/api/console const start = [7.7491, 46.0207]; // [lon, lat]: Zermatt // Lines arrive as polyline6 (lat,lon pairs at 1e-6); turn them into GeoJSON [lon, lat]. function decodePolyline6(s) { const out = []; let i = 0, lat = 0, lon = 0; while (i < s.length) { for (const axis of [0, 1]) { let shift = 0, result = 0, b; do { b = s.charCodeAt(i++) - 63; result |= (b & 31) << shift; shift += 5; } while (b >= 32); const d = result & 1 ? ~(result >> 1) : result >> 1; if (axis === 0) lat += d; else lon += d; } out.push([lon / 1e6, lat / 1e6]); } return out; } const loop = fetch(API + '/v1/roundtrips', { method: 'POST', headers: { 'Content-Type': 'application/json', ...(KEY ? { Authorization: 'Bearer ' + KEY } : {}) }, body: JSON.stringify({ start, distance_km: 8, profile: 'hike' }), }).then(async (res) => { const answer = await res.json(); if (!res.ok) throw new Error(answer.error?.message ?? answer.message ?? res.status); const best = answer.loops[0]; if (!best) throw new Error(answer.reason ?? 'no loop from here'); const minutes = Math.round(best.time?.minutes ?? 0); document.getElementById('info').textContent = (best.distance_m / 1000).toFixed(1) + ' km loop, ' + best.ascent_m + ' m up' + (minutes ? ', about ' + Math.floor(minutes / 60) + ' h ' + (minutes % 60) + ' min' : '') + ' (' + best.status + ')'; return decodePolyline6(best.shape); }).catch((e) => { document.getElementById('info').textContent = 'No loop: ' + e.message; return null; }); maplibregl.addProtocol('pmtiles', new pmtiles.Protocol().tile); const map = new maplibregl.Map({ container: 'map', style: API + '/styles/current/trailsplits_openmaptiles_style.json', center: start, zoom: 13, attributionControl: { customAttribution: '© TrailSplits' } }); map.on('load', async () => { const coordinates = await loop; if (!coordinates) return; map.addSource('loop', { type: 'geojson', data: { type: 'Feature', geometry: { type: 'LineString', coordinates }, properties: {} } }); map.addLayer({ id: 'loop', type: 'line', source: 'loop', paint: { 'line-color': '#e11d48', 'line-width': 4 } }); const bounds = coordinates.reduce((b, c) => b.extend(c), new maplibregl.LngLatBounds(coordinates[0], coordinates[0])); map.fitBounds(bounds, { padding: 40 }); }); </script> </body> </html>Or try it right here: click another start on the map and send.
Sends a real request, with your key or the keyless allowance. 4. Get a key when you need more 1 min
A free key from the API console gives you 30,000 credits a month, commercial use with attribution, and a usage view.
- From a server: use a secret key in the
Authorizationheader. Keep it out of web pages and apps. - From a web page like
loop-map.html: issue a browser key (ts_pub_…) for your page's origin and put it inKEY. It works only from the origins you list.
curl -sS -X POST https://api.trailsplits.com/v1/roundtrips \ -H "Authorization: Bearer $TRAILSPLITS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"start":[7.7491,46.0207],"distance_km":8,"profile":"hike"}'- From a server: use a secret key in the
Next
- Recipes: an elevation profile for a GPX file, weather along a hike, a map with 3D terrain.
- Reference: every call with Try it on a map, in curl, JavaScript and Python.
- Conventions: coordinate order, units, errors and limits.
- Attribution: what to show next to the data.