curl -X POST "https://api.cyclemate.club/multi-modal/v2/" \
-H "apikey: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"origin": [-118.2401, 34.0498],
"destination": [-118.2601, 34.0699]
}'
const response = await fetch('https://api.cyclemate.club/multi-modal/v2/', {
method: 'POST',
headers: {
'apikey': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
origin: [-118.2401, 34.0498],
destination: [-118.2601, 34.0699],
}),
});
const { search_id, options } = await response.json();
console.log(`Found ${options.length} route options (search_id=${search_id})`);
import requests
url = "https://api.cyclemate.club/multi-modal/v2/"
headers = {
"apikey": "YOUR_API_KEY"
}
data = {
"origin": [-118.2401, 34.0498],
"destination": [-118.2601, 34.0699]
}
response = requests.post(url, headers=headers, json=data)
result = response.json()
{
"search_id": "f17bab36-b0cd-4155-9472-77c311878669",
"options": [
{
"name": "My bike",
"bike_type": "Personal",
"supersafe": true,
"nighttime": false,
"label": "SuperSafe™",
"sublabel": null,
"legs": [
{
"mode": "cycling",
"start_label": "Start route",
"end_label": "End",
"distance": 4241,
"duration": 1017,
"elevation_gain": 41,
"elevation_loss": 9,
"safety_code": 1.9,
"geometry": {
"type": "LineString",
"coordinates": [[-118.240115, 34.049835], "…"]
},
"steps": [
"…",
{
"distance": 388,
"duration": 93.0,
"delta_elevation": 3,
"geometry": {
"type": "LineString",
"coordinates": [[-118.240115115, 34.049834828], "…"]
},
"name": "East 1st Street",
"mode": "cycling",
"safety_code": 2,
"safety_classification": "moderate",
"exits": "",
"maneuver": {
"bearing_before": 310,
"bearing_after": 310,
"type": "Start",
"instruction": "Start on East 1st Street",
"location": [-118.240115115, 34.049834828]
},
"cycle_routes": null,
"lit": true,
"voice_instructions": [
"…"
],
"banner_instructions": [
"…"
]
},
"…"
],
"stops": ["…"]
}
],
"disruptions": [
{
"id": "manual:480",
"source": "manual",
"status": "active",
"name": "West 2nd Street",
"notes": "Road closed for street works",
"severity": "avoid",
"location": [-118.246099, 34.052013],
"start_date": "2027-03-10",
"start_minute": 0,
"end_date": "2027-03-20",
"end_minute": 1440
},
"…"
]
},
{
"name": "My bike",
"bike_type": "Personal",
"supersafe": false,
"nighttime": false,
"label": "Direct",
"sublabel": null,
"legs": ["…"]
},
{
"name": "Metro Bike Share",
"bike_type": "Docked",
"supersafe": true,
"nighttime": false,
"label": null,
"sublabel": null,
"legs": [
{
"mode": "walking",
"end_label": "7 bikes",
"distance": 139,
"duration": 100,
"elevation_gain": 0,
"elevation_loss": 0,
"safety_code": 0,
"geometry": { "type": "LineString", "coordinates": ["…"] },
"steps": ["…"],
"stops": []
},
{
"mode": "cycling",
"end_label": "11 docks",
"distance": 4130,
"duration": 991,
"elevation_gain": 40,
"elevation_loss": 9,
"safety_code": 1.8,
"geometry": { "type": "LineString", "coordinates": ["…"] },
"steps": ["…"],
"stops": ["…"]
},
{
"mode": "walking",
"end_label": "End",
"distance": 173,
"duration": 125,
"elevation_gain": 0,
"elevation_loss": 0,
"safety_code": 0,
"geometry": { "type": "LineString", "coordinates": ["…"] },
"steps": ["…"],
"stops": []
}
],
"rental": {
"pricing": {
"currency": "USD",
"unlock_fee": 1.75,
"per_min_rate": null,
"per_min_interval": null,
"per_km_rate": null,
"per_km_interval": null,
"description": "$1.75 every 30 minutes."
},
"app_links": {
"android": null,
"ios": null,
"logo": "https://auth.cyclemate.club/storage/v1/object/public/logos/metro_bike.png"
}
}
}
],
"destination_parking": null
}
{
"error": "Rate limit exceeded",
"retry_after_seconds": 42
}
Routing
Get multiple directions
Get multi-step routes combining walking, bike-share, and personal bikes
curl -X POST "https://api.cyclemate.club/multi-modal/v2/" \
-H "apikey: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"origin": [-118.2401, 34.0498],
"destination": [-118.2601, 34.0699]
}'
const response = await fetch('https://api.cyclemate.club/multi-modal/v2/', {
method: 'POST',
headers: {
'apikey': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
origin: [-118.2401, 34.0498],
destination: [-118.2601, 34.0699],
}),
});
const { search_id, options } = await response.json();
console.log(`Found ${options.length} route options (search_id=${search_id})`);
import requests
url = "https://api.cyclemate.club/multi-modal/v2/"
headers = {
"apikey": "YOUR_API_KEY"
}
data = {
"origin": [-118.2401, 34.0498],
"destination": [-118.2601, 34.0699]
}
response = requests.post(url, headers=headers, json=data)
result = response.json()
{
"search_id": "f17bab36-b0cd-4155-9472-77c311878669",
"options": [
{
"name": "My bike",
"bike_type": "Personal",
"supersafe": true,
"nighttime": false,
"label": "SuperSafe™",
"sublabel": null,
"legs": [
{
"mode": "cycling",
"start_label": "Start route",
"end_label": "End",
"distance": 4241,
"duration": 1017,
"elevation_gain": 41,
"elevation_loss": 9,
"safety_code": 1.9,
"geometry": {
"type": "LineString",
"coordinates": [[-118.240115, 34.049835], "…"]
},
"steps": [
"…",
{
"distance": 388,
"duration": 93.0,
"delta_elevation": 3,
"geometry": {
"type": "LineString",
"coordinates": [[-118.240115115, 34.049834828], "…"]
},
"name": "East 1st Street",
"mode": "cycling",
"safety_code": 2,
"safety_classification": "moderate",
"exits": "",
"maneuver": {
"bearing_before": 310,
"bearing_after": 310,
"type": "Start",
"instruction": "Start on East 1st Street",
"location": [-118.240115115, 34.049834828]
},
"cycle_routes": null,
"lit": true,
"voice_instructions": [
"…"
],
"banner_instructions": [
"…"
]
},
"…"
],
"stops": ["…"]
}
],
"disruptions": [
{
"id": "manual:480",
"source": "manual",
"status": "active",
"name": "West 2nd Street",
"notes": "Road closed for street works",
"severity": "avoid",
"location": [-118.246099, 34.052013],
"start_date": "2027-03-10",
"start_minute": 0,
"end_date": "2027-03-20",
"end_minute": 1440
},
"…"
]
},
{
"name": "My bike",
"bike_type": "Personal",
"supersafe": false,
"nighttime": false,
"label": "Direct",
"sublabel": null,
"legs": ["…"]
},
{
"name": "Metro Bike Share",
"bike_type": "Docked",
"supersafe": true,
"nighttime": false,
"label": null,
"sublabel": null,
"legs": [
{
"mode": "walking",
"end_label": "7 bikes",
"distance": 139,
"duration": 100,
"elevation_gain": 0,
"elevation_loss": 0,
"safety_code": 0,
"geometry": { "type": "LineString", "coordinates": ["…"] },
"steps": ["…"],
"stops": []
},
{
"mode": "cycling",
"end_label": "11 docks",
"distance": 4130,
"duration": 991,
"elevation_gain": 40,
"elevation_loss": 9,
"safety_code": 1.8,
"geometry": { "type": "LineString", "coordinates": ["…"] },
"steps": ["…"],
"stops": ["…"]
},
{
"mode": "walking",
"end_label": "End",
"distance": 173,
"duration": 125,
"elevation_gain": 0,
"elevation_loss": 0,
"safety_code": 0,
"geometry": { "type": "LineString", "coordinates": ["…"] },
"steps": ["…"],
"stops": []
}
],
"rental": {
"pricing": {
"currency": "USD",
"unlock_fee": 1.75,
"per_min_rate": null,
"per_min_interval": null,
"per_km_rate": null,
"per_km_interval": null,
"description": "$1.75 every 30 minutes."
},
"app_links": {
"android": null,
"ios": null,
"logo": "https://auth.cyclemate.club/storage/v1/object/public/logos/metro_bike.png"
}
}
}
],
"destination_parking": null
}
{
"error": "Rate limit exceeded",
"retry_after_seconds": 42
}
Overview
Get routing options that integrate live bike-share availability. Returns multiple route alternatives:- Personal bike: direct cycling route, returned in both the SuperSafe and the direct variant
- Docked bike-share: walk to station, cycle, then walk to the destination. Pickup and drop-off are always in the same system.
- Docked e-bike: the same shape, from the nearest station with an e-bike available. Marked
is_electric: true. - Dockless bike-share: walk to the bike, then cycle to the destination. One option per provider. Marked
is_electric: truewhen the nearest vehicle is electric, which is every Lime and Forest bike.
- Every response includes both the
supersafe: trueandsupersafe: falsepersonal variants, collapsed to one when they are identical, plus the other option types once atsupersafe: true. Filter locally on each option’ssupersafeflag; there is no request parameter for it. - Alternatives more than 3× slower than the personal bike route are filtered out
- Bike availability is fetched in real time from bike-share system feeds and provider APIs
- Assumed speeds: walking 5 km/h, cycling 15 km/h, e-bike 22 km/h
- Maximum bike/station search radius: 1,500 m
- Results are ordered Personal (SuperSafe first), then docked, docked e-bike, dockless
POST /multi-modal/v2/). See API versioning.
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. Multi-modal has a stricter limit than directions. On429, honor the Retry-After header.
Performance
- Typical response time: 800–1500 ms
- Routing segments are computed in parallel
Request body
array
required
Starting point as
[longitude, latitude]array
required
Ending point as
[longitude, latitude]Optional parameters
Optional parameters
string
Identifier of a saved custom route, a UUID string (for example
"a3b8c9d0-1e2f-4a5b-8c7d-6e5f4a3b2c1d"). When it resolves to a route, that route is appended as one or more non-reroutable cycling legs — one per stop on the route (see Custom routes) — and its details are returned in each option’s event field.string
Destination ID for enriched destination information in the response, a UUID string (for example
"3f9c6a1e-8b2d-4c5f-9e7a-1d2b3c4d5e6f").string
Display name for the destination (used if
destination_id not provided)string
Display name for the origin waypoint
string
Planned departure as
"YYYY-MM-DDTHH:mm" with no timezone offset, interpreted in the route city’s timezone. Determines night routing (see nighttime) and which temporary road avoidances apply; nearby active or imminent closures are surfaced per option as disruptions. Omit to route for now. Unparseable values return 400.string
Navigate-to-parking mode:
"ebike" (designated rental e-bike bays) or "regular" (cycle racks/stands). The Personal option becomes cycle-to-spot + walk-to-destination, ending at the nearest qualifying spot within 1,500 m of the destination; the spot is returned in parking. If none is found, or the destination borough does not mandate bays for "ebike", the option stays direct with parking: null. Use destination_parking to tell “park anywhere” from “no parking found”. Ignored when a custom route is attached. Other option types are unaffected.string
Rental e-bike operator (London):
"lime" or "forest". Restricts the "ebike" bay search to boroughs the operator serves; when the destination borough isn’t served, the route ends at the nearest bay inside the operator’s area and walks the rest (destination_parking.covered: false).Response body
string
Unique identifier (UUID) for this request. Include it in support enquiries.
array
Array of route option objects. An empty array with a
200 status means no viable option exists between the requested points; a temporary routing outage returns 503 instead.Show Route option
Show Route option
string
Route name (e.g., “My bike”, “Citi Bike”, “Lime”)
string
How the rider gets a bike:
Personal (their own), Docked (station-based bike-share), or Dockless (free-floating bike-share)boolean
true when the option’s cycling legs were timed at e-bike speed: the docked e-bike option, and a dockless option whose vehicle is electric. Absent otherwise.boolean
Which SuperSafe regime the route was computed under:
true = safety-biased routing, false = shorter/faster. Filter on this locally when the user toggles between the two.boolean
Whether this variant was routed on the night network preferring lit streets. Only set in cities with lighting data (London today); otherwise always
false. At night, or on rides expected to finish after dark, the Personal option fans out night-first: "SuperSafe™ Night", "Direct · Night", and the standard "SuperSafe™" variant.string | null
Display label for the route card. Set on Personal variants only:
"SuperSafe™", "SuperSafe™ Night", "Direct", or "Direct · Night". null for bikeshare options, where the brand name from name should be displayed instead.string | null
"Prefers well-lit roads" on night variants, otherwise null.array
The option’s legs in travel order, e.g. walk to the station, cycle, walk to the destination.
Show Leg object
Show Leg object
string
walking or cyclingboolean
false on custom-route legs, which should not be recomputed mid-ride. Present only when false.string
Display pill at the leg’s first coordinate (e.g.
"Start route"), when a transition deserves a callout. Absent otherwise.string
Display pill at the leg’s last coordinate. Docked pickups/dropoffs show live counts (
"5 bikes" / "3 e-bikes" / "5 docks") when the system publishes them, and generic pick-up and drop-off copy when it does not; dockless and personal transitions use "Pick up bike" / "Drop off bike"; a parking destination uses "Park bike"; the final leg ends with "End" unless a more specific label applies. Absent when the next leg’s start_label would sit at the same coordinate.number
Leg distance in meters
number
Leg duration 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
Leg 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, matching the leg’s mode.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 each leg’s 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 of a leg is the arrive step:"Arrive at destination", or"Arrive at {stop name}"on a custom route’s legs, where each leg ends at a named stop.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 leg, each
{ stop_type, location } with stop_type one of "traffic_signal", "stop_sign", or "give_way" and location as [lon, lat].object
Custom route information (when
event_id was provided)object
The destination entity the option routes to:
id (a UUID string), name, description, latitude, longitude, and images when the request named a saved destination (destination_id), or just name plus coordinates when only name was given. Absent for plain coordinate searches.object
Rental-system context, on
Docked and Dockless options only (absent on Personal options). Carries pricing and app_links; each is null when the system publishes no data.Show pricing
Show pricing
{
"currency": "USD",
"unlock_fee": 1.75,
"per_min_rate": null,
"per_min_interval": null,
"per_km_rate": null,
"per_km_interval": null,
"description": "$1.75 every 30 minutes."
}
per_min_rate / per_km_rate apply per per_min_interval minutes / per_km_interval kilometres; each pair is null when the system doesn’t charge on that dimension.Show app_links
Show app_links
{ android, ios, logo }: the provider’s rental deep links and a hosted logo image URL, each null when the provider publishes none.object | null
The parking spot the option’s cycling leg ends at. Present in two cases: Personal options when
parking_type is set, and Dockless options whose operator requires hires to end at a designated bay in the destination borough (the cycling leg ends at the bay, followed by a walking leg). The spot:{
"id": 70424,
"feature_id": "EBP.HCK.lime_dockless_location.98",
"lat": 51.526292,
"lon": -0.076975,
"borough": "Hackney",
"address": "Calvert Avenue",
"capacity": 0,
"ebike_parking": true,
"distance_meters": 140.86
}
distance_meters is the straight-line distance to the requested destination; capacity is 0 when the source publishes none. null when a spot was required but none was found within range: the option keeps the direct shape and clients can render a “no parking found” notice. Absent when no spot was required.array
Temporary road disruptions active at the departure time or starting within 60 minutes, within ~75 m of this option’s cycling legs. Absent when none apply. One entry per works site.
{
"id": "street_manager:AB123456789-01",
"source": "street_manager",
"status": "active",
"name": "Albert Bridge Road",
"notes": "Road closed for street works, Thames Water (utility_repair)",
"severity": "closed",
"location": [-0.1663, 51.4776],
"start_date": "2026-05-22",
"start_minute": 1195,
"end_date": "2026-07-29",
"end_minute": 1260
}
id: stable pin key, identical across options.source:"manual","tfl", or"street_manager".status:"active"(routing detoured around it) or"upcoming"(starts within 60 minutes; does not influence routing).severity:"closed"or"avoid".location:[lon, lat]centroid of the affected roads.start_date,start_minute,end_date,end_minute: the city-local window (YYYY-MM-DDdates, wall-clock minutes of the day), start-inclusive and end-exclusive.
array
What the SuperSafe route steered around, computed from the road network rather than generated text. Present on every option with Every entry carries
bike_type "Personal" and supersafe true, as an empty array when nothing was avoided; absent on every other option. Busy streets come first (longest first), then turn restrictions in route order, then nearby closures and overrides.{
"type": "turn_restriction",
"name": "Kingsland Road",
"location": [-0.07601, 51.54447],
"metadata": { "onto": "Forest Road" }
}
type, name (the street, or null when the road is unnamed), location ([lon, lat], a point on the street or the centre of a works site) and a metadata object whose keys depend on type:"dangerous_road": a busy or very busy street the Direct option rides for more than 250 m in total and this option does not.metadata.metersis that length. Only returned when the response also contains the Direct option."turn_restriction": a turn the route went around.nameis the street the rider was on andmetadata.ontothe street the turn would have led onto."construction": roadworks from a live feed, active at the departure time, within about 75 m of the route. When several works sit on one street, only one entry is returned for that street.metadatacarriesid(the same value as the matchingdisruptionsentry, so a client can show one pin),source,severity("closed"or"avoid"),notes, and the window:days_of_week(ISO weekdays,1= Monday, for a recurring window, elsenull),start_dateandend_date(YYYY-MM-DD,nullfor a recurring window),start_minuteandend_minute(wall-clock minutes, start-inclusive and end-exclusive)."time_override": a curated time window such as a street market or a school street, active at the departure time. Samemetadatakeys as"construction"."override": a permanent rider-reported avoidance near the route. When several reports sit on one street, only one entry is returned for that street.metadata.severityis"closed"or"avoid".
object | null
The destination borough’s rental e-bike parking rule. Set on every request whose destination falls inside a bounded area (a London borough), whatever
parking_type is, so a rider comparing a rental e-bike option can see the rule they will park under; null elsewhere.{ "area_name": "Hackney", "ebike_parking_rule": "bay_mandatory" }
ebike_parking_rule:"bay_mandatory"(hires must end in a designated bay) or"free_floating"(park anywhere; render as “park anywhere inarea_name”, not an error).- With an
operatorin the request, also carriesoperatorandcovered, indicating whether that operator serves the borough.
curl -X POST "https://api.cyclemate.club/multi-modal/v2/" \
-H "apikey: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"origin": [-118.2401, 34.0498],
"destination": [-118.2601, 34.0699]
}'
const response = await fetch('https://api.cyclemate.club/multi-modal/v2/', {
method: 'POST',
headers: {
'apikey': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
origin: [-118.2401, 34.0498],
destination: [-118.2601, 34.0699],
}),
});
const { search_id, options } = await response.json();
console.log(`Found ${options.length} route options (search_id=${search_id})`);
import requests
url = "https://api.cyclemate.club/multi-modal/v2/"
headers = {
"apikey": "YOUR_API_KEY"
}
data = {
"origin": [-118.2401, 34.0498],
"destination": [-118.2601, 34.0699]
}
response = requests.post(url, headers=headers, json=data)
result = response.json()
…. Each leg’s steps follows the same step shape as a Get directions response.
{
"search_id": "f17bab36-b0cd-4155-9472-77c311878669",
"options": [
{
"name": "My bike",
"bike_type": "Personal",
"supersafe": true,
"nighttime": false,
"label": "SuperSafe™",
"sublabel": null,
"legs": [
{
"mode": "cycling",
"start_label": "Start route",
"end_label": "End",
"distance": 4241,
"duration": 1017,
"elevation_gain": 41,
"elevation_loss": 9,
"safety_code": 1.9,
"geometry": {
"type": "LineString",
"coordinates": [[-118.240115, 34.049835], "…"]
},
"steps": [
"…",
{
"distance": 388,
"duration": 93.0,
"delta_elevation": 3,
"geometry": {
"type": "LineString",
"coordinates": [[-118.240115115, 34.049834828], "…"]
},
"name": "East 1st Street",
"mode": "cycling",
"safety_code": 2,
"safety_classification": "moderate",
"exits": "",
"maneuver": {
"bearing_before": 310,
"bearing_after": 310,
"type": "Start",
"instruction": "Start on East 1st Street",
"location": [-118.240115115, 34.049834828]
},
"cycle_routes": null,
"lit": true,
"voice_instructions": [
"…"
],
"banner_instructions": [
"…"
]
},
"…"
],
"stops": ["…"]
}
],
"disruptions": [
{
"id": "manual:480",
"source": "manual",
"status": "active",
"name": "West 2nd Street",
"notes": "Road closed for street works",
"severity": "avoid",
"location": [-118.246099, 34.052013],
"start_date": "2027-03-10",
"start_minute": 0,
"end_date": "2027-03-20",
"end_minute": 1440
},
"…"
]
},
{
"name": "My bike",
"bike_type": "Personal",
"supersafe": false,
"nighttime": false,
"label": "Direct",
"sublabel": null,
"legs": ["…"]
},
{
"name": "Metro Bike Share",
"bike_type": "Docked",
"supersafe": true,
"nighttime": false,
"label": null,
"sublabel": null,
"legs": [
{
"mode": "walking",
"end_label": "7 bikes",
"distance": 139,
"duration": 100,
"elevation_gain": 0,
"elevation_loss": 0,
"safety_code": 0,
"geometry": { "type": "LineString", "coordinates": ["…"] },
"steps": ["…"],
"stops": []
},
{
"mode": "cycling",
"end_label": "11 docks",
"distance": 4130,
"duration": 991,
"elevation_gain": 40,
"elevation_loss": 9,
"safety_code": 1.8,
"geometry": { "type": "LineString", "coordinates": ["…"] },
"steps": ["…"],
"stops": ["…"]
},
{
"mode": "walking",
"end_label": "End",
"distance": 173,
"duration": 125,
"elevation_gain": 0,
"elevation_loss": 0,
"safety_code": 0,
"geometry": { "type": "LineString", "coordinates": ["…"] },
"steps": ["…"],
"stops": []
}
],
"rental": {
"pricing": {
"currency": "USD",
"unlock_fee": 1.75,
"per_min_rate": null,
"per_min_interval": null,
"per_km_rate": null,
"per_km_interval": null,
"description": "$1.75 every 30 minutes."
},
"app_links": {
"android": null,
"ios": null,
"logo": "https://auth.cyclemate.club/storage/v1/object/public/logos/metro_bike.png"
}
}
}
],
"destination_parking": null
}
{
"error": "Rate limit exceeded",
"retry_after_seconds": 42
}
Variants
With a custom route
JavaScript
const response = await fetch('https://api.cyclemate.club/multi-modal/v2/', {
method: 'POST',
headers: {
'apikey': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
origin: [-73.9851, 40.7589],
destination: [-73.9712, 40.7614],
event_id: 'a3b8c9d0-1e2f-4a5b-8c7d-6e5f4a3b2c1d',
destination_id: '3f9c6a1e-8b2d-4c5f-9e7a-1d2b3c4d5e6f',
}),
});
Route types
Personal bike
- Direct cycling route, always first in results, in both the SuperSafe and direct variant
- With
parking_typeset: cycle to the returnedparkingspot, then walk to the destination
Docked bike-share (GBFS/TFL)
- Walk → cycle station-to-station → walk
- Included only when suitable stations exist within 1.5 km
- Omitted when the pickup and drop-off resolve to the same station — there is no bike-share leg to ride. A custom route still allows it, since the route itself is the ride
Docked e-bike
- Same shape, from the nearest station with an e-bike available; cycling legs use the e-bike speed
- Marked
is_electric: true; name is"{system name} E-bike"
Dockless bike-share
- Walk to the nearest available bike → cycle to destination, one option per provider
- Marked
is_electric: truewhen the vehicle is electric; its cycling legs then use the e-bike speed - Included only when a bike exists within 1.5 km and is standing inside its operator’s service area. A bike parked outside that area is skipped, because a rental cannot start there
Custom routes
Whenevent_id resolves to a custom route:
- All cycling legs route to the custom route’s starting point
- Non-reroutable cycling legs are appended for the custom route itself — one leg per stop on the route, so a route with two intermediate stops arrives as three consecutive legs. Each leg ends with an arrive step named after its stop (
"Arrive at Look Mum No Hands"); a route with no stops stays a single leg. Sum the non-reroutable legs for the route’s total distance and duration - Custom route details are included in each option’s
eventfield - For docked bike-share, the final walking leg goes to the nearest station to the custom route’s endpoint
Navigate to parking
Setparking_type to end the personal route at a real parking spot near the destination, with a walking leg the rest of the way. The chosen spot comes back in parking and the cycling leg’s end_label is "Park bike".
{
"origin": [-0.0865, 51.5272],
"destination": [-0.0702, 51.5390],
"parking_type": "ebike",
"operator": "lime"
}
destination_parking reports the destination borough’s rule on every request. For "ebike" requests it lets you tell “park anywhere” (free_floating, no bay returned) from “no parking found” (parking: null in a bay_mandatory borough); for other requests it is the rule a dockless e-bike option will end its hire under.
Step semantics
Each leg’ssteps follows the same turn-by-turn shape as the directions endpoint:
step.distanceis the length of the road after the maneuver. To render “in X metres, …”, use the previous step’sdistance.step.maneuver.locationequalsstep.geometry.coordinates[0].- The final step of every leg is
"Arrive at destination". voice_instructionsandbanner_instructionsare generated per leg independently, because every leg ends in its own arrive step.
Error responses
400 Bad request
{
"error": "origin and destination must be [lon, lat] arrays"
}
{
"error": "parking_type must be 'ebike' or 'regular'"
}
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 /multi-modal/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 an empty options array. Retry after a short delay.
{
"error": "Routing is temporarily unavailable. Please try again shortly.",
"code": "ROUTING_UNAVAILABLE"
}