Routing API

Turn-by-turn routes for hiking, cycling and driving, plus route optimisation, travel-time matrices, reachability areas (isochrones) and GPS-trace map matching.

EngineValhalla, with MapRiot’s hiking extension (use_marked_trails)
DataOpenStreetMap, with SRTM/Copernicus elevation for hill-aware costing
CoverageEurope
PlanPaid plans only (Starter, Pro, Enterprise). Free-plan keys get 403.
Request formatJSON: send it as a POST body, or URL-encoded in GET ?json=…
Coordinateslat and lon fields in WGS 84. Route shapes are encoded polylines with 6 decimal places (polyline6)

Routing is available on paid plans only — Starter, Pro and Enterprise. A free-plan key gets 403 on every routing endpoint; see Errors for the exact response and Pricing for the plans.

The endpoints and request/response formats are Valhalla’s own, so existing Valhalla client libraries work. Point them at https://api.mapriot.com and add ?apiKey=<key> to the URL.


Endpoints and billing

EndpointWhat it doesUnits
/routeRoute between 2 or more locations, with optional turn-by-turn directions5
/optimized_routeVisit all locations in the shortest order (first and last stay fixed)10
/sources_to_targetsTravel time and distance matrix between sets of locations10
/isochroneAreas reachable within given times or distances10
/trace_routeSnap a GPS trace to the road/trail network and return it as a route5
/trace_attributesSnap a GPS trace and return per-edge attributes (surface, trail marking, …)5
/locateFind the nearest roads and paths to a coordinate1

Units are charged per request, whatever its size (within the limits). A browser’s CORS preflight (OPTIONS) request is never billed.


Quick start

curl -X POST "https://api.mapriot.com/route?apiKey=<key>" \
  -H "Content-Type: application/json" \
  -d '{
    "locations": [
      {"lat": 50.7360, "lon": 15.7399},
      {"lat": 50.7277, "lon": 15.6828}
    ],
    "costing": "pedestrian",
    "costing_options": {"pedestrian": {"use_marked_trails": 1}},
    "units": "kilometers"
  }'

The same request as a GET, for clients that can’t send a body:

GET https://api.mapriot.com/route?json={"locations":[{"lat":50.7360,"lon":15.7399},{"lat":50.7277,"lon":15.6828}],"costing":"pedestrian"}&apiKey=<key>

URL-encode the json value. GET requests are limited by URL length, so use POST for long location lists and GPS traces.

From a browser:

const res = await fetch('https://api.mapriot.com/route?apiKey=<key>', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    locations: [
      { lat: 50.7360, lon: 15.7399 },
      { lat: 50.7277, lon: 15.6828 },
    ],
    costing: 'pedestrian',
    costing_options: { pedestrian: { use_marked_trails: 1 } },
  }),
});
const { trip } = await res.json();

Costing models

costing selects the travel mode. costing_options.<costing> tunes it.

costingUse for
pedestrianWalking and hiking: footpaths, trails, steps. The default choice for outdoor apps
bicycleCycling: road, city, gravel and mountain bikes (set bicycle_type)
autoDriving a car, e.g. to a trailhead

Valhalla’s other costings (motorcycle, motor_scooter, truck, bus, taxi) also work, but we only tune and support the three above.

pedestrian options

OptionDefaultDescription
use_marked_trails0MapRiot extension. 0–1: how strongly to prefer waymarked hiking routes (OSM route=hiking relations, e.g. KČT trails). 1 keeps you on marked trails wherever it is reasonable. Unmarked ways cost up to 5× more, and leaving a marked trail adds up to a 15 s penalty
max_hiking_difficulty1Highest SAC scale grade allowed, 0–6. The default 1 excludes mountain trails (sac_scale=mountain_hiking and above). Use 2–3 for alpine hiking
walking_speed5.1km/h, 0.5–25
use_hills0.50–1: willingness to climb. Lower values avoid ascents
step_penalty30Extra seconds per set of steps
use_ferry0.50–1
use_living_streets0.60–1

bicycle options

OptionDefaultDescription
bicycle_typehybridroad, hybrid, city, cross (gravel) or mountain. Sets which surfaces are acceptable and the default speed
cycling_speedby typekm/h. Defaults: road 25, cross 20, hybrid/city 18, mountain 16
use_roads0.250–1: willingness to share roads with cars. Lower values prefer cycleways and tracks
use_hills0.250–1: willingness to climb
avoid_bad_surfaces0.250–1: how strongly to avoid rough surfaces. Set low for mountain
use_ferry0.50–1

auto options

OptionDefaultDescription
use_highways0.50–1: preference for motorways
use_tolls0.50–1: willingness to use toll roads
top_speed140km/h, 10–252

Route

POST /route?apiKey=<key>

Request

FieldDescription
locationsArray of 2 or more locations, in visiting order (required)
costingpedestrian, bicycle or auto (required)
costing_optionsPer-costing options, see Costing models
unitskilometers (default) or miles
languageLanguage of the directions, e.g. en-US (default), cs-CZ, sk-SK, pl-PL, de-DE, es-ES
directions_typeinstructions (default: maneuvers with text), maneuvers (no text) or none (just the shape and summary, smallest response)
alternatesNumber of alternative routes to return (0–2)
shape_formatpolyline6 (default), polyline5 or geojson
formatjson (default), osrm (OSRM-compatible response) or gpx
elevation_intervalMetres between elevation samples along the route. Adds an elevation array (metres) to each leg; 0 (default) turns it off
exclude_locationsLocations to route around (e.g. a closed bridge)
exclude_polygonsAreas to route around: an array of rings, each an array of [lon, lat]
date_timeDeparture/arrival time for time-dependent routing: {"type": 1, "value": "2026-10-01T08:00"} (0 = now, 1 = depart at, 2 = arrive by)
idYour own identifier, echoed in the response

Locations

FieldDescription
lat, lonCoordinate (required)
typebreak (default: a stop, starts a new leg), through (pass through without stopping), via or break_through
radiusMetres around the point in which to snap to the network
headingPreferred direction of travel at this point, degrees from north
nameLabel, echoed back in the response

Example: marked hiking trail, Sněžka to Luční hora

{
  "locations": [
    {"lat": 50.7360, "lon": 15.7399},
    {"lat": 50.7277, "lon": 15.6828}
  ],
  "costing": "pedestrian",
  "costing_options": {"pedestrian": {"use_marked_trails": 1, "max_hiking_difficulty": 2}},
  "directions_type": "none"
}

Response (shortened)

{
  "trip": {
    "locations": [
      {"type": "break", "lat": 50.736, "lon": 15.7399, "side_of_street": "left", "original_index": 0},
      {"type": "break", "lat": 50.7277, "lon": 15.6828, "side_of_street": "right", "original_index": 1}
    ],
    "legs": [
      {
        "summary": {"length": 5.561, "time": 5162.089, "min_lat": 50.725403, "min_lon": 15.687319, "max_lat": 50.739757, "max_lon": 15.743366, "has_ferry": false},
        "shape": "yttw_Bceu_]gJoHi@mF]}Aq@kAeAa@y@CuAToE…"
      }
    ],
    "summary": {"length": 5.561, "time": 5162.089, "…": "…"},
    "status": 0,
    "status_message": "Found route between points",
    "units": "kilometers",
    "language": "en-US"
  }
}
FieldDescription
trip.summary.lengthTotal distance in units
trip.summary.timeEstimated travel time in seconds
trip.legs[].shapeThe route geometry as a polyline with 6-digit precision. Decode with e.g. @mapbox/polyline using precision 6, or request "shape_format": "geojson"
trip.legs[].maneuversTurn-by-turn steps (unless directions_type is none): instruction, length, time, begin_shape_index, end_shape_index, …
alternatesAlternative routes, each with its own trip, when alternates > 0

Drawing the route on a MapLibre map:

import polyline from '@mapbox/polyline';

const coords = polyline.decode(trip.legs[0].shape, 6).map(([lat, lon]) => [lon, lat]);
map.addSource('route', { type: 'geojson', data: { type: 'LineString', coordinates: coords } });
map.addLayer({ id: 'route', type: 'line', source: 'route', paint: { 'line-color': '#D03D33', 'line-width': 4 } });

Optimized route

Reorders the intermediate locations for the shortest total travel time: a round of summit visits, huts, or delivery stops. The first and last locations stay fixed; for a round trip, repeat the start as the last location.

POST /optimized_route?apiKey=<key>

Takes the same request as /route (alternates excluded). Up to 50 locations for pedestrian and bicycle, 20 for auto.

Example: walking round of four Prague sights

{
  "locations": [
    {"lat": 50.0875, "lon": 14.4213},
    {"lat": 50.0906, "lon": 14.4003},
    {"lat": 50.0810, "lon": 14.4136},
    {"lat": 50.0755, "lon": 14.4378},
    {"lat": 50.0875, "lon": 14.4213}
  ],
  "costing": "pedestrian",
  "directions_type": "none"
}

The response has the same shape as /route. trip.locations comes back in the optimised order, and each location’s original_index points to its position in your request. For the example above, the order is 0, 3, 2, 1, 4.


Matrix (sources to targets)

Travel time and distance from every source to every target, without the route geometry. Useful for “nearest hut” searches and for sorting results by real travel time.

POST /sources_to_targets?apiKey=<key>
FieldDescription
sourcesArray of locations (required)
targetsArray of locations (required)
costing, costing_options, unitsAs for /route
verbosetrue (default): one object per pair. false: compact durations/distances arrays

sources × targets may be at most 2,500 pairs.

Example

{
  "sources": [{"lat": 50.0875, "lon": 14.4213}],
  "targets": [{"lat": 50.0906, "lon": 14.4003}, {"lat": 50.0755, "lon": 14.4378}],
  "costing": "pedestrian"
}

Response

{
  "sources_to_targets": [
    [
      {"from_index": 0, "to_index": 0, "time": 1678, "distance": 2.031},
      {"from_index": 0, "to_index": 1, "time": 1764, "distance": 2.29}
    ]
  ],
  "sources": [{"lat": 50.087561, "lon": 14.42161}],
  "targets": [{"lat": 50.090539, "lon": 14.400531}, {"lat": 50.075499, "lon": 14.437752}],
  "units": "kilometers"
}

time is in seconds and distance in units. Both are null for a pair with no connection.


Isochrone

Areas reachable from a point within given times or distances, e.g. “everything within a 2-hour walk of this car park”.

POST /isochrone?apiKey=<key>
FieldDescription
locationsExactly one location (required)
costing, costing_optionsAs for /route
contoursUp to 4 objects, each with time (minutes, max 120) or distance (km, max 200), and an optional color (hex without #, e.g. "ff0000") (required)
polygonstrue: return filled polygons. false (default): outlines as lines
denoise0–1 (default 1): drop smaller disconnected islands. 1 keeps only the largest
generalizeSimplification tolerance in metres
show_locationstrue adds the input and snapped points to the output
reversetrue: areas from which the location can be reached in the given time

Example: 30- and 60-minute walk from Sněžka

{
  "locations": [{"lat": 50.7360, "lon": 15.7399}],
  "costing": "pedestrian",
  "contours": [{"time": 30}, {"time": 60}],
  "polygons": true
}

Response: a GeoJSON FeatureCollection you can add straight to a map as a source. Each feature has properties.contour (the value), properties.metric (time or distance) and styling hints (fill, color, fillOpacity). Coordinates are [lon, lat]. Larger contours come first.

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {"contour": 60.0, "metric": "time", "color": "#bf4040", "fill": "#bf4040", "fillOpacity": 0.33},
      "geometry": {"type": "Polygon", "coordinates": [[[15.7479, 50.752474], [15.746552, 50.752347], "…"]]}
    }
  ]
}

Map matching

Snap a recorded GPS trace (e.g. a GPX track) onto the path network.

  • /trace_route returns the matched path as a route, with the same response as /route, including directions.
  • /trace_attributes returns the matched edges with their attributes: surface, SAC scale, trail marking, names, OSM way ids and more.
POST /trace_route?apiKey=<key>
POST /trace_attributes?apiKey=<key>
FieldDescription
shapeArray of {"lat", "lon"} points, optionally with time (epoch seconds) (required, unless encoded_polyline is set)
encoded_polylineThe trace as a polyline6 string, instead of shape
costing, costing_optionsAs for /route (pedestrian, bicycle or auto)
shape_matchwalk_or_snap (default), map_snap for real GPS traces, or edge_walk for shapes that came from our own /route
trace_optionssearch_radius (metres, max 100), gps_accuracy (metres), breakage_distance, interpolation_distance, turn_penalty_factor
use_timestampstrue to use the time of each point instead of the costing’s speed
filters/trace_attributes only: {"attributes": [...], "action": "include"} to choose which attributes are returned. Keeps responses small

At most 16,000 trace points and 200 km per request.

Example: which parts of a hike follow marked trails

{
  "shape": [
    {"lat": 50.73597, "lon": 15.74},
    {"lat": 50.7365, "lon": 15.74056},
    {"lat": 50.73599, "lon": 15.74136},
    {"lat": 50.73524, "lon": 15.74216},
    {"lat": 50.73637, "lon": 15.743}
  ],
  "costing": "pedestrian",
  "shape_match": "map_snap",
  "filters": {
    "attributes": ["edge.names", "edge.length", "edge.surface", "edge.sac_scale", "edge.way_id", "edge.hiking_network"],
    "action": "include"
  }
}

Response (/trace_attributes, shortened)

{
  "units": "kilometers",
  "edges": [
    {
      "way_id": 390385744,
      "names": ["Jubilejní cesta"],
      "length": 0.191,
      "surface": "paved",
      "sac_scale": 0,
      "hiking_network": {"international": false, "national": true, "regional": true, "local": false, "marked": true}
    }
  ],
  "shape": "_utw_Bieu_]aJiHi@mF…"
}

edge.hiking_network is a MapRiot extension: it reports which levels of waymarked hiking route (iwn/nwn/rwn/lwn) the edge belongs to, and marked is true if it belongs to any of them.


Locate

The nearest roads and paths to one or more coordinates, as the router would snap to them. Useful to check whether a point is reachable before routing to it.

POST /locate?apiKey=<key>
FieldDescription
locationsArray of locations (required)
costingOnly returns ways usable by this travel mode
verbosetrue for full edge and node details

Response

[
  {
    "input_lat": 50.08,
    "input_lon": 14.42,
    "edges": [
      {"way_id": 192075532, "correlated_lat": 50.080021, "correlated_lon": 14.419847, "side_of_street": "left", "percent_along": 0.55066}
    ],
    "nodes": []
  }
]

Limits

pedestrianbicycleauto
Route distance100 km500 km5,000 km
Locations per route205020
Matrix pairs (sources × targets)2,5002,5002,500
Matrix distance200 km200 km400 km
Other limits
Alternative routes2
Isochrone1 location, 4 contours, 120 min or 200 km
Map matching16,000 points, 200 km, search_radius ≤ 100 m
Request time30 s

A request over a limit returns 400 with a Valhalla error, e.g. {"error_code": 154, "error": "Path distance exceeds the max distance limit"}. For long hikes, route each day as a separate request. Contact us if you need higher limits.

Request rate

On top of the shape limits above, each API key has a request rate limit. /route, /trace_route and /trace_attributes share one budget; /isochrone, /sources_to_targets and /optimized_route share a much tighter one, because they are far more expensive to serve.

WindowFreeStarterProEnterprise
/route, /trace_route, /trace_attributesper minute3001,2006,00024,000
/isochrone, /sources_to_targets, /optimized_routeper 10 seconds1030100300
/locateper minute6003,00012,00048,000

Going over returns 429 with a Retry-After header and a JSON body. See rate limits for the full contract.

Request size

POST bodies are capped at 512 KB (413 above that). A 5,000-point trace sent as raw lat/lon objects is about 180 KB, so only a request that is already over the shape limits can hit this.


Errors

StatusCause
400Invalid request or no route found. The body is JSON: {"error_code": 442, "error": "No path could be found for input", "status_code": 400, "status": "Bad Request"}
403Missing, invalid or restricted API key — or routing is not included in your plan (free plan). Plain-text body: This feature is not available on your plan. See https://mapriot.com/pricing for details or contact us at support@mapriot.com. A rejected request is not billed.
413Request body over 512 KB
429Rate limit for this API key (JSON body with Retry-After), or the free-tier hard cap at 200% of the monthly limit (plain text, no Retry-After)
5xxTemporary server error. Retry with backoff

Common Valhalla error_codes:

CodeMeaning
100The request body is not valid JSON
120Too few locations (at least 2 for a route)
125Unknown costing
150, 154Too many locations, or distance over the limit
171No suitable edges near a location: it’s too far from any road or path usable by this costing
442No path could be found (e.g. locations on unconnected islands)
443, 444Map matching failed. Try "shape_match": "map_snap" or a larger search_radius

Full list: Valhalla status codes.