Map matching
Snaps an ordered GPS trace to the most plausible route on the road network.
Signed in? Open this endpoint in the dashboard playground, which uses your organization automatically. Signed out, use Try in Playground below with your API key.
apiKey<token>Your API key from the dashboard, sent as ?apiKey=<key>.
GPS trace and matching options. Must be a JSON object.
application/json- body
mode*stringTravel profile used to decide which roads can plausibly connect the GPS measurements. drive represents a passenger car.
"walk""hike""scooter""motorcycle""drive""light_truck""bicycle""mountain_bike""road_bike""bus""medium_truck""truck""truck_dangerous_goods""heavy_truck""long_truck"waypoints*array<>Ordered GPS measurements to match. Input order is preserved; the API does not optimize or reorder these points.
Deviation (unsupported): at least 2 waypoints are required (Geoapify's schema allows 1; a single point cannot be matched and returns 400).
2 <= items <= 1000geocode_step_address?booleanWhen true, adds an address object to each matched step.
Deviation (approximated): the address is derived without a geocoder: street (the way name), state, country and country_code (lower case, from the routing tiles' admin data). suburb, city, county and postcode are never returned.
falselookup_step_osmtags?booleanWhen true, adds osmtags to each matched step.
Deviation (empty): osmtags is always an empty object {}; there is no OSM tag store behind the engines.
falselookup_osm_overlap?booleanWhen true, adds overlaps to each matched step.
Deviation (empty): overlaps is always an empty array [].
falselang?stringLanguage code for the step instruction text.
"en""en""de""es""fr""hi""it""pt""ru""sl""sv""ca""cs"Matched route as a GeoJSON FeatureCollection.
application/json- response
type*unknownfeatures*array<>Exactly one feature with the matched route.
bbox?array<number>[minLon, minLat, maxLon, maxLat] of the matched route.
4 <= items <= 4curl -X POST 'https://api.data.graphitelab.ai/v1/mapmatching' \ -H 'Content-Type: application/json' \ -d '{ "mode": "drive", "waypoints": [ { "location": [ -86.15813, 39.769 ] }, { "location": [ -86.15813, 39.7705 ] }, { "location": [ -86.15813, 39.772 ] }, { "location": [ -86.15813, 39.7735 ] }, { "location": [ -86.15813, 39.775 ] }, { "location": [ -86.15813, 39.7765 ] } ]}'{ "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "mode": "drive", "distance": 827, "time": 74.43, "legs": [ { "time": 74.43, "distance": 827, "steps": [ { "from_index": 0, "to_index": 1, "time": 1.17, "distance": 13, "osm_way_id": 113084204, "instruction": { "text": "Drive north on North Meridian Street." }, "toll": false, "ferry": false, "bridge": false, "tunnel": false, "roundabout": false, "speed": 40, "speed_limit": 40, "surface": "paved", "lane_count": 2, "road_class": "tertiary", "name": "North Meridian Street", "begin_bearing": 2, "end_bearing": 2 }, { "from_index": 1, "to_index": 2, "time": 2.79, "distance": 31, "osm_way_id": 113084204, "toll": false, "ferry": false, "bridge": false, "tunnel": false, "roundabout": false, "speed": 40, "speed_limit": 40, "surface": "paved", "lane_count": 2, "road_class": "tertiary", "name": "North Meridian Street", "begin_bearing": 2, "end_bearing": 2 }, { "from_index": 2, "to_index": 3, "time": 3.78, "distance": 42, "osm_way_id": 113084204, "toll": false, "ferry": false, "bridge": false, "tunnel": false, "roundabout": false, "speed": 40, "speed_limit": 40, "surface": "paved", "lane_count": 2, "road_class": "tertiary", "name": "North Meridian Street", "begin_bearing": 1, "end_bearing": 1 } ] } ], "waypoints": [ { "original_index": 0, "location": [ -86.15802, 39.769051 ], "original_location": [ -86.15813, 39.769 ], "match_type": "matched", "match_distance": 10.917381, "leg_index": 0, "step_index": 0 }, { "original_index": 1, "location": [ -86.157968, 39.770495 ], "original_location": [ -86.15813, 39.7705 ], "match_type": "matched", "match_distance": 13.773137, "leg_index": 0, "step_index": 7 } ] }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -86.15802, 39.769051 ], [ -86.158016, 39.769167 ], [ -86.158003, 39.769441 ], [ -86.157723, 39.776491 ] ] ] } } ], "bbox": [ -86.15802, 39.769051, -86.157723, 39.776491 ]}Full schema: API reference
POST /v1/mapmatching takes a noisy GPS trace (2 to 1,000 points) and returns the road-network route the vehicle most likely followed, with per-point match results and road-level steps. Use it to clean up recorded tracks, attribute distance and time to roads, or recover road names and speeds along a trip.
How it works
The gateway runs a single Engine, Valhalla, in map-snap mode (Meili). It makes two Valhalla calls in parallel:
-
trace_attributessupplies the matched edges (one step per road edge), matched points, geometry, and road attributes such as speed, surface, lane count and OSM way ID. -
trace_routesupplies the turn instruction text for each step, in the requestedlang. If this call fails (other than the Engine being down), steps are returned withoutinstruction.
Costing is derived from mode. Road data is OpenStreetMap; admin data (state, country) comes from the Valhalla tiles. No Geocoding Waterfall, Nominatim or other geocoder is involved.
Timestamps are used for matching only when every waypoint has one and they are strictly increasing. Otherwise all timestamps are ignored. bearing values are passed through to Valhalla as headings.
Quick start
curl -s -X POST -H "Content-Type: application/json" -d '{"mode":"drive","waypoints":[{"location":[-86.15813,39.7690]},{"location":[-86.15813,39.7705]},{"location":[-86.15813,39.7720]},{"location":[-86.15813,39.7735]},{"location":[-86.15813,39.7750]},{"location":[-86.15813,39.7765]}]}' "https://api.data.graphitelab.ai/v1/mapmatching?apiKey=$GRAPHITE_API_KEY"Response, trimmed to the first waypoint and first step:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"mode": "drive",
"distance": 827,
"time": 74.43,
"legs": [
{
"time": 74.43,
"distance": 827,
"steps": [
{
"from_index": 0,
"to_index": 1,
"time": 1.17,
"distance": 13,
"osm_way_id": 113084204,
"instruction": { "text": "Drive north on North Meridian Street." },
"toll": false,
"ferry": false,
"bridge": false,
"tunnel": false,
"roundabout": false,
"speed": 40,
"speed_limit": 40,
"surface": "paved",
"lane_count": 2,
"road_class": "tertiary",
"name": "North Meridian Street",
"begin_bearing": 2,
"end_bearing": 2
}
]
}
],
"waypoints": [
{
"original_index": 0,
"location": [-86.15802, 39.769051],
"original_location": [-86.15813, 39.769],
"match_type": "matched",
"match_distance": 10.917381,
"leg_index": 0,
"step_index": 0
}
]
},
"geometry": {
"type": "MultiLineString",
"coordinates": [[[-86.15802, 39.769051], [-86.158016, 39.769167], [-86.157723, 39.776491]]]
}
}
],
"bbox": [-86.15802, 39.769051, -86.157723, 39.776491]
}Parameters
Send a JSON body. Only mode and waypoints are required. The apiKey goes in the query string.
| Name | Type | Required | What it does |
|---|---|---|---|
mode | string | yes | Travel profile used to decide which roads can connect the points: walk, hike, scooter, motorcycle, drive, light_truck, bicycle, mountain_bike, road_bike, bus, medium_truck, truck, truck_dangerous_goods, heavy_truck, long_truck. |
waypoints | array | yes | 2 to 1,000 ordered GPS points. Order is preserved, never reordered. |
waypoints[].location | array | yes | [longitude, latitude]. |
waypoints[].timestamp | string | no | ISO 8601 date-time. Used only if all points have one and they strictly increase. |
waypoints[].bearing | number | no | Travel direction in degrees clockwise from north, 0 to 360. |
geocode_step_address | boolean | no | Adds an address object to each step. Default false. |
lookup_step_osmtags | boolean | no | Adds osmtags to each step. Default false. |
lookup_osm_overlap | boolean | no | Adds overlaps to each step. Default false. |
lang | string | no | Instruction language: en, de, es, fr, hi, it, pt, ru, sl, sv, ca, cs. Default en. |
Response
The body is a GeoJSON FeatureCollection with one Feature and a top-level bbox. This response has no place_id, datasource or rank fields.
| Field | Meaning |
|---|---|
geometry | MultiLineString of the matched route, [longitude, latitude] pairs. |
properties.distance | Total distance in meters. |
properties.time | Total time in seconds. |
properties.legs[0].steps | One step per road edge. Each has distance (meters), time (seconds), osm_way_id, name, road_class, surface, lane_count, speed and speed_limit (km/h), truck_limit when known, flags (toll, ferry, bridge, tunnel, roundabout), begin_bearing and end_bearing. |
steps[].from_index, to_index | Indexes into the matched geometry for the step. |
steps[].instruction.text | Turn instruction in lang. Present only on steps that start a maneuver. |
properties.waypoints | One entry per input point, in input order. |
waypoints[].match_type | matched, unmatched, interpolated or merged. |
waypoints[].location | Snapped position. null when the point is not matched or interpolated. |
waypoints[].original_location | The point you sent. |
waypoints[].match_distance | Distance in meters from your point to the snapped position; null when unmatched. |
waypoints[].leg_index, step_index | Which step the point was matched to. |
Differences from Geoapify
-
unsupported: at least 2 waypoints are required (1 returns 400). The
transitandapproximated_transitmodes are not accepted. -
approximated:
geocode_step_addressreturns onlystreet,state,countryandcountry_code(lower case). Stepinstructioncarries onlytext. -
empty:
lookup_step_osmtagsyieldsosmtags: {}andlookup_osm_overlapyieldsoverlaps: []on every step. -
extra: a trace that cannot be matched to any road returns 400 ("The trace could not be matched to the road network").
Step fields elevation, elevation_gain, max_elevation, min_elevation, admin_area, rightside, traversability, from_stop_* and to_stop_* are not returned. application/geo+json and XML output are not supported.
US and Canada notes
Matching runs on OpenStreetMap roads in the Valhalla tiles for the US and Canada. State and country in step addresses come from the tiles' admin data, not from ZCTA, FSA or Address Point sources. Street names are the OSM way names as stored, so Canadian French names appear as OSM has them, and lang affects only the instruction text.
More examples
With address lookups and timestamps
Sets geocode_step_address, lookup_step_osmtags and lookup_osm_overlap, and sends a strictly increasing timestamp on every point so timing is used for matching. Each step gains address, osmtags (always {}) and overlaps (always []).
curl -s -X POST -H "Content-Type: application/json" -d '{"mode":"drive","geocode_step_address":true,"lookup_step_osmtags":true,"lookup_osm_overlap":true,"waypoints":[{"location":[-86.15813,39.7690],"timestamp":"2026-11-01T14:00:00Z"},{"location":[-86.15813,39.7705],"timestamp":"2026-11-01T14:00:10Z"},{"location":[-86.15813,39.7720],"timestamp":"2026-11-01T14:00:20Z"},{"location":[-86.15813,39.7735],"timestamp":"2026-11-01T14:00:30Z"},{"location":[-86.15813,39.7750],"timestamp":"2026-11-01T14:00:40Z"}]}' "https://api.data.graphitelab.ai/v1/mapmatching?apiKey=$GRAPHITE_API_KEY"A step from the response (trimmed):
{
"from_index": 0,
"to_index": 1,
"time": 1.17,
"distance": 13,
"osm_way_id": 113084204,
"name": "North Meridian Street",
"address": { "street": "North Meridian Street", "state": "None", "country": "None" },
"osmtags": {},
"overlaps": []
}Errors and limits
| Status | Meaning |
|---|---|
| 400 | Invalid or missing parameters: fewer than 2 or more than 1,000 waypoints, bad location, timestamp or bearing, unsupported mode, or a trace that cannot be matched to any road. |
| 401 | Missing or invalid API key. |
| 500 | Unexpected gateway error. |
| 502 | An Engine returned an error. |
| 503 | An Engine is down. |
Limit: 1,000 waypoints per request.
Known issue: with geocode_step_address=true, state and country can come back as the text "None" and country_code is missing, because admin data is missing in the production tiles or is not passed through by the gateway. The same defect affects /routing with details=admin_areas. Treat those values as unreliable until it is fixed.
Try in Playground (signed in to your Graphite Lab organization). Signed out, use the paste-your-key widget on the reference page.