A snap to road API moves each GPS point onto the road or path the traveler most likely used and returns one clean route, and the travel mode you send decides which roads are allowed. In our test, the same Manhattan track was snapped to the Hudson River Greenway bike path as bicycle and to West Street (NY 9A) next to it as drive. One parameter changed. This post shows how the Geoapify Map Matching API works: the request, the response fields that tell you how well each point matched, the 15 travel modes, and the credits.
Key takeaways
- One POST, up to 1,000 points: send
modeandwaypointswith[lon, lat]locations; timestamps and bearings are optional. - The mode sets the network:
bicyclematched a West Side track to the Greenway (average match distance 4.4 m),drivematched it to NY 9A (21.1 m). - A wrong mode is visible: a car track on the Henry Hudson Parkway matched as
truckleft 28 of 35 pointsunmatched, because trucks are not allowed on that parkway. - Cheap per track: 1 credit per started 100 points, so an 8-hour shift with a point every 30 seconds costs 10 credits.
On this page
What Does Snap to Road Mean?
Snap to road, also called map matching or GPS trace matching, is the process of turning a sequence of recorded GPS points into a route on the road network. Each point is moved to the most likely position on a road or path, and the matched positions are connected into one continuous line that follows the real geometry of the streets.
A raw GPS track drawn on a map zigzags. Points land on sidewalks, rooftops and the river, and a line drawn straight between them cuts through buildings. A matched route has none of this: it bends where the road bends, stays on one side of a divided highway, and comes with road names, road classes and speed limits for every segment.
That is what makes the output useful. You can measure the real driven distance, check which roads a vehicle used, compare speed with speed limits, or draw a clean trip line in a fleet dashboard. The difference from routing is direction: routing builds a new route between stops, while map matching reconstructs a route that already happened.
bicycle the route follows the Hudson River Greenway (green), as drive it follows NY 9A, West Street (amber). The right panel zooms in near Pier 40, where the two lines run about 30 m apart; the first ~70 m of the drive match, on Chambers Street, is not drawn. Routes from the Geoapify Map Matching API. Map: Geoapify, © OpenMapTiles, © OpenStreetMap contributors.Why Do GPS Points Drift Off the Road?
A GPS position is an estimate, not a measurement of where the wheels are. Several things push it off the road:
- Receiver accuracy. Phones and trackers usually report positions within several meters of the true location, and the error changes from one fix to the next.
- Urban canyons. Tall buildings block satellites and reflect signals, so the receiver can compute a position on the wrong side of the street or a block away.
- Tree cover and tunnels. Dense foliage weakens the signal, and in tunnels the receiver can lose it completely.
- Sampling interval. When points are recorded far apart, a straight line between them cuts corners and skips turns, even if every point is accurate.
A snap to road algorithm handles all four at once. It looks at the whole sequence, not each point alone, and picks the road path that best explains the points in order. A point that is 15 m off the road but between two good points is pulled back onto the path; a point with no plausible road nearby is reported as unmatched instead of being forced onto the map.
How Do You Send a GPS Track to a Snap to Road API?
The Map Matching API is a POST request with a JSON body. The API reference documents these body fields:
| Field | Required | Description |
|---|---|---|
mode | Yes | Travel mode, one of 15 values, the same as in the Routing API |
waypoints | Yes | Array of up to 1,000 GPS points, in the order they were recorded |
waypoints[].location | Yes | [longitude, latitude] |
waypoints[].timestamp | No | ISO timestamp; helps calculate effective speed and filter incorrect waypoints |
waypoints[].bearing | No | Number from 0 to 360; helps determine speed more accurately |
geocode_step_address | No | true adds an address object to each matched step; +1 credit per step |
lookup_step_osmtags | No | true adds the OpenStreetMap tags of each step's way, such as highway, surface and maxspeed |
This REST request sends the start of our Henry Hudson Parkway test track, recorded every 8 seconds, as a drive trip:
curl -X POST "https://api.geoapify.com/v1/mapmatching?apiKey=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "drive",
"waypoints": [
{ "location": [-73.986555, 40.782006], "timestamp": "2026-11-04T14:00:00.000Z" },
{ "location": [-73.985279, 40.783501], "timestamp": "2026-11-04T14:00:08.000Z" }
]
}'The full track has 35 points; add the rest to the waypoints array in the same format. To try a track without code, paste it into the Map Matching playground.
Three practical notes. Coordinates are [lon, lat], like GeoJSON, which is the opposite of the lat,lon order in Routing API URLs. Keep points in recorded order, because the matcher reads them as a sequence. Split long tracks into chronological chunks of 1,000 points or fewer; to match many tracks at once, send them as one job to the Batch API.
If your tracks come from GPX files, each track point (trkpt) becomes one waypoint: its lon and lat attributes go into location, and its time element into timestamp. The how-to Snap a GPS Track to the Road Network walks through the full request and how to check match quality.
What Does the API Return?
The response is a GeoJSON FeatureCollection with one MultiLineString feature: the matched route. Its properties hold the totals and two arrays you will use most:
distance(meters) andtime(seconds) for the whole route, plus the requestedmode;waypoints[]: one entry per GPS point you sent, with the original and matched location;legs[].steps[]: the route split into segments with the same road properties.
Simplified response for the Henry Hudson Parkway track matched as drive, trimmed to the first waypoint and the first step:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"distance": 4691,
"time": 213,
"mode": "drive",
"waypoints": [
{
"original_index": 0,
"location": [-73.986469, 40.78197],
"original_location": [-73.986555, 40.782006],
"match_type": "matched",
"match_distance": 8.220287,
"leg_index": 0,
"step_index": 0
}
],
"legs": [
{
"distance": 4691,
"time": 213,
"steps": [
{
"name": "HH, NY 9A North, Henry Hudson Parkway",
"road_class": "motorway",
"surface": "paved_smooth",
"lane_count": 3,
"speed": 80,
"speed_limit": 80,
"traversability": "forward",
"toll": false,
"tunnel": false,
"bridge": false,
"roundabout": false,
"time": 9,
"distance": 198,
"begin_bearing": 29,
"end_bearing": 29,
"from_index": 0,
"to_index": 2,
"osm_way_id": 340825593
}
]
}
]
},
"geometry": { "type": "MultiLineString", "coordinates": [] }
}
]
}Waypoints tell you how well each point matched
Each entry in waypoints[] links one GPS point to the route. original_index is its position in your request, original_location is what you sent, and location is where it landed on the road. match_distance is the distance in meters between the two: here the first point moved 8.2 m onto the parkway. leg_index and step_index tell you which part of the route the point belongs to.
match_type has four documented values:
match_type | Meaning |
|---|---|
matched | The point was associated with a road position |
interpolated | The position was inferred from surrounding matched points |
merged | The point was combined with a nearby point |
unmatched | No suitable road position was found |
location, the match_distance in meters from the original point, and a match_type. The JSON is the first waypoint of the bicycle match from Test A below, with ±20 m noise.Steps carry the road details
Each step is a stretch of road with the same properties. The fields include name, road_class (motorway, trunk, primary, secondary, tertiary, unclassified, residential, service_other), surface, traversability (forward, backward, both), the calculated speed, speed_limit, lane_count, the toll, tunnel, bridge and roundabout flags, time, distance, and begin_bearing / end_bearing. from_index and to_index point to the step's part of the geometry, and osm_way_id is the OpenStreetMap way the step follows.
The drive track above returned 17 steps, all with the motorway road class on the Henry Hudson Parkway. You get these road details in every response, so one request is enough to compare the actual speed with the speed_limit or to sum distances by road_class.
Why Does the Travel Mode Decide the Road?
The travel mode defines which part of the road network the matcher may use. A bike can use a greenway but not a motorway; a truck cannot use a road closed to trucks. When two networks run side by side, the mode alone decides which one the points are snapped to. We ran two tests in Manhattan on November 4, 2026.
Test A: a bike path and a highway side by side
On the Lower Manhattan West Side, the Hudson River Greenway runs right next to West Street (NY 9A). We built a 22-point track from a bike route between Chambers Street and West 11th Street, one point every ~95 m and 20 seconds, and moved every point randomly by up to ±8 m. Then we matched it with two modes:
| Mode | Route | Main road | All points matched | Average match_distance |
|---|---|---|---|---|
bicycle | 2,513 m | Hudson River Greenway, 1,649 m | 22 of 22 | 4.4 m |
drive | 2,606 m | NY 9A, West Street, 1,714 m | 22 of 22 | 21.1 m |
Both modes matched every point, so match_type alone would not tell you which is right. The average match distance does: the bike points sit on the Greenway, and the car line is on the road next to it, about 20 m away.
We then increased the noise. With ±20 m, the bicycle match had an average match distance of 10.7 m and still put 1,641 m on the Greenway, while drive stayed on NY 9A (1,714 m, 21.1 m on average); with ±35 m, 17.8 m and 1,664 m. The match stayed on the same path as the noise grew. Removing the timestamps gave the same result for this track.
Test B: a car track on a parkway
The second track is a car trip on the Henry Hudson Parkway on the Upper West Side, from the West 79th Street area to the West 125th Street area: 35 points, 4.7 km, one point every 8 seconds. We matched it with three modes (unmatched points have no match_distance, so the averages cover matched points only):
| Mode | Matched points | Result | Average match_distance |
|---|---|---|---|
drive | 35 of 35 | 4,691 m, all on the Henry Hudson Parkway (motorway) | 3.5 m |
truck | 7 of 35 | 28 points unmatched; 585 m of route in 3 disconnected legs | 37.2 m (7 matched points) |
bicycle | 34 of 35 | Riverside paths: Cherry Walk 2,437 m, Hudson River Greenway 363 m, Riverside Walk 350 m | 36 m |
drive is the mode that recorded the track, and it shows: every point matched, 3.5 m on average. The truck result is the documented unmatched match type doing its job. Commercial vehicles are not allowed on New York City parkways, and the NYC DOT Parkway Truck Restrictions page lists the Henry Hudson Parkway north of 59th Street. With no allowed road near most points, the API reported them as unmatched instead of forcing the track onto the parkway, and the few matched pieces came back as three separate legs.
The bicycle match is the subtle one. It matched 34 of 35 points, but onto the waterfront paths that run beside the parkway, with an average match distance of 36 m, ten times the drive value. A high match rate does not prove the mode is right.
drive matches all 35 points to the Henry Hudson Parkway, bicycle moves them to the riverside paths, and truck leaves 28 points unmatched because trucks are not allowed on the parkway. Insets A and B zoom in on the south end and on Cherry Walk. Routes from the Geoapify Map Matching API. Map: Geoapify, © OpenMapTiles, © OpenStreetMap contributors.The lesson: the mode decides the allowed network. A wrong mode shows up as many unmatched points, or as a high average match distance when a parallel network exists.
Which Travel Mode Should You Choose?
The Map Matching API accepts 15 travel modes. The docs state that the modes are the same as in the Routing API, with vehicle sizes and descriptions in the travel modes table:
| Group | Modes | Use for tracks from |
|---|---|---|
| On foot | walk, hike | Pedestrians, field workers; hike also uses hiking trails |
| Bicycle | bicycle, mountain_bike, road_bike | City bikes and bike couriers, mountain bikes, road bikes |
| Car and two-wheelers | drive, scooter, motorcycle | Passenger cars, motor scooters and mopeds, motorcycles |
| Trucks | light_truck, medium_truck, truck, heavy_truck, truck_dangerous_goods, long_truck | Delivery vans and small trucks (light_truck, 3.5 t), larger trucks by weight, trucks with dangerous goods, long trucks |
| Bus | bus | Buses; the mode checks bus access |
Choosing well comes down to a short checklist:
- Pick the mode that recorded the track. Use the vehicle type from your fleet data, not a default
drivefor everything. - Watch the unmatched share. Many
unmatchedpoints mean the mode is not allowed on the roads the vehicle used, or the data has gaps. - Compare the average
match_distance. A value far above the device's usual error suggests a parallel network, as withbicycleon the parkway track. - Look for detours. If the matched
distanceis much longer than the straight path through the points, the matcher may be avoiding a road the mode cannot use.
If you are not sure which mode produced a track, match it with two candidates and keep the one with fewer unmatched points and the lower average match distance. For details on the bike profiles, see bicycle route planning with an API; for truck modes and restrictions, see truck route planning.
How Much Does Map Matching Cost?
Map matching is priced per started 100 waypoints. The Map Matching API pricing documents the formula max(1, ceil(waypoints / 100)) credits per request:
| Request | Credits |
|---|---|
| 35 points (the parkway track) | 1 |
| 567 points | 6 |
567 points with geocode_step_address=true and 42 matched steps | 6 + 42 = 48 |
| 1,000 points (the maximum) | 10 |
Any request sent through the Batch API with the default priority=0.5 | The total above × 0.5 |
Step addresses are the expensive part: each geocoded step adds 1 credit, so request them only when you need street-level addresses. The Batch API multiplies the per-request credits by its priority: 0.5 by default, for jobs that can wait up to a few hours; priority=1 processes faster but without the discount.
Example: a fleet of 200 vehicles. Each vehicle records one point every 30 seconds during an 8-hour shift: 8 × 120 = 960 points, or 10 credits per vehicle-day. For 200 vehicles, that is 2,000 credits per day, within the Free plan's 3,000 credits per day. See Geoapify pricing for paid plans.
Frequently Asked Questions
What is a snap to road API?
A snap to road API, also called a map matching API, takes an ordered list of GPS points and returns the route on the road network that the traveler most likely used. The Geoapify Map Matching API returns the route as GeoJSON with the matched position of each point and road details such as road class, speed limit and lanes.
Map matching or routing: what is the difference?
Map matching follows recorded points and reconstructs a trip that already happened. Routing builds a new route between waypoints you choose. Use the Map Matching API for GPS tracks and the Routing API for directions. Both accept the same travel modes.
How many GPS points can I send in one request?
Up to 1,000 waypoints per POST request. Split longer tracks into chronological chunks. To match many tracks at once, use the Batch API: with the default priority=0.5, credits are multiplied by 0.5.
What does an unmatched point mean?
match_type: "unmatched" means no suitable road position was found for that point in the requested mode. Check the mode first: in our test, a car track on the Henry Hudson Parkway matched as truck left 28 of 35 points unmatched, because trucks are not allowed on that parkway. Also check the data for gaps or bad fixes. See the snap a GPS track how-to.
Do I need timestamps?
No, timestamps are optional. When you send them as ISO strings, they help the API calculate effective speed and filter incorrect waypoints. You can also send an optional bearing from 0 to 360. See the API reference.

