POST /v1/match
Overview
Map matching takes a sequence of GPS coordinates (e.g. from a vehicle tracker or fitness app) and snaps them to the most probable route on the actual road network. The result is a clean, road-aligned geometry with distance and duration estimates.
Request
POST https://api.geocodeme.net/v1/match
Content-Type: application/json
Required headers:
X-Api-Key: gm_YOUR_API_KEYContent-Type: application/json
Request Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
coordinates | array | Yes | n/a | Array of [longitude, latitude] pairs (min 2, max 100 points) |
timestamps | array | No | n/a | Array of UNIX timestamps (seconds); must match coordinates length |
radiuses | array | No | n/a | Search radius in meters per coordinate point (default: 5m per point) |
profile | string | No | car | Routing profile: car, bike, foot, truck, or moto (plan-gated, see below) |
geometries | string | No | geojson | Output geometry format: geojson, polyline, or polyline6 |
overview | string | No | full | Route overview detail: full, simplified, or false |
attributes | array | No | n/a | Enrich the response with per-edge segments: any of street_names, speed_limit, road_class (no extra credit cost) |
use_tolls | boolean | No | n/a | Prefer (true) or avoid (false) toll roads, motorized profiles only (400 on bike/foot) |
truck_height / truck_width / truck_weight | number | No | n/a | Vehicle dimensions (m / m / metric tons); require profile: "truck" (400 otherwise) |
Cost: 1 credit, plus 1 per started block of 25 points beyond the first 25 (25 points → 1 credit, 26 → 2, 100 → 4). See Credits & quotas.
Plan gating: car is available on every plan; bike and foot require
Startup or higher; truck and moto require Growth or higher. A profile
outside your plan returns 403 PLAN_UPGRADE_REQUIRED, and never consumes
credits. When attributes is requested, the response carries a segments
array (length_meters, plus the requested attributes); if the enrichment is
temporarily unavailable, segments is null and confidence is 0.0;
the matched geometry itself is unaffected.
Coordinates are in [longitude, latitude] order (GeoJSON standard), not [lat, lon]. This is the reverse of the /v1/geocode and /v1/reverse query parameters.
The coordinates array must contain at least 2 and at most 100 points. The API returns an INVALID_PARAMETER error if the array is outside this range. For a single point, use /v1/nearest instead.
The format query parameter is not supported on this endpoint. The geometry format is controlled by the geometries request body field (geojson, polyline, or polyline6).
Code Examples
- cURL
- Python
- JavaScript
- PHP
curl -X POST \
-H "X-Api-Key: gm_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"coordinates": [
[2.3522, 48.8566],
[2.3488, 48.8534],
[2.3470, 48.8521]
],
"profile": "car",
"geometries": "geojson"
}' \
"https://api.geocodeme.net/v1/match"
import os
import requests
api_key = os.environ["GEOCODEME_API_KEY"]
response = requests.post(
"https://api.geocodeme.net/v1/match",
json={
"coordinates": [
[2.3522, 48.8566],
[2.3488, 48.8534],
[2.3470, 48.8521],
],
"profile": "car",
"geometries": "geojson",
},
headers={"X-Api-Key": api_key},
)
response.raise_for_status()
data = response.json()
print(f"Confidence: {data['confidence']}")
print(f"Distance: {data['distance_meters']}m")
const apiKey = process.env.GEOCODEME_API_KEY;
const response = await fetch("https://api.geocodeme.net/v1/match", {
method: "POST",
headers: {
"X-Api-Key": apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
coordinates: [
[2.3522, 48.8566],
[2.3488, 48.8534],
[2.3470, 48.8521],
],
profile: "car",
geometries: "geojson",
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log(`Confidence: ${data.confidence}`);
console.log(`Distance: ${data.distance_meters}m`);
<?php
$apiKey = getenv('GEOCODEME_API_KEY');
$payload = json_encode([
'coordinates' => [
[2.3522, 48.8566],
[2.3488, 48.8534],
[2.3470, 48.8521],
],
'profile' => 'car',
'geometries' => 'geojson',
]);
$ch = curl_init('https://api.geocodeme.net/v1/match');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"X-Api-Key: $apiKey",
'Content-Type: application/json',
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new \RuntimeException("HTTP $httpCode: $response");
}
$data = json_decode($response, true);
echo "Confidence: " . $data['confidence'] . "\n";
echo "Distance: " . $data['distance_meters'] . "m\n";
Response
All responses include rate limiting headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Monthly-Remaining, etc.).
With geometries=geojson (default):
{
"matched_coordinates": [
[2.3522, 48.8566],
[2.3510, 48.8555],
[2.3495, 48.8540],
[2.3488, 48.8534],
[2.3475, 48.8525],
[2.3470, 48.8521]
],
"geometry_polyline": null,
"confidence": 0.95,
"distance_meters": 620.4,
"duration_seconds": 78.2,
"attribution": "Route data (c) OpenStreetMap contributors",
"meta": {
"took_ms": 62,
"cache_hit": false
}
}
With geometries=polyline (or polyline6), the geometry is returned as an encoded string instead:
{
"matched_coordinates": null,
"geometry_polyline": "u`fiHiuqM^`BhAtEjBdI",
"confidence": 0.95,
"distance_meters": 620.4,
"duration_seconds": 78.2,
"attribution": "Route data (c) OpenStreetMap contributors",
"meta": {
"took_ms": 62,
"cache_hit": false
}
}
Schema
| Field | Type | Description |
|---|---|---|
matched_coordinates | array | null | Snapped route as [[longitude, latitude], ...] pairs; set when geometries=geojson, null otherwise |
geometry_polyline | string | null | Encoded polyline string; set when geometries=polyline or polyline6, null otherwise |
confidence | float | Match confidence between 0 and 1 |
distance_meters | float | Total matched route distance in meters |
duration_seconds | float | Travel duration in seconds. Estimated from the road network, or, when you send timestamps, read from them: the elapsed time between your first and last matched point, stops included |
attribution | string | Always "Route data (c) OpenStreetMap contributors" |
meta.took_ms | integer | Server processing time in milliseconds |
meta.cache_hit | boolean | Whether the response was served from cache |
When overview=false, both matched_coordinates and geometry_polyline are null; only confidence, distance, and duration are returned.
Traces with gaps
Some traces cannot be matched as one continuous drive. A U-turn is the common case: the road cannot be joined to itself in the opposite direction, so the matcher walks the trace in several pieces. Out-and-back routes and loops therefore contain at least one gap.
The response still describes the whole trace. matched_coordinates and
geometry_polyline cover it from end to end, joined by a straight line across
each gap, and distance_meters counts every piece. Only the segment indices
carry the seam, as described below.
If you need to know where the gaps are, look for the breaks in the segment
indices: a segment whose from_index is not the previous to_index starts a
new piece.
Segments (attributes)
When attributes is requested, the response carries a segments array, one
entry per matched road edge:
{
"segments": [
{
"street_names": ["Quai Perrache"],
"road_class": "primary",
"length_meters": 650.0,
"speed_limit_kph": null,
"from_index": 0,
"to_index": 12
},
{
"street_names": ["A7", "E15"],
"road_class": "motorway",
"length_meters": 4300.0,
"speed_limit_kph": 130,
"from_index": 12,
"to_index": 47
}
]
}
| Field | Type | Description |
|---|---|---|
length_meters | float | Length of the edge in meters |
street_names | array | null | Street names, when street_names was requested |
speed_limit_kph | integer | null | Posted speed limit, when speed_limit was requested and known |
road_class | string | null | Road classification, when road_class was requested |
from_index | integer | null | Index of the segment's first vertex in the geometry returned by this response |
to_index | integer | null | Index of the segment's last vertex in the same geometry |
from_index and to_index locate the segment on the geometry returned in the
same response, decoded. Both bounds are inclusive, so consecutive segments
normally share their junction vertex: segments[i].to_index == segments[i+1].from_index.
The first segment starts at 0 and the last ends at len(geometry) - 1.
The one exception is a gap (see below): where the trace could not be matched
end to end, the next segment starts one vertex further on
(segments[i+1].from_index == segments[i].to_index + 1). Walk the array with
from_index rather than assuming the previous to_index.
Geometry and attributes are computed by two separate engine calls. When their
shapes cannot be proven identical, from_index and to_index are left out
rather than approximated: an index pointing at a shape that was not served
would label the wrong stretch of road while looking perfectly valid. They are
also absent with overview=false, since no geometry is returned to index into.
Treat their absence as "position unknown" and fall back accordingly. Never
reconstruct a position by accumulating length_meters: that value is 0 when
the data is missing, so the result would be silently wrong.
Error Responses
{
"error": {
"code": "INVALID_PARAMETER",
"message": "Parameter 'coordinates' must contain between 2 and 100 coordinate pairs",
"field": "coordinates"
}
}
All endpoints are also subject to INVALID_API_KEY (401) and RATE_LIMIT_EXCEEDED / QUOTA_EXCEEDED (429) errors. See Error Codes for the full reference.