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.
| Engine | Valhalla, with MapRiot’s hiking extension (use_marked_trails) |
| Data | OpenStreetMap, with SRTM/Copernicus elevation for hill-aware costing |
| Coverage | Europe |
| Plan | Paid plans only (Starter, Pro, Enterprise). Free-plan keys get 403. |
| Request format | JSON: send it as a POST body, or URL-encoded in GET ?json=… |
| Coordinates | lat 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
| Endpoint | What it does | Units |
|---|---|---|
/route | Route between 2 or more locations, with optional turn-by-turn directions | 5 |
/optimized_route | Visit all locations in the shortest order (first and last stay fixed) | 10 |
/sources_to_targets | Travel time and distance matrix between sets of locations | 10 |
/isochrone | Areas reachable within given times or distances | 10 |
/trace_route | Snap a GPS trace to the road/trail network and return it as a route | 5 |
/trace_attributes | Snap a GPS trace and return per-edge attributes (surface, trail marking, …) | 5 |
/locate | Find the nearest roads and paths to a coordinate | 1 |
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.
costing | Use for |
|---|---|
pedestrian | Walking and hiking: footpaths, trails, steps. The default choice for outdoor apps |
bicycle | Cycling: road, city, gravel and mountain bikes (set bicycle_type) |
auto | Driving 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
| Option | Default | Description |
|---|---|---|
use_marked_trails | 0 | MapRiot 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_difficulty | 1 | Highest 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_speed | 5.1 | km/h, 0.5–25 |
use_hills | 0.5 | 0–1: willingness to climb. Lower values avoid ascents |
step_penalty | 30 | Extra seconds per set of steps |
use_ferry | 0.5 | 0–1 |
use_living_streets | 0.6 | 0–1 |
bicycle options
| Option | Default | Description |
|---|---|---|
bicycle_type | hybrid | road, hybrid, city, cross (gravel) or mountain. Sets which surfaces are acceptable and the default speed |
cycling_speed | by type | km/h. Defaults: road 25, cross 20, hybrid/city 18, mountain 16 |
use_roads | 0.25 | 0–1: willingness to share roads with cars. Lower values prefer cycleways and tracks |
use_hills | 0.25 | 0–1: willingness to climb |
avoid_bad_surfaces | 0.25 | 0–1: how strongly to avoid rough surfaces. Set low for mountain |
use_ferry | 0.5 | 0–1 |
auto options
| Option | Default | Description |
|---|---|---|
use_highways | 0.5 | 0–1: preference for motorways |
use_tolls | 0.5 | 0–1: willingness to use toll roads |
top_speed | 140 | km/h, 10–252 |
Route
POST /route?apiKey=<key>Request
| Field | Description |
|---|---|
locations | Array of 2 or more locations, in visiting order (required) |
costing | pedestrian, bicycle or auto (required) |
costing_options | Per-costing options, see Costing models |
units | kilometers (default) or miles |
language | Language of the directions, e.g. en-US (default), cs-CZ, sk-SK, pl-PL, de-DE, es-ES |
directions_type | instructions (default: maneuvers with text), maneuvers (no text) or none (just the shape and summary, smallest response) |
alternates | Number of alternative routes to return (0–2) |
shape_format | polyline6 (default), polyline5 or geojson |
format | json (default), osrm (OSRM-compatible response) or gpx |
elevation_interval | Metres between elevation samples along the route. Adds an elevation array (metres) to each leg; 0 (default) turns it off |
exclude_locations | Locations to route around (e.g. a closed bridge) |
exclude_polygons | Areas to route around: an array of rings, each an array of [lon, lat] |
date_time | Departure/arrival time for time-dependent routing: {"type": 1, "value": "2026-10-01T08:00"} (0 = now, 1 = depart at, 2 = arrive by) |
id | Your own identifier, echoed in the response |
Locations
| Field | Description |
|---|---|
lat, lon | Coordinate (required) |
type | break (default: a stop, starts a new leg), through (pass through without stopping), via or break_through |
radius | Metres around the point in which to snap to the network |
heading | Preferred direction of travel at this point, degrees from north |
name | Label, 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"
}
}| Field | Description |
|---|---|
trip.summary.length | Total distance in units |
trip.summary.time | Estimated travel time in seconds |
trip.legs[].shape | The 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[].maneuvers | Turn-by-turn steps (unless directions_type is none): instruction, length, time, begin_shape_index, end_shape_index, … |
alternates | Alternative 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>| Field | Description |
|---|---|
sources | Array of locations (required) |
targets | Array of locations (required) |
costing, costing_options, units | As for /route |
verbose | true (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>| Field | Description |
|---|---|
locations | Exactly one location (required) |
costing, costing_options | As for /route |
contours | Up to 4 objects, each with time (minutes, max 120) or distance (km, max 200), and an optional color (hex without #, e.g. "ff0000") (required) |
polygons | true: return filled polygons. false (default): outlines as lines |
denoise | 0–1 (default 1): drop smaller disconnected islands. 1 keeps only the largest |
generalize | Simplification tolerance in metres |
show_locations | true adds the input and snapped points to the output |
reverse | true: 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_routereturns the matched path as a route, with the same response as/route, including directions./trace_attributesreturns 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>| Field | Description |
|---|---|
shape | Array of {"lat", "lon"} points, optionally with time (epoch seconds) (required, unless encoded_polyline is set) |
encoded_polyline | The trace as a polyline6 string, instead of shape |
costing, costing_options | As for /route (pedestrian, bicycle or auto) |
shape_match | walk_or_snap (default), map_snap for real GPS traces, or edge_walk for shapes that came from our own /route |
trace_options | search_radius (metres, max 100), gps_accuracy (metres), breakage_distance, interpolation_distance, turn_penalty_factor |
use_timestamps | true 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>| Field | Description |
|---|---|
locations | Array of locations (required) |
costing | Only returns ways usable by this travel mode |
verbose | true 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
pedestrian | bicycle | auto | |
|---|---|---|---|
| Route distance | 100 km | 500 km | 5,000 km |
| Locations per route | 20 | 50 | 20 |
Matrix pairs (sources × targets) | 2,500 | 2,500 | 2,500 |
| Matrix distance | 200 km | 200 km | 400 km |
| Other limits | |
|---|---|
| Alternative routes | 2 |
| Isochrone | 1 location, 4 contours, 120 min or 200 km |
| Map matching | 16,000 points, 200 km, search_radius ≤ 100 m |
| Request time | 30 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.
| Window | Free | Starter | Pro | Enterprise | |
|---|---|---|---|---|---|
/route, /trace_route, /trace_attributes | per minute | 300 | 1,200 | 6,000 | 24,000 |
/isochrone, /sources_to_targets, /optimized_route | per 10 seconds | 10 | 30 | 100 | 300 |
/locate | per minute | 600 | 3,000 | 12,000 | 48,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
| Status | Cause |
|---|---|
400 | Invalid 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"} |
403 | Missing, 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. |
413 | Request body over 512 KB |
429 | Rate 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) |
5xx | Temporary server error. Retry with backoff |
Common Valhalla error_codes:
| Code | Meaning |
|---|---|
100 | The request body is not valid JSON |
120 | Too few locations (at least 2 for a route) |
125 | Unknown costing |
150, 154 | Too many locations, or distance over the limit |
171 | No suitable edges near a location: it’s too far from any road or path usable by this costing |
442 | No path could be found (e.g. locations on unconnected islands) |
443, 444 | Map matching failed. Try "shape_match": "map_snap" or a larger search_radius |
Full list: Valhalla status codes.