> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cyclemate.club/llms.txt
> Use this file to discover all available pages before exploring further.

# Create a route

> Create a custom route or group ride and receive it back

## Overview

Creates a route from a name and one route input, and answers `201` with the route. The line is matched or planned in the background: poll [`GET /custom-routes/{id}/`](https://docs.cyclemate.club/api-reference/custom-routes/get-route) until `route_status` is `ready`.

### Authentication

A [Login with Cyclemate](https://docs.cyclemate.club/api-reference/oauth.md) access token, or a signed-in rider's session token. Every route belongs to a rider: your app acts for the rider who connected it, and the route is theirs. Your app reaches only the routes it created: a route the rider made in our apps, or through another app, answers `404`.

```
Authorization: Bearer ACCESS_TOKEN
```

An API key on its own is rejected, as are anonymous requests. To create routes as your organisation, sign up for a Cyclemate account and connect your app to it with Login with Cyclemate.

### Rate limits

Riders and apps acting for them may make **60 requests a minute**; a request that routes between `stops` counts once per hop. Every rider may create **10 routes in any 24 hours**, counted across our apps and every connected app: the eleventh answers `429` with `code: "daily_limit"` and the seconds until a slot frees.

## Request body

**name** `string` *required*

1 to 120 characters.

One of `stops`, `legs` or `geometry` is required:

**stops** `array`

2 to 25 stops as `{ location: [longitude, latitude], name? }`, all in one city. We plan the safest cycling route between them, one leg per hop, and name each stop as given.

**legs** `array`

Your own line, 1 to 24 legs of `{ geometry, end_name?, hidden? }`. `geometry` is a GeoJSON LineString; each leg starts within 25 m of the previous leg's end. The whole route is at most 10,000 coordinates and 200 km and must start and end in the same city. `hidden: true` marks a shaping point that is not shown as a stop (fixed routes only). Pass `start_name` for the start.

**geometry** `object`

A single GeoJSON LineString, the same as one leg. Fixed routes only. `start_name` and `end_name` name its ends.

> **Optional parameters**
>
> **description** `string`
>
> Up to 4,000 characters.
>
> **emoji** `string` (default: 🚴)
>
> One emoji.
>
> **alias** `string`
>
> The link slug: 3 to 60 lowercase letters, digits and hyphens. Route and product names are reserved. Generated from the name when omitted. A taken alias answers `409`.
>
> **privacy** `string` (default: unlisted)
>
> `public`, `unlisted` (anyone with the link) or `private` (hosts only). A route your app creates is `unlisted` unless you say otherwise, so it stays off the rider's profile until they choose to share it.
>
> **route\_mode** `string` (default: dynamic)
>
> `dynamic` or `fixed`. A dynamic route is re-planned between its stops as roads change, so it has no hidden shaping points; `geometry` and a hidden leg require `fixed`.
>
> **theme** `object`
>
> The page theme. Keys: `headerFont`, `headerFlair`, `headerColor` (`#rrggbb`), `headerLayout`, `backgroundColor`, `backgroundTexture`, `animation`, `stickers` (up to 12 of `{ id, position: { x, y } }`). Every value must be one the app offers; an unknown one answers `400` naming it.
>
> **categories** `string[]`
>
> Up to 10 category slugs, for example `coffee`, `park`, `live-music`.
>
> **start\_time** `string`
>
> ISO 8601. Setting it makes the route a group ride. `end_time` (after it; derived from the route's duration when omitted), `max_rsvps` (0 to 1000) and `rsvp_questions` are accepted only with a start time.
>
> **rsvp\_questions** `array`
>
> Up to 20 of `{ prompt, required, type?, options? }`; `type` is `text` or `choice`, and a choice question lists `options` as `{ label, limit? }`. Ids are generated when omitted and must be sent back unchanged on edit.
>
> **show\_elevation** `boolean` (default: true)
>
> With `show_safety_colors`, `show_why_this_route` and `show_street_by_street`: which sections the route's page shows.
>
> **start\_name** `string`
>
> With `legs` or `geometry`: the name of the start. With `stops`, name the stops instead.
>
> **end\_name** `string`
>
> With `geometry`: the name of the end.

## Response body

**id** `string`

The route's id, a UUID string. Pass it as `event_id` to [`POST /multi-modal/v2/`](https://docs.cyclemate.club/api-reference/routing/multimodal) to ride it.

**alias** `string`

The route's link slug, chosen or generated from the name.

**url** `string`

The route's page on cyclemate.club.

**name** `string`

**description** `string`

**emoji** `string`

**privacy** `string`

`public`, `unlisted` or `private`.

**route\_mode** `string`

`dynamic` (we re-plan the line between the stops as roads change) or `fixed` (the line is kept as given).

**theme** `object`

The page theme, when one is set. The same object you send.

**categories** `array`

Each `{ slug, name, emoji }`.

**start\_time** `string`

Present on a group ride, with `end_time`, and `max_rsvps` and `rsvp_questions` when set.

**show\_elevation** `boolean`

With `show_safety_colors`, `show_why_this_route` and `show_street_by_street`: which sections the page shows.

**city** `string`

The city the route is in: `london`, `new_york_city`, `los_angeles` or `bay_area`.

**host** `object`

The rider who created the route: `{ name, slug, image_url }`. `null` once that rider has deleted their account.

**route** `object`

`distance_m`, `duration_s`, `polyline` (precision 5), `geometry` (a GeoJSON LineString; omitted in lists) and `stops`: the start then every stop in order, each `{ name, location }`, with `hidden: true` on a shaping point that is not shown as a stop.

**route\_status** `string`

`pending` while we match the line to the road network (a fixed route) or plan it between the stops (a dynamic route), `ready` once turn-by-turn steps and the safety summary are stored, `unavailable` when the line could not be matched or no route exists between the stops. A pending route is already viewable and rideable; poll every few seconds.

**images** `array`

Each `{ id, url, status, cover }`. `status` is `pending` until our image check has run, then `approved`. Hosts see every image with its status; everyone else sees approved images only.

**created\_at** `string`

**updated\_at** `string`

```bash cURL theme={null}
curl -X POST 'https://api.cyclemate.club/custom-routes/' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Culver City coffee loop",
    "description": "An easy Sunday loop with a stop at the bakery.",
    "emoji": "☕",
    "categories": ["coffee"],
    "stops": [
      { "location": [-118.37753, 34.02795], "name": "Home" },
      { "location": [-118.37866, 34.02727], "name": "Bakery" },
      { "location": [-118.37675, 34.02727], "name": "Park" }
    ]
  }'
```

```bash With your own line theme={null}
curl -X POST 'https://api.cyclemate.club/custom-routes/' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Canal towpath",
    "route_mode": "fixed",
    "start_name": "Angel",
    "end_name": "Victoria Park",
    "geometry": { "type": "LineString", "coordinates": [[-0.1057, 51.5322], [-0.1021, 51.5341], [-0.0557, 51.5360]] }
  }'
```

```json Response theme={null}
{
  "id": "78360cf7-0f4a-48f1-91ca-25e83db44aff",
  "alias": "culver-city-coffee-loop-1792536692813",
  "url": "https://cyclemate.club/explore/los-angeles/routes/culver-city-coffee-loop-1792536692813",
  "name": "Culver City coffee loop",
  "description": "An easy Sunday loop with a stop at the bakery.",
  "emoji": "☕",
  "privacy": "public",
  "route_mode": "dynamic",
  "categories": [{ "slug": "coffee", "name": "Coffee", "emoji": "☕" }],
  "show_elevation": true,
  "show_safety_colors": true,
  "show_why_this_route": true,
  "show_street_by_street": true,
  "city": "los_angeles",
  "host": { "name": "Tom", "slug": "tom", "image_url": null },
  "route": {
    "distance_m": 833.5,
    "duration_s": 200,
    "polyline": "ycunEbsoqUMu@EU??M?YH{Er…",
    "geometry": { "type": "LineString", "coordinates": [[-118.377621, 34.028292], [-118.37735, 34.028364], "…"] },
    "stops": [
      { "name": "Home", "location": [-118.377621, 34.028292] },
      { "name": "Bakery", "location": [-118.378755, 34.027277] },
      { "name": "Park", "location": [-118.37675, 34.02727] }
    ]
  },
  "route_status": "pending",
  "images": [],
  "created_at": "2026-10-20T22:51:32.816+00:00",
  "updated_at": "2026-10-20T22:51:32.816+00:00"
}
```

## Variants

### A group ride

Send `start_time` (and optionally `end_time`, `max_rsvps` and `rsvp_questions`). The route becomes a group ride with its own page under `/group-rides/`, riders can RSVP, and `end_time` is derived from the route's duration when you leave it out.

## Error responses

Every error body is `{ "error", "code" }`; `error` is a sentence, `code` is stable.

### 400 Bad request

Invalid body or query: the message names the field. `code` is `invalid_body`, or for a route input `outside_coverage` (the route starts outside the cities we cover), `cross_city` (its stops span two cities) or `no_route` (no cycling route exists between two stops).

```json theme={null}
{ "error": "unknown field: colour", "code": "invalid_body" }
```

### 401 Unauthorized

Missing or invalid credentials, or an anonymous caller.

```json theme={null}
{ "error": "Missing user credentials" }
```

### 404 Not found

No route with that id that you may see. A private route you do not host, a route you may read but not edit, and a route your app did not create all answer `404`, never `403`.

```json theme={null}
{ "error": "Custom route not found", "code": "not_found" }
```

### 405 Method not allowed

```json theme={null}
{ "error": "Method not allowed", "code": "method_not_allowed" }
```

### 409 Conflict

The alias you chose is already taken, by another route or by a rider's profile link.

```json theme={null}
{ "error": "That alias is already taken", "code": "alias_taken" }
```

### 429 Too many requests

The burst limit, with a `Retry-After` header; or the daily creation cap, which adds `code: "daily_limit"`.

```json theme={null}
{ "error": "Daily limit of 10 custom routes reached", "code": "daily_limit", "retry_after_seconds": 3600 }
```

### 500 Internal server error

```json theme={null}
{ "error": "Internal server error", "code": "internal_error" }
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.