Bicycle Route Planning With an API: City, Road and Mountain Bike Routes Compared

Pick the bike mode that matches the rider, check surfaces and climbing, and stay inside distance limits, with real Portland routes

Map of Forest Park, Portland: a paved city and road bike route and a longer, mostly dirt mountain bike route
Map of Forest Park, Portland: a paved city and road bike route and a longer, mostly dirt mountain bike route

To plan a bicycle route with an API, send the start and destination with the bike mode that matches the rider's bike (bicycle, road_bike or mountain_bike), and add details=route_details,elevation to check surfaces and climbing before you show the route. The mode does more than change the travel time. In Portland's Forest Park, the same two points gave city and road bikes a 12.3 km paved route, while the mountain bike got a 17.3 km route that is mostly dirt. This post compares the three bike modes of the Geoapify Routing API on three real Portland rides and shows how to read surfaces, climbing, limits and credits from the response.

Key takeaways

  • Three bike modes: bicycle (city bike), road_bike and mountain_bike. There is no e-bike mode, and scooter means a motor scooter or moped.
  • Same roads, different times: on paved rides the three modes often follow the same streets; the responses averaged about 15, 17 and 23 km/h for mountain_bike, bicycle and road_bike.
  • Different roads off the pavement: mountain_bike can take unpaved roads and trails. In Forest Park, 11.4 km of its 17.3 km route was dirt.
  • Check before you show: sum step distances by surface, calculate ascent from elevation_range, and keep each waypoint pair inside the per-mode distance limit.

What Changes When the Route Is for a Bike?

A bike route is shaped by the bike and the rider, not only by the road network. A car route between two points is mostly about speed and legality; a bike route also depends on what the tires can handle and how much the rider wants to climb.

Four things change the answer:

  • Bike type. A road bike with narrow tires needs pavement. A mountain bike can take dirt and gravel trails that are closed to cars or useless for a city bike.
  • Surface. Smooth asphalt, gravel and dirt make a big difference in comfort, speed and safety. The Routing API returns a surface value for every route segment when you ask for route_details.
  • Climbing. A route that is 2 km shorter can be the harder ride if it climbs 300 m more. Elevation data shows this before the rider starts.
  • Speed. Travel time depends on the mode. The same 3.9 km of streets took between 10 and 15 minutes in our tests, depending only on the bike mode.

A good bike route planner shows all four, so the rider can decide whether the suggested route fits the bike they actually have.

Which Bike Mode Matches the Rider?

A bike mode is a routing profile for one type of bicycle. It changes which ways the router prefers and how fast it expects the rider to travel. The Routing API documents three bike modes in its travel mode reference:

ModeDescription in the API documentationUse it for
bicycleA city bicycleCommuting, everyday rides, bike-share trips
road_bikeA road-style bicycle with narrow tires and lightweight constructionRoad rides and training on paved roads
mountain_bikeA mountain bicycle to ride on unpaved surfaces but slower on paved terrainRiders who accept dirt and gravel trails

Two modes are often confused with bike modes. The Routing API has no e-bike mode, so an e-bike rider gets one of the three profiles above, with their speeds. The scooter mode is documented as a motor scooter or moped, not a kick scooter or a bicycle, so do not use it for bike routes.

If your app does not ask which bike the rider has, bicycle is the safe default. Offer road_bike and mountain_bike as explicit choices, because they can change the route, not just the time.

How Do You Request a Bike Route?

A bike route is a regular Routing API GET request with a bike mode. Waypoints go in lat,lon order, separated by | (encoded as %7C). This REST request asks for a mountain_bike route through Portland's Forest Park, from the Lower Macleay Park Trailhead to Skyline Tavern, with surfaces and elevation:

curl "https://api.geoapify.com/v1/routing?waypoints=45.5360307,-122.7126743%7C45.5796448,-122.7971658&mode=mountain_bike&details=route_details,elevation&format=json&apiKey=YOUR_API_KEY"

The parameters that matter for bikes:

ParameterValueEffect
waypointslat,lon|lat,lonStart, optional intermediate stops, destination
modebicycle, road_bike, mountain_bikeBike profile
detailsroute_details,elevationAdds surface, road class and road name per segment, plus a height for every geometry point
formatjsonPlain JSON results instead of the default GeoJSON
typebalanced (default), shortRoute optimization type; less_maneuvers is only for motor modes

With route_details, each step describes one stretch of road with the same properties. This step is the longest stretch of Leif Erikson Drive on the route, and the elevation_range pairs are [distance along the leg, height] in meters:

Simplified response:

{
  "results": [
    {
      "mode": "mountain_bike",
      "waypoints": [
        { "location": [-122.712674, 45.53603], "original_index": 0 },
        { "location": [-122.797165, 45.579644], "original_index": 1 }
      ],
      "distance": 17259,
      "distance_units": "meters",
      "time": 4914.737,
      "legs": [
        {
          "distance": 17259,
          "time": 4914.737,
          "steps": [
            {
              "name": "Northwest Leif Erikson Drive",
              "surface": "dirt",
              "road_class": "service_other",
              "speed": 9,
              "distance": 4204,
              "time": 1187.0439568395404,
              "from_index": 157,
              "to_index": 449
            }
          ],
          "elevation_range": [
            [0, 30],
            [2559.7153154282064, 125.36761561523932],
            [6763.542225973486, 170.3194369870202],
            [10689.147317420724, 205.93901788386185],
            [17241.045617073156, 318.1775670894317]
          ]
        }
      ]
    }
  ]
}

Note the coordinate order: the request uses lat,lon, but results[].waypoints[].location is [lon, lat], like GeoJSON. The full leg has 28 steps and 1,106 elevation_range entries, one per geometry point; from_index and to_index link a step to its part of the geometry, which you can use to color the line by surface. Bike paths and park roads like Leif Erikson Drive appear with the service_other road class.

How Different Are City, Road and Mountain Bike Routes?

We requested three Portland rides in all three bike modes, with details=route_details,elevation and the default type=balanced. All waypoints come from the Geocoding API, and all numbers below come from those live responses.

Same streets, different times

The first ride is a short commute from Powell's City of Books on West Burnside Street to Lloyd Center, across the Willamette River on the Broadway Bridge. All three modes returned the same geometry:

ModeDistanceTimeAverage speed
mountain_bike3,857 m15.1 min15.3 km/h
bicycle3,857 m13.6 min17.1 km/h
road_bike3,857 m10.0 min23.2 km/h

The route is almost entirely paved: 3,822 m of paved_smooth and 31 m of paved, with 53 m of ascent. The main streets are Northeast Weidler Street (1,187 m), Northwest Broadway (732 m), the Broadway Bridge (605 m) and West Burnside Street (369 m). For a commute like this, the mode only changes the ETA, by five minutes between the slowest and the fastest profile.

A long road ride

The second ride goes from Pioneer Courthouse Square downtown to Multnomah Falls Lodge in the Columbia River Gorge. Almost half of it follows the road the API names "East Historic Columbia River Highway, US 30 Historic":

ModeDistanceTimeAverage speedAscent
mountain_bike51,957 m203.7 min15.3 km/h656 m
bicycle51,957 m181.0 min17.2 km/h656 m
road_bike51,698 m130.8 min23.7 km/h657 m

bicycle and mountain_bike returned the same geometry. The road_bike route is 259 m shorter and uses slightly different streets: 24,599 m on the historic highway and 11,116 m on East Burnside Street, against 24,327 m and 10,721 m for bicycle. Both routes are paved except for 6 m of gravel near the lodge.

On a ride this long, the speed profile matters more than the streets. The road_bike estimate is about 50 minutes shorter than bicycle and more than an hour shorter than mountain_bike, for practically the same route.

When the mountain bike leaves the pavement

The third ride crosses Forest Park, from the Lower Macleay Park Trailhead in Northwest Portland up to Skyline Tavern on Northwest Skyline Boulevard. Here the modes split:

ModeDistanceTimeAverage speedAscentSurface
bicycle12,344 m49.2 min15.1 km/h423 m100% paved_smooth
road_bike12,344 m35.8 min20.7 km/h423 m100% paved_smooth
mountain_bike17,259 m81.9 min12.6 km/h463 m11,367 m dirt, 4,684 m paved_smooth, 1,189 m gravel

bicycle and road_bike take public streets around the park: Northwest Skyline Boulevard (5,608 m), Northwest Cornell Road (2,766 m) and Northwest Thompson Road (2,533 m). mountain_bike goes through it. Its main roads are Northwest Leif Erikson Drive (9,921 m) and Northwest Saltzman Road (2,816 m), and 12,582 m of the route has the service_other road class.

The mountain bike route is 4.9 km longer and about 33 minutes slower, but about two thirds of it is dirt, which is exactly what a mountain biker is looking for. Portland Parks & Recreation lists Leif Erikson Drive and Saltzman Road among the roads, firelanes and trails open to bikes in Forest Park.

Map of Forest Park in Portland with two bike routes from Lower Macleay Park to Skyline Tavern: a blue paved route along NW Thompson Road and NW Skyline Boulevard for bicycle and road_bike, and a mountain_bike route along NW Leif Erikson Drive colored brown for dirt, amber for gravel and green for paved sections
Same start and end, different routes: bicycle and road_bike share the blue paved route around the park (12.3 km; 49 and 36 min), while mountain_bike goes through the park on NW Leif Erikson Drive and Saltzman Road (17.3 km, 82 min), with 11.4 km on dirt and 1.2 km on gravel. Routes from the Geoapify Routing API. Map data © OpenStreetMap contributors, map © Geoapify.

The speed pattern

Across the paved rides, the averages are consistent: about 15 km/h for mountain_bike, 17 km/h for bicycle and 23 km/h for road_bike on the same geometry. On the hilly Forest Park ride, the averages drop to 15.1 and 20.7 km/h for bicycle and road_bike, and to 12.6 km/h for mountain_bike on dirt.

These are averages we calculated from distance and time in the responses, not a documented speed setting. The Routing API has no parameter for a custom bike speed, and max_speed applies only to motor modes. If your riders are much faster or slower, adjust the ETA in your app and keep the route geometry from the API.

Grouped bar chart of average speed by bike mode on three Portland rides: city commute 15.3, 17.1 and 23.2 km/h, Columbia Gorge ride 15.3, 17.2 and 23.7 km/h, Forest Park 12.6, 15.1 and 20.7 km/h for mountain_bike, bicycle and road_bike
Average speed (distance divided by time) from the Routing API responses. On the paved commute and Gorge rides, road_bike is about 6 km/h faster than bicycle and mountain_bike about 2 km/h slower. The hatched Forest Park bar is a different, mostly dirt route.

How Do You Check Surface and Climbing?

The response has no ready-made surface summary and no total ascent field, so calculate both on your side. For surfaces, add up step.distance for each surface value; for climbing, add up every rise between neighboring points in elevation_range:

function summarizeBikeRoute(routingResponse) {
  const surfaces = {};
  let ascent = 0;

  for (const leg of routingResponse.results[0].legs) {
    for (const step of leg.steps) {
      // The arrival step has distance 0 and no road properties.
      if (!step.distance) continue;
      const surface = step.surface ?? "unknown";
      surfaces[surface] = (surfaces[surface] ?? 0) + step.distance;
    }

    const range = leg.elevation_range ?? [];
    for (let i = 1; i < range.length; i++) {
      const rise = range[i][1] - range[i - 1][1];
      if (rise > 0) ascent += rise;
    }
  }

  return { surfaces, ascent: Math.round(ascent) };
}

For the Forest Park mountain_bike response, this returns 11,367 m of dirt, 4,684 m of paved_smooth, 1,189 m of gravel and 463 m of ascent. Step distances are rounded to whole meters, so their sum (17,240 m) is slightly less than the route distance; calculate shares from the sum and show distance as the ride length.

Do not add up step elevation_gain for the total climb. It compares only the first and last point of a step, and when you request route_details,elevation together, the split steps keep the elevation values of the whole maneuver, so the same gain can repeat several times. elevation_range has one height per geometry point and gives a reliable total. Every small rise counts, so smooth the heights first if you need a conservative climb value.

The surface values are paved_smooth, paved, paved_rough, compacted, dirt, gravel, path and impassable. They are derived from OpenStreetMap tags such as surface. The plan a bicycle route how-to has the complete code, including road class shares, descent, minimum and maximum height, and a Boulder, Colorado example.

Should You Use type=short for Bikes?

type=short asks for the shortest route instead of the default balanced one. For bikes, that can trade a few hundred meters for a much worse surface.

We repeated the Forest Park ride with type=short. All three modes returned the same 11,296 m route, about 1 km shorter than the balanced city and road bike route:

Modetype=balancedtype=short
bicycle12,344 m, 49.2 min11,296 m, 66.7 min
road_bike12,344 m, 35.8 min11,296 m, 56.1 min
mountain_bike17,259 m, 81.9 min11,296 m, 61.2 min

The shorter route is slower for every mode, and for a road bike it is the wrong route. With route_details, the road_bike short route has 9,806 m of paved_smooth and 1,406 m of gravel, mostly on Holman Lane (1,271 m of gravel). The balanced road_bike route was 100% paved and about 20 minutes faster.

Map of the southeast part of Forest Park comparing two road_bike routes: the type=short route in coral via NW 53rd Drive with its gravel section on Holman Lane highlighted in amber, and the default balanced route in gray along paved roads to the south
The road_bike route with type=short (11.3 km, 56 min) cuts through the park on Holman Lane and NW 53rd Drive and includes 1.4 km of gravel. The default balanced route (gray, 12.3 km, 36 min) stays on pavement. Both routes continue on NW Skyline Blvd to Skyline Tavern. Routes from the Geoapify Routing API. Map data © OpenStreetMap contributors, map © Geoapify.

Recommendation: keep the default type=balanced for bike routes and use the surface summary as your check. If you offer a "shortest" option, show its surface breakdown next to it.

What Limits and Costs Apply?

Bike modes have a maximum distance between waypoints. The check uses the straight-line distance between each pair of consecutive waypoints, not the length of the cycling route:

ModeRegular API callAsynchronous batch call
bicycle300 km600 km
mountain_bike500 km1,000 km
road_bike1,000 km1,000 km

The Route Matrix API applies the same limits to every source–target pair, and the Route Planner API to every pair of locations in the request. When a pair is too far apart, the Routing API returns HTTP 400. This bicycle request from Boulder to Trinidad, Colorado, is documented in the bicycle route how-to:

{
  "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)."
}

For long or multi-day rides, add intermediate waypoints so that no pair exceeds the limit, or split the trip into stages. Show the API message to the rider instead of an empty map; How to Handle Unreachable Waypoints and Routing Errors covers the other failure cases.

Credits follow the Routing API pricing: 1 credit per waypoint pair, plus 1 for route_details and 1 for elevation per pair, plus 1 for each completed 500 km of route distance.

RequestCredits
One bike mode, two waypoints, no details1
One bike mode, details=route_details,elevation3
Three bike modes side by side, both details9

To compare modes cheaply, request the three modes without details and fetch surfaces and elevation only for the mode the rider picks. See Geoapify pricing for plans.

Not every routing option works for bikes. avoid=tolls, avoid=highways, traffic and max_speed apply only to motor modes. avoid=ferries and avoid=location:lat,lon work for all modes, so you can steer a bike route around a closed trail section; avoids also add to the request cost.

What Should a Bike Route Planner Check Before Showing a Route?

Most bike routing problems are visible in the response before the rider sees them. Check these points:

  1. The mode matches the bike. A road bike rider should not get a mountain_bike route, and the reverse wastes the trails a mountain biker wants.
  2. The surface summary fits the mode. Flag gravel or dirt on a road_bike route, especially with type=short.
  3. The climb is visible. Show ascent from elevation_range next to distance and time; 423 m of climbing over 12 km is a different ride than a flat 12 km.
  4. Each waypoint pair is inside the limit. Check straight-line distances on the client before you call the API, or handle the HTTP 400 message.
  5. The ETA fits your riders. Mode speeds are model averages; e-bike riders and slow riders need your own adjustment.
  6. The data is current enough. Routes follow OpenStreetMap, and trail access, surfaces and closures change. Link to official trail information. For example, Portland Parks & Recreation says e-bikes and other motorized transport are not allowed in Forest Park, which matters if an e-bike rider requests the mountain_bike route above.

Beyond single routes, the same bike modes work in other APIs. The Isoline API shows how far a rider gets in 20 or 30 minutes, and the Route Planner API can plan bike courier routes with several riders. For background on the data behind the routes, read OpenStreetMap routing; for motor vehicles, see the truck route planning API post.

Frequently Asked Questions

What is the difference between bicycle, road_bike and mountain_bike?

bicycle is a city bike, road_bike is a road bike with narrow tires, and mountain_bike is for unpaved surfaces and slower on pavement. On paved Portland rides they often shared the same streets with average speeds of about 17, 23 and 15 km/h; in Forest Park, mountain_bike took a 17.3 km dirt route while the others stayed on 12.3 km of pavement. See the travel modes reference.

Can the API avoid gravel for a road bike?

There is no avoid=unpaved option. Use mode=road_bike with the default type=balanced, request details=route_details and check the surface summary. In our test, type=short put a road bike on 1.4 km of gravel while the balanced route was fully paved. The bicycle route how-to shows the surface code.

How do I get the total climb of a bike route?

Request details=elevation and add up every rise between neighboring heights in results[].legs[].elevation_range. The response has no total ascent field, and step elevation_gain values should not be summed. See the bicycle route how-to for the full function.

Is there an e-bike mode?

No. The Routing API has bicycle, road_bike and mountain_bike, and no custom bike speed parameter. The scooter mode is a motor scooter or moped, not a bicycle. For e-bike riders, pick the closest bike mode and adjust ETAs in your app. Check local rules too: some parks, such as Portland's Forest Park, do not allow e-bikes.

How long can a bike route be in one request?

The limit applies to the straight-line distance between each pair of consecutive waypoints: 300 km for bicycle, 500 km for mountain_bike and 1,000 km for road_bike in a regular call (600, 1,000 and 1,000 km in batch). Add intermediate waypoints to plan longer rides. See How to Handle Unreachable Waypoints and Routing Errors.

How many credits does a bike route with surface and elevation cost?

A two-waypoint bike route with details=route_details,elevation costs 3 credits: 1 for the route, 1 for route_details and 1 for elevation. Comparing three modes with both details costs 9. Routes add 1 credit per completed 500 km. See Routing API pricing.

World map

Plan Your First Bike Route

Compare bike modes in the playground, follow the how-to,
and start building with a free Routing API key.