curl -X POST "https://api.cyclemate.club/directions/v2/" \
-H "apikey: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"coordinates": [[-118.2401, 34.0498], [-118.2502, 34.0603]],
"profile": "cycling"
}'
const response = await fetch('https://api.cyclemate.club/directions/v2/', {
method: 'POST',
headers: {
'apikey': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
coordinates: [[-118.2401, 34.0498], [-118.2502, 34.0603]],
profile: 'cycling',
}),
});
const directions = await response.json();
if (directions.route) {
console.log(`Route distance: ${directions.route.distance}m`);
}
import requests
url = "https://api.cyclemate.club/directions/v2/"
headers = {
"apikey": "YOUR_API_KEY"
}
data = {
"coordinates": [[-118.2401, 34.0498], [-118.2502, 34.0603]],
"profile": "cycling",
"supersafe": True
}
response = requests.post(url, headers=headers, json=data)
directions = response.json()
{
"route": {
"distance": 2239,
"duration": 536.9,
"elevation_gain": 19,
"elevation_loss": 4,
"safety_code": 1.62,
"geometry": {
"type": "LineString",
"coordinates": [[-118.240115, 34.049835], "…"]
},
"steps": [
"…",
{
"distance": 250,
"duration": 60.0,
"delta_elevation": 2,
"geometry": {
"coordinates": [[-118.253284, 34.0566427], "…"],
"type": "LineString"
},
"name": "sidewalk",
"mode": "cycling",
"safety_code": 3,
"safety_classification": "shared",
"exits": "",
"maneuver": {
"bearing_before": 307,
"bearing_after": 38,
"type": "Turn",
"modifier": "right",
"instruction": "Turn right on sidewalk",
"location": [-118.253284, 34.0566427]
},
"cycle_routes": null,
"lit": true,
"voice_instructions": [
{
"distance_along_geometry": 33,
"announcement": "Turn right on sidewalk"
}
],
"banner_instructions": [
{
"distance_along_geometry": 33,
"primary": {
"text": "Turn right on sidewalk",
"components": [
{ "text": "Turn right on ", "type": "text" },
{ "text": "sidewalk", "type": "road-name" }
],
"type": "Turn",
"modifier": "right"
}
}
]
},
"…"
],
"stops": [
{ "stop_type": "traffic_signal", "location": [-118.2399, 34.0496876] },
{ "stop_type": "stop_sign", "location": [-118.2525962, 34.0588972] },
"…"
]
},
"waypoints": [
{ "name": "", "location": [-118.240115, 34.049835] },
{ "name": "", "location": [-118.250157, 34.060296] }
],
"search_id": "5aed4b34-7999-490d-a785-677505e92b06",
"graph_version": "b58ba70782b2",
"sublabel": null,
"metadata": {
"profile": "cycling",
"supersafe": true,
"nighttime": false,
"waypoint_count": 2
}
}
{
"error": "Rate limit exceeded",
"retry_after_seconds": 42
}
Routing
Get directions
Get turn-by-turn directions between waypoints for cycling and walking
curl -X POST "https://api.cyclemate.club/directions/v2/" \
-H "apikey: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"coordinates": [[-118.2401, 34.0498], [-118.2502, 34.0603]],
"profile": "cycling"
}'
const response = await fetch('https://api.cyclemate.club/directions/v2/', {
method: 'POST',
headers: {
'apikey': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
coordinates: [[-118.2401, 34.0498], [-118.2502, 34.0603]],
profile: 'cycling',
}),
});
const directions = await response.json();
if (directions.route) {
console.log(`Route distance: ${directions.route.distance}m`);
}
import requests
url = "https://api.cyclemate.club/directions/v2/"
headers = {
"apikey": "YOUR_API_KEY"
}
data = {
"coordinates": [[-118.2401, 34.0498], [-118.2502, 34.0603]],
"profile": "cycling",
"supersafe": True
}
response = requests.post(url, headers=headers, json=data)
directions = response.json()
{
"route": {
"distance": 2239,
"duration": 536.9,
"elevation_gain": 19,
"elevation_loss": 4,
"safety_code": 1.62,
"geometry": {
"type": "LineString",
"coordinates": [[-118.240115, 34.049835], "…"]
},
"steps": [
"…",
{
"distance": 250,
"duration": 60.0,
"delta_elevation": 2,
"geometry": {
"coordinates": [[-118.253284, 34.0566427], "…"],
"type": "LineString"
},
"name": "sidewalk",
"mode": "cycling",
"safety_code": 3,
"safety_classification": "shared",
"exits": "",
"maneuver": {
"bearing_before": 307,
"bearing_after": 38,
"type": "Turn",
"modifier": "right",
"instruction": "Turn right on sidewalk",
"location": [-118.253284, 34.0566427]
},
"cycle_routes": null,
"lit": true,
"voice_instructions": [
{
"distance_along_geometry": 33,
"announcement": "Turn right on sidewalk"
}
],
"banner_instructions": [
{
"distance_along_geometry": 33,
"primary": {
"text": "Turn right on sidewalk",
"components": [
{ "text": "Turn right on ", "type": "text" },
{ "text": "sidewalk", "type": "road-name" }
],
"type": "Turn",
"modifier": "right"
}
}
]
},
"…"
],
"stops": [
{ "stop_type": "traffic_signal", "location": [-118.2399, 34.0496876] },
{ "stop_type": "stop_sign", "location": [-118.2525962, 34.0588972] },
"…"
]
},
"waypoints": [
{ "name": "", "location": [-118.240115, 34.049835] },
{ "name": "", "location": [-118.250157, 34.060296] }
],
"search_id": "5aed4b34-7999-490d-a785-677505e92b06",
"graph_version": "b58ba70782b2",
"sublabel": null,
"metadata": {
"profile": "cycling",
"supersafe": true,
"nighttime": false,
"waypoint_count": 2
}
}
{
"error": "Rate limit exceeded",
"retry_after_seconds": 42
}
Overview
Get directions between waypoints for cycling and walking. Setsupersafe to route on Cyclemate’s safety-prioritized cycling network.
Coordinates are [longitude, latitude], maximum 25 waypoints per request. Duration estimates assume 15 km/h cycling and 5 km/h walking. Instructions are in English. SuperSafe routing is available in supported cities (New York City, Los Angeles, the SF Bay Area, London).
Versions are a per-endpoint path segment; this page documents v2 (POST /directions/v2/). See API versioning.
When no route exists between the waypoints, the response is a 200 with route: null. That is an outcome, not an error; a routing outage returns 503 instead.
Authentication
This endpoint requires an API key. Include it in theapikey header:
apikey: YOUR_API_KEY
Rate limits
Requests are rate-limited; authenticated requests are allowed a higher rate than anonymous ones. On429, honor the Retry-After header.
Request body
array
required
Array of
[longitude, latitude] waypoints. Minimum 2, maximum 25.Optional parameters
Optional parameters
string
default:"cycling"
cycling or walking.boolean
default:"true"
Prefer protected bike lanes, bike paths, and low-traffic streets. Cycling only.
number
Current direction of travel in degrees
[0, 360), used to constrain how the route leaves the start point. Pass the rider’s heading on a mid-ride reroute so the new route does not begin with a U-turn. If no road satisfies the constraint, route is null; retry without bearing. Cycling only.string
Planned departure as
"YYYY-MM-DDTHH:mm" with no timezone offset, interpreted in the route city’s timezone. Determines night routing (see metadata.nighttime) and which temporary road avoidances apply. Omit to route for now. Unparseable values return 400. Cycling only.boolean
Overrides the clock:
true routes on well-lit streets as if after dark, false routes on the daytime preference even at night. Honoured only in cities with street-lighting data, where metadata.nighttime reports the regime actually used; elsewhere it is ignored and metadata.nighttime stays false. Omit to let the departure time decide. Temporary road avoidances still follow the departure time. Cycling only.Response body
object | null
The route.
null when no route exists between the waypoints (a 200 outcome, not an error).Show Route object
Show Route object
number
Total distance in meters
number
Estimated travel time in seconds
number
Elevation gain in meters
number
Elevation loss in meters
number
deprecated
Deprecated: read each step’s
safety_classification instead. Mean of the per-step safety_code values. Still returned until a future version removes it.object
Route geometry as a GeoJSON
LineStringarray
Turn-by-turn instructions
Show Step object
Show Step object
number
Length in metres of the road after this step’s maneuver. To render “in X metres, …”, use the previous step’s
distance.number
Step duration in seconds
number
Net elevation change in meters (positive = uphill)
string
Street name after the maneuver
string
cycling or walking. Steps of a walking route report walking.number
deprecated
Deprecated: read
safety_classification instead. Numeric safety class of the step’s road: 0 and 1 are quiet, 2 is moderate, 3 is busy. Still returned until a future version removes it.string
How the step’s road reads to a rider:
quiet, cycleway (a quiet road on a signed cycle route), moderate, busy, or shared (a path shared with pedestrians). This is the class our route lines are coloured by (see the routing overview). Cycling steps only: absent on walking steps and on the final arrive step.object
type:"Start","Turn","Continue","roundabout","U-turn", or the end sentinel on the last step.modifier:"left","right","straight","sharp left","uturn", etc. On a roundabout, the exit direction. Absent when the maneuver has no direction.instruction: pre-rendered string, e.g."Turn left on East 1st Street". The final step is always"Arrive at destination".location:[lon, lat]of the maneuver, equal togeometry.coordinates[0].bearing_before/bearing_after: inbound and outbound headings in degrees.
string
Roundabout exit number as a 1-based string (e.g.
"3"). Empty string on non-roundabout steps.object
Step geometry as a GeoJSON
LineString; coordinates[0] is the maneuver location.string[] | null
Named cycle routes the step’s road belongs to, or
null.boolean
Whether the road is lit, from OpenStreetMap data. Cycling steps only; coverage is approximate (London today).
array
Spoken cues for the maneuver, ordered far to near.
distance_along_geometry is the metres remaining to the maneuver at which to speak the cue; announcement is the spoken text, unit-agnostic, with an optional ssml_announcement variant. Prepend a localized “In X, …” for non-imminent cues. The arrive step has none.array
Visual banner for the step.
distance_along_geometry is the metres before the maneuver at which the banner becomes current. primary is the maneuver to display ({ text, components, type, modifier }, with components split into "text" and "road-name" fragments; on a compound “X, then Y” step it is the first turn). An optional secondary carries a supporting line. An optional sub (same shape as primary) carries the stacked “then” preview of the maneuver that follows quickly: on compound “X, then Y” steps, and on any step shorter than about 30 m, where it previews the next step’s maneuver without changing either step. Absent when the following maneuver is further away, and never previews arrival.array
Traffic-control points along the route, each
{ stop_type, location } with stop_type one of "traffic_signal", "stop_sign", or "give_way" and location as [lon, lat].array
Snapped waypoint locations (
{ name, location })string
Unique identifier (UUID) for this request. Include it in support enquiries.
string | null
Routing network version identifier.
null on walking responses.string | null
"Prefers well-lit roads" on night routes, otherwise null.array
Temporary road disruptions near the route that are active at the departure time or start within 60 minutes. Same shape as the
disruptions field on Multi-Modal Routing. Absent when none apply, and never present on walking responses.object
profile: routing profile used.supersafe: whether SuperSafe routing was applied.nighttime: whether night routing (preferring lit streets) was applied. Cycling only, in cities with lighting data (London today).waypoint_count: number of waypoints.
curl -X POST "https://api.cyclemate.club/directions/v2/" \
-H "apikey: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"coordinates": [[-118.2401, 34.0498], [-118.2502, 34.0603]],
"profile": "cycling"
}'
const response = await fetch('https://api.cyclemate.club/directions/v2/', {
method: 'POST',
headers: {
'apikey': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
coordinates: [[-118.2401, 34.0498], [-118.2502, 34.0603]],
profile: 'cycling',
}),
});
const directions = await response.json();
if (directions.route) {
console.log(`Route distance: ${directions.route.distance}m`);
}
import requests
url = "https://api.cyclemate.club/directions/v2/"
headers = {
"apikey": "YOUR_API_KEY"
}
data = {
"coordinates": [[-118.2401, 34.0498], [-118.2502, 34.0603]],
"profile": "cycling",
"supersafe": True
}
response = requests.post(url, headers=headers, json=data)
directions = response.json()
….
{
"route": {
"distance": 2239,
"duration": 536.9,
"elevation_gain": 19,
"elevation_loss": 4,
"safety_code": 1.62,
"geometry": {
"type": "LineString",
"coordinates": [[-118.240115, 34.049835], "…"]
},
"steps": [
"…",
{
"distance": 250,
"duration": 60.0,
"delta_elevation": 2,
"geometry": {
"coordinates": [[-118.253284, 34.0566427], "…"],
"type": "LineString"
},
"name": "sidewalk",
"mode": "cycling",
"safety_code": 3,
"safety_classification": "shared",
"exits": "",
"maneuver": {
"bearing_before": 307,
"bearing_after": 38,
"type": "Turn",
"modifier": "right",
"instruction": "Turn right on sidewalk",
"location": [-118.253284, 34.0566427]
},
"cycle_routes": null,
"lit": true,
"voice_instructions": [
{
"distance_along_geometry": 33,
"announcement": "Turn right on sidewalk"
}
],
"banner_instructions": [
{
"distance_along_geometry": 33,
"primary": {
"text": "Turn right on sidewalk",
"components": [
{ "text": "Turn right on ", "type": "text" },
{ "text": "sidewalk", "type": "road-name" }
],
"type": "Turn",
"modifier": "right"
}
}
]
},
"…"
],
"stops": [
{ "stop_type": "traffic_signal", "location": [-118.2399, 34.0496876] },
{ "stop_type": "stop_sign", "location": [-118.2525962, 34.0588972] },
"…"
]
},
"waypoints": [
{ "name": "", "location": [-118.240115, 34.049835] },
{ "name": "", "location": [-118.250157, 34.060296] }
],
"search_id": "5aed4b34-7999-490d-a785-677505e92b06",
"graph_version": "b58ba70782b2",
"sublabel": null,
"metadata": {
"profile": "cycling",
"supersafe": true,
"nighttime": false,
"waypoint_count": 2
}
}
{
"error": "Rate limit exceeded",
"retry_after_seconds": 42
}
Variants
Reroute with current heading
Passbearing when recomputing a route mid-ride so the new route leaves in the rider’s current direction of travel:
JavaScript
const response = await fetch('https://api.cyclemate.club/directions/v2/', {
method: 'POST',
headers: {
'apikey': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
coordinates: [currentLocation, destination],
profile: 'cycling',
supersafe: true,
bearing: currentHeadingDegrees,
}),
});
Route for a night departure
Passdeparture_time to route for a particular time of day. After dark, in cities with lighting data, cycling routes prefer lit streets: metadata.nighttime is true and sublabel is "Prefers well-lit roads".
{
"coordinates": [[-0.1263, 51.5206], [-0.1145, 51.5045]],
"profile": "cycling",
"departure_time": "2026-08-05T23:30"
}
Walking
Setprofile to walking for a pedestrian route. Every step’s mode is walking, graph_version is null, and night routing and disruptions do not apply.
Error responses
400 Bad request
{
"error": "At least 2 coordinates required"
}
{
"error": "Profile must be cycling or walking"
}
code and the request’s search_id:
{
"error": "Cross-city routing is not supported",
"code": "CROSS_CITY_ROUTE",
"search_id": "9b2f2f9e-1d0a-4f4e-9d2e-7c8a53f6f210"
}
401 Unauthorized
{
"error": "Invalid credentials"
}
404 Unknown version
Returned for a version sub-path that does not exist (e.g.POST /directions/v3/). See API versioning.
{
"error": "Unknown endpoint or API version"
}
405 Method not allowed
Returned for any HTTP method other thanPOST.
{
"error": "Method not allowed"
}
429 Too many requests
Includes aRetry-After header with the seconds until the current window closes:
{
"error": "Rate limit exceeded",
"retry_after_seconds": 42
}
500 Internal server error
{
"error": "Error message"
}
503 Service unavailable
Routing is temporarily unavailable. This is distinct from a genuine no-route result, which returns200 with route: null. Retry after a short delay.
{
"error": "Routing is temporarily unavailable. Please try again shortly.",
"code": "ROUTING_UNAVAILABLE"
}