How to Plan a Bicycle Route with Bike Type, Surface, and Elevation

API or service
Routing API
Task
Start, destination, and bike type → route with surface and elevation
Examples
URLJavaScriptAI
Difficulty
Intermediate
Time
12 min

Plan a bicycle route with bike type, surface, and elevation

You have a start and a destination for a ride and need a route that suits the bike: a city bike, a mountain bike on gravel trails, or a road bike that should stay on pavement. Bicycle route planning with the Routing API takes the bike type as the mode parameter, and the optional details parameter adds the road surface, road class, and elevation of every part of the route.

This guide rides from 1001 Arapahoe Avenue, Boulder, CO 80302, to 350 Terry Street, Longmont, CO 80501. It compares the bicycle, mountain_bike, and road_bike modes, adds up the meters on each surface, and summarizes climbs and descents from the elevation data.

Task flow: start and destination → bike mode → Routing API with route_details and elevation → distance and time per mode → meters by surface → climb and descent summary.

Choose a bike mode and route details

The Routing API has three bike modes. Each one changes which ways the router prefers and how fast it expects the rider to travel:

Mode Bike type in the API documentation Use it for
bicycle A city bicycle Everyday rides, commuting, and bike-share trips
mountain_bike A mountain bicycle to ride on unpaved surfaces but slower on paved terrain Riders who accept gravel and dirt trails
road_bike A road-style bicycle with narrow tires and lightweight construction Riders who need paved roads and paths

The Routing API has no separate e-bike mode. The scooter mode describes a motor scooter or moped, not a kick scooter or a bicycle, so do not use it for bike routes.

Request extra data with the comma-separated details parameter:

Value What it adds Where it appears
route_details Road surface, road class, road name, estimated speed, speed limits, and bridge, tunnel, and roundabout flags for each route segment results[].legs[].steps[]
elevation A height for every geometry point and a [distance, height] profile; minimum, maximum, average elevation, and elevation gain for each step results[].legs[] and results[].legs[].steps[]

When route_details is requested, the API splits the route into more steps. A new step starts wherever a road property changes, such as the surface or road class, so many steps have no turn instruction. Without route_details, a step usually covers one maneuver.

The surface values are paved_smooth, paved, paved_rough, compacted, dirt, gravel, path, and impassable. The road class values are motorway, trunk, primary, secondary, tertiary, unclassified, residential, and service_other. In the examples in this guide, bike paths and regional trails appear as service_other.

Both details increase the cost of a request. The Credits and long bike routes section of this guide shows the calculation.

Compare bicycle, mountain bike, and road bike routes

Send the same waypoints three times and change only mode. The start is 1001 Arapahoe Avenue, Boulder, CO 80302 (40.013757,-105.280843), and the destination is 350 Terry Street, Longmont, CO 80501 (40.1653743,-105.1047757). Both coordinates come from the Geocoding API. Waypoints use latitude,longitude order.

https://api.geoapify.com/v1/routing?waypoints=40.013757,-105.280843|40.1653743,-105.1047757&mode=bicycle&format=json&apiKey=YOUR_API_KEY
https://api.geoapify.com/v1/routing?waypoints=40.013757,-105.280843|40.1653743,-105.1047757&mode=mountain_bike&format=json&apiKey=YOUR_API_KEY
https://api.geoapify.com/v1/routing?waypoints=40.013757,-105.280843|40.1653743,-105.1047757&mode=road_bike&format=json&apiKey=YOUR_API_KEY

Reduced response for mode=mountain_bike:

{
  "results": [
    {
      "mode": "mountain_bike",
      "waypoints": [
        { "location": [-105.280843, 40.013757], "original_index": 0 },
        { "location": [-105.104775, 40.165374], "original_index": 1 }
      ],
      "units": "metric",
      "distance": 29093,
      "distance_units": "meters",
      "time": 7105.532
    }
  ]
}

The bicycle and road_bike responses return "distance": 32107.999999999996. Round distances before you display them. The response waypoints use [longitude, latitude] order.

Mode Distance Time Average speed Gravel Compacted
bicycle 32.1 km 1 h 47 min (6,418 s) 18.0 km/h 0 m 40 m
mountain_bike 29.1 km 1 h 58 min (7,106 s) 14.7 km/h 8,113 m 2,353 m
road_bike 32.1 km 1 h 18 min (4,671 s) 24.7 km/h 0 m 40 m

Average speed is distance / time × 3.6, for example 29093 / 7105.532 × 3.6 = 14.7 km/h. The surface columns come from the same requests with details=route_details; the next sections show how to calculate them.

The mountain_bike route is 3 km shorter because it leaves the paved roads for the gravel Longmont-to-Boulder (LOBO) Regional Trail and the Niwot Loop Trail. It still takes longer because the mode expects lower speeds. The bicycle and road_bike modes return the same geometry on this trip, through 63rd Street and the Dry Creek Trail. Only the travel time differs.

The modes do not always share a route. From the same start to 911 10th Street, Golden, CO 80401 (39.756696,-105.2245561), all three modes return different routes:

Mode Distance Time Gravel Compacted
bicycle 51.7 km (51,671 m) 2 h 59 min 144 m 1,140 m
mountain_bike 51.6 km (51,590 m) 3 h 22 min 1,146 m 839 m
road_bike 51.7 km (51,743 m) 2 h 9 min 0 m 0 m

On this trip, road_bike is the only mode that stays on paved surfaces for the whole route.

Show riders the route of the mode that matches their bike rather than the fastest result. When you compare modes in your application, check the geometry and the surface summary as well as the time: two modes can share a route, and a shorter route can take longer.

Request surface and road class

Add details=route_details to the mountain bike request:

https://api.geoapify.com/v1/routing?waypoints=40.013757,-105.280843|40.1653743,-105.1047757&mode=mountain_bike&details=route_details&format=json&apiKey=YOUR_API_KEY

Reduced response:

{
  "results": [
    {
      "mode": "mountain_bike",
      "distance": 29093,
      "time": 7105.532,
      "legs": [
        {
          "distance": 29093,
          "time": 7105.532,
          "steps": [
            {
              "from_index": 0,
              "to_index": 1,
              "distance": 22,
              "time": 5.210826923076923,
              "surface": "paved_smooth",
              "road_class": "tertiary",
              "name": "Arapahoe Avenue",
              "speed": 22,
              "elevation": 1630,
              "instruction": { "text": "Bike east on Arapahoe Avenue." }
            },
            {
              "from_index": 696,
              "to_index": 720,
              "distance": 498,
              "time": 131.355133192389,
              "surface": "gravel",
              "road_class": "service_other",
              "name": "Longmont-to-Boulder (LOBO) Regional Trail",
              "speed": 7,
              "elevation": 1570,
              "instruction": { "text": "Turn right onto Longmont-to-Boulder (LOBO) Regional Trail." }
            },
            {
              "from_index": 1228,
              "to_index": 1228,
              "distance": 0,
              "time": 0,
              "instruction": { "text": "Your destination is on the right." }
            }
          ]
        }
      ]
    }
  ]
}

The complete leg has 170 steps. Without route_details, the same route has 62 steps.

Read these step fields for a bike route:

  • surface and road_class describe the whole step.
  • distance is the step length in meters, rounded to a whole number. time is in seconds.
  • from_index and to_index point to the first and last coordinates of the step in the leg geometry. Use them to color the route line by surface.
  • speed is an estimated speed for the road segment. It does not match distance / time of the step: on Arapahoe Avenue, speed is 22, while 22 m in 5.21 s is 15.2 km/h. Use the leg and route time for travel times, and do not derive per-surface speeds from step values.
  • instruction is missing on steps that continue straight without a maneuver.
  • The final arrival step has distance: 0 and no road properties. Skip it when you sum surfaces.

Road attributes come from map data. A trail can be tagged gravel in one section and compacted in the next, and some ways have no surface information.

Summarize meters by surface

Add up distance for each value of a step field across all legs. The same function works for surface and road_class. It expects a JSON response (format=json) requested with details=route_details.

function sumStepDistance(routingResponse, field) {
  const totals = {};

  for (const leg of routingResponse.results[0].legs) {
    for (const step of leg.steps) {
      // The arrival step has zero distance and no road properties.
      if (!step.distance) continue;

      const value = step[field] ?? "unknown";
      totals[value] = (totals[value] ?? 0) + step.distance;
    }
  }

  return totals;
}

function toShares(totals) {
  const total = Object.values(totals).reduce((sum, meters) => sum + meters, 0);

  return Object.entries(totals)
    .sort(([, a], [, b]) => b - a)
    .map(([value, meters]) => ({
      value,
      meters,
      share: Math.round(meters / total * 1000) / 10 // percent, one decimal
    }));
}

const surfaces = toShares(sumStepDistance(routingResponse, "surface"));
const roadClasses = toShares(sumStepDistance(routingResponse, "road_class"));

Result for the mountain_bike route from the previous section:

[
  { "value": "paved_smooth", "meters": 18456, "share": 63.5 },
  { "value": "gravel", "meters": 8113, "share": 27.9 },
  { "value": "compacted", "meters": 2353, "share": 8.1 },
  { "value": "paved", "meters": 136, "share": 0.5 }
]

By road class, the same route has 22,383 m of service_other (77%), 2,951 m of tertiary, 1,947 m of secondary, and 1,777 m of residential.

The step distances add up to 29,058 m, while the route distance is 29,093 m. The 35 m difference comes from rounding each of the 169 step distances to whole meters. Calculate shares from the sum of step distances, as the function does, so that they add up to 100%. Show the route distance as the total length of the ride.

For the bicycle and road_bike routes, the same function returns 31,991 m of paved_smooth (99.8%), 40 m of compacted, and 37 m of paved.

To color the route line by surface, use each step's from_index and to_index to slice the coordinates of the matching leg geometry.

Add elevation, climb, and descent

Add details=elevation to see how much a rider climbs. Combine it with surfaces as details=route_details,elevation.

https://api.geoapify.com/v1/routing?waypoints=40.013757,-105.280843|40.1653743,-105.1047757&mode=mountain_bike&details=elevation&format=json&apiKey=YOUR_API_KEY

Reduced response:

{
  "results": [
    {
      "mode": "mountain_bike",
      "distance": 29093,
      "time": 7105.532,
      "legs": [
        {
          "distance": 29093,
          "time": 7105.532,
          "elevation_range": [
            [0, 1631.7],
            [21.894444426312575, 1631.1891296300528],
            [32.429183531376424, 1631],
            [69.34159010669416, 1630.782029564177]
          ],
          "elevation": [1631.7, 1631.1891296300528, 1631, 1630.782029564177],
          "steps": [
            {
              "from_index": 0,
              "to_index": 15,
              "distance": 312,
              "time": 73.899,
              "instruction": { "text": "Bike east on Arapahoe Avenue." },
              "max_elevation": 1631.7,
              "min_elevation": 1626.617022898081,
              "elevation_gain": -5.055195985282353,
              "elevation": 1629
            }
          ]
        }
      ]
    }
  ]
}

The complete leg has 1,229 entries in elevation_range and elevation, one for each point of the leg geometry. The example shows the first four and the first of 62 steps.

Field Level Meaning
elevation_range leg [distance, height] pairs in meters. Distance is measured along the leg from its start.
elevation leg Heights in meters for the leg geometry points
max_elevation, min_elevation step Highest and lowest point of the step
elevation_gain step Height of the last point of the step minus height of the first point. Negative values mean the step goes downhill.
elevation step Average elevation of the step

The response has no total ascent or descent field. Calculate them from elevation_range:

function summarizeElevation(routingResponse) {
  let ascent = 0;
  let descent = 0;
  let min = Infinity;
  let max = -Infinity;

  for (const leg of routingResponse.results[0].legs) {
    const range = leg.elevation_range ?? [];

    range.forEach(([, height], i) => {
      min = Math.min(min, height);
      max = Math.max(max, height);
      if (i === 0) return;

      // Add every rise to the ascent and every drop to the descent.
      const change = height - range[i - 1][1];
      if (change > 0) ascent += change;
      else descent -= change;
    });
  }

  return {
    ascent: Math.round(ascent),
    descent: Math.round(descent),
    min: Math.round(min),
    max: Math.round(max)
  };
}
Mode Ascent Descent Lowest point Highest point
bicycle 145 m 256 m 1,514 m 1,632 m
mountain_bike 144 m 255 m 1,510 m 1,632 m
road_bike 145 m 256 m 1,514 m 1,632 m

The ride starts at 1,631.7 m and ends at about 1,521 m, so it loses about 110 m overall. The ascent shows the climbing along the way. Every small rise between neighboring geometry points counts, so smooth the heights first if you need a conservative climb value.

The step elevation_gain compares only the first and last points of a step. Do not add these values up for the total ascent, because a step can climb and descend in between. When you request route_details,elevation together, the steps are split by road properties, but max_elevation, min_elevation, and elevation_gain keep the values of the whole maneuver: in this route, the first three Arapahoe Avenue steps all report elevation_gain: -5.055195985282353. Read climbs from elevation_range in that case.

The last elevation_range distance is 29,061.8 m, slightly less than the leg distance of 29,093 m. To chart the profile of a multi-leg route, follow How to Create an Elevation Profile for a Route.

Credits and long bike routes

Credits

A Routing API request costs 1 credit per waypoint pair. According to Routing API pricing, route_details adds 1 credit per waypoint pair, and elevation adds 1 credit per waypoint pair. The long-distance surcharge adds 1 credit for each completed 500 km of route distance (floor(route_distance_meters / 500000)); a route shorter than 500 km has no distance surcharge. Distance does not multiply the credits for route_details or elevation.

For the two-waypoint routes in this guide, which are shorter than 500 km:

Request Credits
One mode, no details 1
One mode, details=route_details,elevation 1 + 1 + 1 = 3
Three modes, details=route_details,elevation 3 × 3 = 9

Request the details only for the mode the rider selects if you show several modes side by side.

Long bike routes

A request in bicycle mode returned HTTP 400 when the waypoints were more than 300 km apart. From 1001 Arapahoe Avenue, Boulder, CO 80302, to 135 North Animas Street, Trinidad, CO 81082:

https://api.geoapify.com/v1/routing?waypoints=40.013757,-105.280843|37.1679225,-104.5088184&mode=bicycle&format=json&apiKey=YOUR_API_KEY
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Too long distance between locations. Distance should not exceed 300000 meters for a regular API call, and 600000 meters for an asynchronous batch API call. Estimated distance is 323474 meter(s)."
}

The estimated distance in the message, 323,474 m, matched the straight-line distance between the two waypoints, not the length of a cycling route. The check applied to each waypoint pair: the same trip with an intermediate waypoint at 1 North Main Street, Pueblo, CO 81003 (38.2816143,-104.6098264) returned a 442,454 m route with two legs.

For multi-day rides, split the trip into stages, or add intermediate waypoints so that no pair is more than 300 km apart. Show the API message to the user instead of an empty map. How to Handle Unreachable Waypoints and Routing Errors covers other cases where the Routing API cannot return a route.

Bike route planner with an AI coding agent

Copy this prompt into Codex, Claude Code, Cursor, or another coding agent to add a bike route planner with a bike type selector, a surface breakdown, and an elevation summary to your project.

Add a bike route planner to my project with the Geoapify Routing API.

Before you start
- Inspect the existing project first. Reuse its current framework, components, styling, HTTP client, map library, and conventions. Make the smallest necessary changes.
- Ask only what the project does not answer: where the start and destination come from, whether the route is drawn on a map, and whether browser requests use a restricted API key or a backend proxy.
- Read the Geoapify API key from the project's environment configuration. Never hardcode, print, or log it.

API request
- GET https://api.geoapify.com/v1/routing with waypoints=lat,lon|lat,lon, mode, details=route_details,elevation, and format=json.
- Bike type selector: City bike → mode=bicycle, Mountain bike → mode=mountain_bike, Road bike → mode=road_bike. Do not offer mode=scooter as a bike; it is a motor scooter or moped.
- Request details only for the selected mode. route_details and elevation each add 1 credit per waypoint pair.

Result
- Show the route distance (meters, rounded) and time (seconds, formatted as hours and minutes).
- Surface breakdown: for results[0].legs[].steps[], skip steps with distance 0, add step.distance per step.surface, and show meters and percent of the summed step distance. Do the same for step.road_class.
- Elevation summary: from legs[].elevation_range ([distance, height] pairs in meters), calculate ascent as the sum of rises, descent as the sum of drops, and the lowest and highest points. Do not sum step elevation_gain values.
- If a map is used, draw the route geometry and optionally color it by surface with step.from_index and step.to_index.

Errors and limits
- Show the API message for HTTP 400 responses. Bicycle requests can fail with "Too long distance between locations" when waypoints are more than 300 km apart; suggest adding an intermediate waypoint.
- Show loading and empty states, cancel stale requests when the user changes the mode, and retry 429 responses a limited number of times with backoff.
- Add a test with a mocked response that includes a gravel step, the zero-distance arrival step, and an elevation_range.

Review the generated code against the requests in this guide. Check the waypoint order (latitude,longitude), the mode names, the skipped arrival step, the ascent calculation from elevation_range, and the API-key handling.