Map matching and trace attributes

POST https://api.trailsplits.com/valhalla/trace_attributes

Checked by a live probe Key optional · 500 credits a day without one

Matches a line to the mapped network and returns each way it follows with surface (and whether the surface is mapped or a class default), use and SAC grade, plus the matched line per segment, unmatched stretches with reasons, and three lengths. Valhalla-compatible input; up to 2,000 points. Edge lengths are kilometres. For a full analysis with climb, time and unknowns use route analysis.

Example

Map matching and trace attributes (curl)
# Without a key, leave out the Authorization line (500 credits a day per address).
curl --fail-with-body 'https://api.trailsplits.com/valhalla/trace_attributes' \
  -H "Authorization: Bearer $TRAILSPLITS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"shape":[{"lat":46.5369,"lon":12.1352},{"lat":46.536,"lon":12.133}],"costing":"pedestrian","shape_match":"map_snap"}'

Sends a real request, with your key or the keyless allowance.

Request body

FieldTypeDescription
shaperequiredarray
costingrequiredstring: pedestrian | bicycle
shape_matchstring: map_snap

Response

FieldTypeDescription
edgesarray
shapestringPolyline6 of the matched line, only when the match is one segment.
unitsstring: kilometers
trailsplitsobject
Full response schema (JSON Schema)
{
  "type": "object",
  "properties": {
    "edges": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "length": {
            "type": "number",
            "description": "Kilometres."
          },
          "begin_shape_index": {
            "type": "integer"
          },
          "end_shape_index": {
            "type": "integer"
          },
          "surface": {
            "type": "string",
            "nullable": true
          },
          "trailsplits_surface_tagged": {
            "type": "boolean"
          },
          "use": {
            "type": "string",
            "nullable": true
          },
          "sac_scale": {
            "type": "integer",
            "nullable": true
          }
        },
        "required": [
          "length"
        ]
      }
    },
    "shape": {
      "type": "string",
      "description": "Polyline6 of the matched line, only when the match is one segment."
    },
    "units": {
      "type": "string",
      "enum": [
        "kilometers"
      ]
    },
    "trailsplits": {
      "type": "object",
      "properties": {
        "status": {
          "type": "string",
          "enum": [
            "matched",
            "partial",
            "unmatched"
          ]
        },
        "coverage": {
          "type": "number"
        },
        "trace_length_m": {
          "type": "number"
        },
        "matched_length_m": {
          "type": "number"
        },
        "segments": {
          "type": "array",
          "items": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "unmatched": {
          "type": "array",
          "items": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "required": [
        "status",
        "coverage",
        "segments"
      ]
    }
  },
  "required": [
    "edges",
    "trailsplits"
  ]
}

Errors

400, 422, plus the gateway's 401, 429 and 503 (error conventions). A refused request costs no credits.

Contract

What the answer now carries

FieldMeaning
shapeValhalla's top-level matched line, polyline6 ([lat, lon] × 1e6, Valhalla's encoding). Only when the match is one segment.
trailsplits.segments[i].shapeThe line of matched segment i, polyline6. Always present for every segment.
edges[k].begin_shape_index, edges[k].end_shape_indexFirst and last vertex of edge k in the line of its segment, inclusive (Valhalla's names). Consecutive edges of a segment share a vertex: end of one is begin of the next.
edges[k].trailsplits_segmentThe segment whose line the indices refer to.

Unchanged:

  • edges[k].length is in kilometres (units: "kilometers"), and equals the length of the edge's stretch of the line within rounding.
  • The facts per edge, including trailsplits_surface_tagged: false means the surface is the class default, not a mapped surface.

Lengths: three different facts

FieldWhat it measures
trailsplits.trace_length_mThe authored trace, end to end
trailsplits.matched_length_mThe authored trace inside matched segments, measured along the trace
trailsplits.segments[i].distance_m = Σ edges.length of the segment × 1000The network path the trace was matched to

coverage = matched_length_m / trace_length_m. A coverage of 1 says the whole trace was matched. It does not say that the network path is as long as the trace.

Gaps and refusals

  • Partial match (trailsplits.status: "partial"):
    • several segments, each with its own line;
    • no top-level shape, and no line is ever drawn across an unmatched stretch;
    • the stretches are listed in trailsplits.unmatched (fromIndex, toIndex, length_m, reason: no_edge_nearby or no_connection).
  • No match (status: "unmatched"): HTTP 422 with Valhalla's error 443 and trailsplits.code: "no_match".
  • Never changed: the authored trace is returned as sent; endpoints are never rerouted.
  • Bicycle traces are answered by the same matcher.

Controls (2 Oct 2026, router fbf1ee8f2)

CaseResult
A01 (Dolomites, 2 points, docs/b2b/commercial/benchmarks/observations-2026-10-02.json)Matched, coverage 1, 1 segment, 49 vertices. Edges contiguous from 0 to 48. The line measures 393.6 m, the segment 395 m and the edges 394.6 m; the trace is 196 m.
A02 (Zermatt, 14 points)Matched, coverage 1, 1 segment. Edges contiguous from 0 to 28.

Consumers

  • DX: examples and the public reference.