Round trips from a start point

POST https://api.trailsplits.com/v1/roundtrips

Checked by a live probe Key optional · 500 credits a day without one Heavy: 1 at a time on Free

Loops that start and end at a point, from the same engine as the TrailSplits Planner. Each loop reports its target error, the distance walked twice and the same facts as route analysis. Status complete means within 10 % of the target with at most 20 % repeated; rough is answered with a reason; when no good loop exists the answer is 422 no_loop, never an invented loop. Limits: 0.5–40 km on foot, 0.5–60 km by bike (422 too_long), up to 3 alternatives.

Example

Round trips from a start point (curl)
# Without a key, leave out the Authorization line (500 credits a day per address).
curl --fail-with-body 'https://api.trailsplits.com/v1/roundtrips' \
  -H "Authorization: Bearer $TRAILSPLITS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"start":[7.7491,46.0207],"distance_km":12,"profile":"hike"}'

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

Request body

FieldTypeDescription
startrequiredarray[lon, lat]
distance_kmnumber0.5–40 km for hike and run, to 60 km for bike. Or distance_m, not both.
distance_mnumber
profilestring: hike | run | bike
directionnumber or null
alternativesinteger
max_sacinteger
avoid_pavedboolean
hillsnumber
bicycle_typestring: road | hybrid | mountain

Response

FieldTypeDescription
schemastring: roundtrips/1
engineobject
statusstring: complete | rough
reasonstring or null
requestobject
loopsarray
Full response schema (JSON Schema)
{
  "type": "object",
  "properties": {
    "schema": {
      "type": "string",
      "enum": [
        "roundtrips/1"
      ]
    },
    "engine": {
      "type": "object"
    },
    "status": {
      "type": "string",
      "enum": [
        "complete",
        "rough"
      ]
    },
    "reason": {
      "type": "string",
      "nullable": true
    },
    "request": {
      "type": "object"
    },
    "loops": {
      "type": "array",
      "minItems": 1,
      "maxItems": 4,
      "items": {
        "type": "object",
        "properties": {
          "rank": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "complete",
              "rough"
            ]
          },
          "distance_m": {
            "type": "number"
          },
          "target_m": {
            "type": "number"
          },
          "target_error_pct": {
            "type": "number"
          },
          "repeated_m": {
            "type": "number"
          },
          "repeated_share": {
            "type": "number"
          },
          "shape": {
            "type": "string",
            "description": "Polyline6."
          },
          "ascent_m": {
            "type": "number",
            "nullable": true
          },
          "time": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true
          }
        },
        "required": [
          "rank",
          "status",
          "distance_m",
          "target_m",
          "target_error_pct",
          "repeated_share",
          "shape"
        ]
      }
    }
  },
  "required": [
    "schema",
    "engine",
    "status",
    "loops"
  ]
}

Errors

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

Contract

Give a start and a length; get back loops that start and end there, as the TrailSplits Planner builds them, each with the facts route analysis gives: surfaces, ways, tagged grades, climb, the hike time with its scope, and what is not known. When no good loop exists, the answer says so.

Request

{ "start": [7.7491, 46.0207], "distance_km": 12, "profile": "hike", "alternatives": 2 }
Field
start[lon, lat]Where the loop starts and ends
distance_km or distance_mnumberOne of them, not both: the length to aim for. Limits: 0.5 to 40 km for hike and run, to 60 km for bike (below)
profilehike \run \bikeDefault hike
directionbearing in degrees, or nullA preference, not a rule: loops heading that way score better, and the best loop may still head elsewhere when the ground there is much better. Default null (any)
alternatives0 to 3Loops from other directions besides the best; default 0
max_sac1 to 6hike and run only: the hardest tagged SAC grade used; default 3 (T3). A tagged grade above it is never used; untagged paths are unknown, never "easy"
avoid_pavedbooleanDefault false
hills−1 to 1−1 avoids climbing, 1 seeks it; default 0
bicycle_typeroad \hybrid \mountainbike only; default the router's hybrid
callerstringOptional label for logs and receipts

Defaults are listed in request.settings_defaulted. Unknown fields and fields that do not apply (max_sac on a bike) are refused, not ignored.

Response

FieldMeaning
schemaroundtrips/1
statuscomplete: the best loop is within 10 % of the target with at most 20 % walked twice. rough: the best found misses one of those (still answered, with reason). none: no loop (HTTP 422, below)
reasonWhy the best loop is rough, or why there is none; else null
requeststart, target_m, profile, direction, alternatives, settings (the router's), settings_defaulted
loops[]The best first (rank 0), then the alternatives
searchThe engine's own counters (version, tree nodes, candidates, pairs routed, ms, and stopped when the budget ended the search early)
timingTiles and milliseconds

Each loop:

FieldMeaning
statuscomplete or rough, by the rule above
distance_m, target_m, target_error_pctThe length, the target, and the signed error in per cent
repeated_m, repeated_shareMetres walked twice
stem_mOf those, the way out from the start and back along the same path (a start at the end of a spur)
spur_mOf those, dead ends walked into and back out of at the turn points
turn_pointsThe loop's two turn points, [lon, lat]
bearing_degMean bearing of the turn points from the start
qualityCost per metre over the settings' best ground (1 is all best-case ground)
shapeThe loop, polyline6
ascent_m, descent_m, ascent_methodClimb by the elevation contract on the routing graph's heights: route-smoothed-100m, or route-smoothed-100m+gorge40 when the loop runs through a gorge whose heights were bridged (the router core's two names; elevation contract)
timehike only. The calibrated trailsplits-hike/2 (moving time), applies, calibration, outside_calibration with reasons (T4 or harder tagged on 50 m or more, less than half on marked trails), and models (every named model's minutes). Null for run and bike
surfacesAs in route analysis: paved_m, gravel_m, trail_m, unknown_m, mapped_m, class_default_m, stretches[] along the loop
waysby_type_m, marked_m, max_sac, ford_m, aid_m, informal_m, steps_m
hazards[]As in route analysis: each ford, aided passage and via ferrata, with from_m and to_m in metres along the loop from the start
difficultyAs in route analysis: graded_m (T1…T6), untagged_path_m, road_m, hardest
warnings[]The router's own (code, message): fords, aided passages, informal paths, high or unknown difficulty, pushing a bike, a start moved to a reachable path. Their figures are the router's: its unknown_difficulty counts untagged paths, tracks and bridleways, while unknowns.difficulty_untagged also counts footpaths and steps, so the two can differ
unknowns[]time_not_estimated, time_outside_calibration, surface_class_default, surface_unknown, difficulty_untagged (each with code, message, and length_m where it is a length)

Errors

{ "error": { "code", "message" } }.

HTTPcodeWhen
400invalid_requestNot JSON, unknown field, a field that does not apply, a value out of range, start not [lon, lat] or off the map, no distance, both distance fields, a distance under 0.5 km
405method_not_allowedAny method but POST; the answer carries Allow: POST
409capability_unavailablebike while the served routing tiles carry no verified one-ways or turn restrictions (as the Planner's loops)
422too_longOver 40 km on foot or 60 km by bike, checked on the length the engine will build
422no_loopNo loop: the start is not near a path the settings may use, or none was found. The body also carries status: none, reason, request and search
422corridor_too_largeThe area the loop needs is too dense or too large to load
503overloadedThe worker's heavy-job queue is full; Retry-After: 5
503tiles_unavailableRouting tiles could not be read

A loop the work budget cut short is still answered when one was found (search.stopped says so); with none, the reason names the limit, never "no loop exists".

Every request is one heavy job: admission, the shared 25 s and 12 M-node budget, receipts with op: roundtrips, a hard kill 10 s past the deadline.

Case (3 alternatives unless said)TimeTiles
Zermatt hike 12 km (2 alternatives)0.7 s—
Interlaken hike 40 km2.5 s16
Tokyo run 40 km / hike 40 km4.6 s / 4.0 s16
London hike 40 km, New York run 40 km9.5 s, 10.0 s20, 16
Paris bike 60 km, London bike 60 km, Oslo bike 60 km12.4 s, 12.9 s, 9.8 s42, 36, 64
Over the public limits: Tokyo run 60 km24.1 s, at the budget25
Over the public limits: Tokyo bike 80 km21.7 s49
Over the public limits: Interlaken hike 100 km, Oslo bike 70 km, Tromsø hike 60 kmrefused: corridor over 64 tiles—

Hence 40 km on foot and 60 km by bike: inside about half the budget everywhere measured, and inside the 64-tile corridor up to about 60° north (a 60 km loop from Tromsø would still be refused as corridor_too_large).

Consumers

  • The parity script (here) and npm run test:roundtrip.

Versioning

Additive fields may appear in roundtrips/1. A change of meaning, a removed field or a new default is roundtrips/2.

JSON Schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://trailsplits.com/schemas/roundtrips-1.request.json",
  "type": "object",
  "additionalProperties": false,
  "required": ["start"],
  "properties": {
    "start": { "type": "array", "items": { "type": "number" } },
    "distance_km": { "type": "number" },
    "distance_m": { "type": "number" },
    "profile": { "enum": ["hike", "run", "bike"] },
    "direction": { "type": ["number", "null"] },
    "alternatives": { "enum": [0, 1, 2, 3] },
    "max_sac": { "enum": [1, 2, 3, 4, 5, 6] },
    "avoid_paved": { "type": "boolean" },
    "hills": { "type": "number" },
    "bicycle_type": { "enum": ["road", "hybrid", "mountain"] },
    "caller": { "type": "string" }
  }
}
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://trailsplits.com/schemas/roundtrips-1.response.json",
  "type": "object",
  "required": ["schema", "engine", "status", "reason", "request", "loops", "search", "timing"],
  "properties": {
    "schema": { "const": "roundtrips/1" },
    "status": { "enum": ["complete", "rough", "none"] },
    "reason": { "type": ["string", "null"] },
    "request": { "type": "object", "required": ["start", "target_m", "profile", "direction", "alternatives", "settings", "settings_defaulted"] },
    "loops": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["rank", "status", "distance_m", "target_m", "target_error_pct", "repeated_m", "repeated_share", "stem_m", "spur_m", "turn_points", "bearing_deg", "quality", "shape", "ascent_m", "descent_m", "ascent_method", "time", "surfaces", "hazards", "ways", "difficulty", "warnings", "unknowns"],
        "properties": {
          "status": { "enum": ["complete", "rough"] },
          "shape": { "type": "string" },
          "time": { "type": ["object", "null"], "required": ["model", "minutes", "moving_only", "applies", "calibration", "outside_calibration", "reasons", "models"] },
          "surfaces": { "type": "object", "required": ["paved_m", "gravel_m", "trail_m", "unknown_m", "mapped_m", "class_default_m", "stretches"] },
          "ways": { "type": "object", "required": ["by_type_m", "marked_m", "max_sac", "ford_m", "aid_m", "informal_m", "steps_m"] },
          "difficulty": { "type": ["object", "null"], "required": ["scale", "graded_m", "untagged_path_m", "road_m", "hardest"] },
          "warnings": { "type": "array", "items": { "type": "object", "required": ["code", "message"] } },
          "unknowns": { "type": "array", "items": { "type": "object", "required": ["code", "message"] } }
        }
      }
    },
    "search": { "type": "object", "required": ["version", "tree_nodes", "candidates", "pairs_routed"] },
    "timing": { "type": "object" }
  }
}
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://trailsplits.com/schemas/roundtrips-1.error.json",
  "type": "object",
  "required": ["error"],
  "properties": { "error": { "type": "object", "required": ["code", "message"], "properties": { "code": { "type": "string" }, "message": { "type": "string" } } } }
}