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,
"segments": [
{
"start_index": 0,
"end_index": 12,
"safety_classification": "shared",
"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,
"segments": [
{
"start_index": 0,
"end_index": 12,
"safety_classification": "shared",
"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 per request. Every intermediate coordinate is a stop: the route arrives there, and the response carries one leg per consecutive pair in legs (see Route through stops). Duration estimates assume 15 km/h cycling and 5 km/h walking. Instructions are in the requested language. 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] points in travel order: origin, any intermediate stops, destination. Minimum 2, maximum 25. Every intermediate point is a stop, routed to exactly and announced with its own arrive step; a request through stops costs one rate-limit token per consecutive pair.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.string
default:"en"
Language of every instruction, banner and spoken cue:
en (English) or es (Spanish). A regional tag such as es-MX is accepted and read as its base language. Any other language returns 400. Maneuver type and modifier values are the same in every language.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). One representative value for the whole step: our route lines are coloured per stretch, from segments (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 in the requestedlanguage, e.g."Turn left on East 1st Street"("Gira a la izquierda hacia East 1st Street"in Spanish). The final step is always"Arrive at destination"("Llega a tu destino"). Display text only: branch ontypeandmodifier, and use the banner’scomponentsto separate the road name.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 of the step’s road, or
null. One representative value for the whole step; see segments for each stretch.boolean
Whether the road is lit, from OpenStreetMap data. One representative value for the whole step; see
segments for each stretch. Cycling steps only; coverage is approximate (London today).array
The stretches of the step’s road, in order, covering its whole geometry. A step spans one maneuver, and the road can change character along it without a turn, so colour and measure the route from these rather than from the step-level
safety_classification, cycle_routes and lit, which carry one representative value for the whole step.Each stretch is { start_index, end_index, safety_classification, cycle_routes, lit }. geometry.coordinates.slice(start_index, end_index + 1) is the stretch; consecutive stretches meet at a repeated coordinate, so each one draws on its own with no gap. The other fields use the step’s vocabulary. safety_classification is absent on walking steps. The arrive step has an empty list.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" (an all-way stop), "two_way_stop" (only the minor road stops), or "give_way", and location as [lon, lat]. A two-way stop or give way that applies to one direction of travel is listed only when the route approaches it from that direction.array
One leg per consecutive coordinate pair, in request order. Present only when the request carried more than two coordinates and
route is not null. Each leg has the same fields as route (distance, duration, elevation_gain, elevation_loss, safety_code, geometry, steps, stops) plus mode (cycling or walking), is aggregated on its own, so it starts with a depart step and ends with its own "Arrive at destination" step, and equals the route a two-coordinate request for that pair returns, so you can re-request one pair and splice. route is these legs stitched end to end: geometry concatenated, totals summed, steps concatenated with each stop’s arrive step mid-list at distance 0. A two-coordinate request carries no legs key: the route is the leg.array
One snapped location per requested coordinate, in request order (
{ 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 coordinates in the request.
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,
"segments": [
{
"start_index": 0,
"end_index": 12,
"safety_classification": "shared",
"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"
}
Route through stops
Send every point incoordinates, in travel order. Each intermediate point is a stop: the response’s legs carries one leg per consecutive pair, and route is their stitched whole. A leg equals the route a two-coordinate request for that pair returns, so an editor that moves one stop only needs to re-request the two legs around it.
{
"coordinates": [[-0.1057, 51.5323], [-0.0877, 51.5257], [-0.0810, 51.5278]],
"profile": "cycling",
"departure_time": "2026-10-15T12:00"
}
200 (abridged)
{
"route": { "distance": 3624, "steps": ["…"], "…": "…" },
"legs": [
{ "mode": "cycling", "distance": 2098, "steps": ["…"], "…": "…" },
{ "mode": "cycling", "distance": 1526, "steps": ["…"], "…": "…" }
],
"waypoints": [
{ "name": "", "location": [-0.105695, 51.532295] },
{ "name": "", "location": [-0.087808, 51.525447] },
{ "name": "", "location": [-0.081006, 51.527789] }
],
"metadata": { "profile": "cycling", "supersafe": true, "nighttime": false, "waypoint_count": 3 }
}
bearing constrains how the route leaves the first coordinate only. A coordinate the router cannot reach makes the whole response route: null, exactly as for a two-coordinate request. All coordinates must lie in one city; otherwise the response is a 400 with code CROSS_CITY_ROUTE.
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"
}
{
"error": "language must be one of: en, es"
}
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"
}