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. 1. Ask for a loop 1 min

    Send a start point as [lon, lat], a distance and a profile (hike, run or bike). The loop follows signed trails.

    Round trip request (curl)
    # 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. 2. Read the answer 1 min

    You get the best loop with its distance, climb, walking time, surfaces, hazards and its line; add "alternatives": 3 for three more. When the best loop misses the distance, status says rough and reason says why. When there is no loop at all, the answer is 422 no_loop.

    Shortened answer
    {
      "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. 3. Draw it on a map 2 min

    Save this as loop-map.html and 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

    loop-map.html
    <!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. 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 Authorization header. 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 in KEY. It works only from the origins you list.
    The same call with a key
    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"}'

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.