{
  "openapi": "3.1.0",
  "info": {
    "title": "Biiikes",
    "version": "2.1.0",
    "description": "Group bike rides. Public reads need a developer key in the X-Api-Key header, or a rider's sign-in token; a rider's own layer needs their sign-in. The contract only grows within a major version: no field is removed, renamed, retyped or made nullable (ADR-0013). 2.0.0 is the one deliberate break: it requires the key (decision 1204)."
  },
  "jsonSchemaDialect": "https://spec.openapis.org/oas/3.1/dialect/base",
  "servers": [
    {
      "url": "https://biiikes.com/api",
      "description": "Biiikes"
    }
  ],
  "paths": {
    "/openapi.json": {
      "get": {
        "operationId": "getDocument",
        "summary": "The API's description, as OpenAPI 3.1",
        "responses": {
          "200": {
            "description": "The OpenAPI document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/metros/{metro}/days/{day}": {
      "get": {
        "operationId": "getDay",
        "summary": "One day of a metro's calendar",
        "parameters": [
          {
            "name": "metro",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]{2,32}$"
            },
            "example": "pdx"
          },
          {
            "name": "day",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-10-03"
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Words, each matched in a ride's title or its meeting place's name, as the website's search matches them.",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 200
            },
            "example": "gravel"
          },
          {
            "name": "far",
            "in": "query",
            "required": false,
            "style": "form",
            "explode": false,
            "description": "Distance bands, any of them. A ride that never stated a distance shows under every band.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "under_5",
                  "5_to_10",
                  "10_to_20",
                  "20_to_40",
                  "over_40"
                ]
              },
              "uniqueItems": true
            },
            "example": [
              "10_to_20"
            ]
          },
          {
            "name": "who",
            "in": "query",
            "required": false,
            "style": "form",
            "explode": false,
            "description": "Who a ride is for, any of these. A ride that never said shows under every one.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "General",
                  "Family Friendly",
                  "21+ Only"
                ]
              },
              "uniqueItems": true
            },
            "example": [
              "General"
            ]
          },
          {
            "name": "where",
            "in": "query",
            "required": false,
            "style": "form",
            "explode": false,
            "description": "Neighborhoods, any of them, by the district slug a meeting carries. A withheld meeting point is in none.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "pattern": "^[a-z0-9-]{1,80}$"
              },
              "uniqueItems": true
            },
            "example": [
              "portland-pearl-district"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "The day's nights",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Day"
                }
              }
            }
          },
          "400": {
            "description": "An input its schema refuses",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such metro",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No live developer key, and no rider's sign-in token: the same answer for a missing, unknown or deleted key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "apiKey": []
          },
          {
            "bearer": [
              "read"
            ]
          }
        ]
      }
    },
    "/rides/{ride}": {
      "get": {
        "operationId": "getRide",
        "summary": "One ride and the dates it meets",
        "parameters": [
          {
            "name": "ride",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9]{4,32}$"
            },
            "example": "zdh5pnp"
          }
        ],
        "responses": {
          "200": {
            "description": "The ride",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ride"
                }
              }
            }
          },
          "400": {
            "description": "An input its schema refuses",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such posted ride",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No live developer key, and no rider's sign-in token: the same answer for a missing, unknown or deleted key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "apiKey": []
          },
          {
            "bearer": [
              "read"
            ]
          }
        ]
      }
    },
    "/me/nights": {
      "get": {
        "operationId": "getMyNights",
        "summary": "The rider's nights and why each is theirs",
        "security": [
          {
            "bearer": [
              "read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "The rider's nights",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MyNights"
                }
              }
            }
          },
          "401": {
            "description": "No live token for this resource. WWW-Authenticate names the resource's metadata."
          }
        },
        "description": "The rider's nights from 30 days ago to 90 days ahead, each with why it is theirs, as their own calendar shows them."
      }
    },
    "/me/known": {
      "get": {
        "operationId": "getKnown",
        "summary": "The riders the rider follows among the people going, night by night",
        "security": [
          {
            "bearer": [
              "read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "metro",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]{2,32}$"
            },
            "example": "pdx"
          },
          {
            "name": "from",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-10-01"
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-10-07"
          }
        ],
        "responses": {
          "200": {
            "description": "Night by night",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KnownNights"
                }
              }
            }
          },
          "400": {
            "description": "An input its schema refuses",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No live token for this resource. WWW-Authenticate names the resource's metadata."
          },
          "404": {
            "description": "No such metro",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Night by night from `from` to `to` (at most 31 days) in one metro, the nights where somebody the rider follows is going, with the count and up to two names the signed-in page shows."
      }
    },
    "/me/friends": {
      "get": {
        "operationId": "getFriends",
        "summary": "The rides the rider's friends are going to, night by night",
        "security": [
          {
            "bearer": [
              "read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "metro",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]{2,32}$"
            },
            "example": "pdx"
          },
          {
            "name": "from",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-10-01"
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-10-07"
          },
          {
            "name": "close_friends_only",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            },
            "example": false
          }
        ],
        "responses": {
          "200": {
            "description": "Night by night",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FriendNights"
                }
              }
            }
          },
          "400": {
            "description": "An input its schema refuses",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No live token for this resource. WWW-Authenticate names the resource's metadata."
          },
          "404": {
            "description": "No such metro",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Night by night from `from` to `to` (at most 31 days) in one metro, the nights the rider's friends are going to: the riders they follow and the riders they marked close. Asked with `close_friends_only`, the nights a close friend is going to. Riders the rider has only ridden with are not friends."
      }
    },
    "/me/flagged": {
      "get": {
        "operationId": "getFlagged",
        "summary": "The nights a rider the rider flagged is going to",
        "security": [
          {
            "bearer": [
              "read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "metro",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]{2,32}$"
            },
            "example": "pdx"
          },
          {
            "name": "from",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-10-01"
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-10-07"
          }
        ],
        "responses": {
          "200": {
            "description": "Night by night",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlaggedNights"
                }
              }
            }
          },
          "400": {
            "description": "An input its schema refuses",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No live token for this resource. WWW-Authenticate names the resource's metadata."
          },
          "404": {
            "description": "No such metro",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Night by night from `from` to `to` (at most 31 days) in one metro, the nights where a rider the rider flagged said going or confirmed, each with those riders' display names and handles. For marking the public calendar's nights, which carry no flags."
      }
    }
  },
  "components": {
    "schemas": {
      "Meeting": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "withheld",
          "place_name",
          "neighbourhood",
          "neighbourhood_slug"
        ],
        "properties": {
          "withheld": {
            "type": "boolean",
            "description": "The leader marked the meeting point secret; nothing about where it is is published (decision 465).",
            "examples": [
              false
            ]
          },
          "place_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where the ride meets, as the page names it. Null when withheld.",
            "examples": [
              "Jamison Square"
            ]
          },
          "neighbourhood": {
            "type": [
              "string",
              "null"
            ],
            "description": "The district. For a withheld point, only when the leader chose to show it.",
            "examples": [
              "Pearl District"
            ]
          },
          "neighbourhood_slug": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[a-z0-9-]{1,80}$",
            "description": "The district's slug, which search's where takes (decision 1196). Null where the neighbourhood is not shown or is a city rather than a district; names repeat across cities, slugs do not.",
            "examples": [
              "portland-pearl-district"
            ]
          }
        },
        "description": "Where a night meets, exactly as the signed-out page shows it."
      },
      "Night": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "ride",
          "title",
          "day",
          "meet_time",
          "time_zone",
          "status",
          "cancellation_reason",
          "past",
          "going_count",
          "meeting",
          "url"
        ],
        "properties": {
          "ride": {
            "type": "string",
            "pattern": "^[a-z0-9]{4,32}$",
            "description": "The ride's public id.",
            "examples": [
              "zdh5pnp"
            ]
          },
          "title": {
            "type": "string",
            "examples": [
              "Forest Park Gravel"
            ]
          },
          "day": {
            "type": "string",
            "format": "date",
            "examples": [
              "2026-10-03"
            ]
          },
          "meet_time": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-2][0-9]:[0-5][0-9]$",
            "description": "Local wall-clock time in the metro's zone.",
            "examples": [
              "09:00"
            ]
          },
          "time_zone": {
            "type": "string",
            "description": "The metro's IANA zone.",
            "examples": [
              "America/Los_Angeles"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "on",
              "cancelled"
            ],
            "examples": [
              "on"
            ]
          },
          "cancellation_reason": {
            "type": [
              "string",
              "null"
            ],
            "examples": [
              null
            ]
          },
          "past": {
            "type": "boolean",
            "examples": [
              false
            ]
          },
          "going_count": {
            "type": "integer",
            "minimum": 0,
            "examples": [
              8
            ]
          },
          "meeting": {
            "$ref": "#/components/schemas/Meeting"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The night's page.",
            "examples": [
              "https://biiikes.com/pdx/forest-park-gravel-zdh5pnp/2026-10-03"
            ]
          }
        },
        "description": "One date of one ride, as anybody may read it. Names no rider."
      },
      "Day": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "metro",
          "day",
          "nights"
        ],
        "properties": {
          "metro": {
            "type": "string",
            "examples": [
              "pdx"
            ]
          },
          "day": {
            "type": "string",
            "format": "date",
            "examples": [
              "2026-10-03"
            ]
          },
          "nights": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Night"
            }
          }
        }
      },
      "Ride": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "ride",
          "title",
          "description",
          "type",
          "metro",
          "nights",
          "url"
        ],
        "properties": {
          "ride": {
            "type": "string",
            "pattern": "^[a-z0-9]{4,32}$",
            "examples": [
              "zdh5pnp"
            ]
          },
          "title": {
            "type": "string",
            "examples": [
              "Forest Park Gravel"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "examples": [
              "A gravel loop through Forest Park, back by eleven."
            ]
          },
          "type": {
            "type": "string",
            "examples": [
              "ride"
            ]
          },
          "metro": {
            "type": "string",
            "examples": [
              "pdx"
            ]
          },
          "nights": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Night"
            },
            "description": "The nights the ride's page lists."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "examples": [
              "https://biiikes.com/pdx/forest-park-gravel-zdh5pnp"
            ]
          }
        }
      },
      "MyNight": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "night",
          "why"
        ],
        "properties": {
          "night": {
            "$ref": "#/components/schemas/Night"
          },
          "why": {
            "type": "string",
            "enum": [
              "led",
              "going",
              "ridden",
              "saved"
            ],
            "examples": [
              "going"
            ]
          }
        },
        "description": "One of the rider's nights and why it is theirs."
      },
      "MyNights": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "nights",
          "flagged"
        ],
        "properties": {
          "nights": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MyNight"
            }
          },
          "flagged": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FlaggedNight"
            },
            "description": "The riders the rider flagged on these nights."
          }
        }
      },
      "KnownName": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "handle",
          "name",
          "flagged"
        ],
        "properties": {
          "handle": {
            "type": "string",
            "examples": [
              "samrides"
            ]
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "examples": [
              "Sam"
            ]
          },
          "flagged": {
            "type": "boolean",
            "description": "The rider flagged them.",
            "examples": [
              false
            ]
          }
        }
      },
      "Known": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "ride",
          "day",
          "known",
          "names"
        ],
        "properties": {
          "ride": {
            "type": "string",
            "pattern": "^[a-z0-9]{4,32}$",
            "examples": [
              "zdh5pnp"
            ]
          },
          "day": {
            "type": "string",
            "format": "date",
            "examples": [
              "2026-10-03"
            ]
          },
          "known": {
            "type": "integer",
            "minimum": 0,
            "description": "How many of the riders the rider follows are going.",
            "examples": [
              2
            ]
          },
          "names": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/KnownName"
            }
          }
        },
        "description": "The riders the rider follows among the people going to one night, and up to two names, as the signed-in page shows them."
      },
      "KnownNights": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "nights"
        ],
        "properties": {
          "nights": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Known"
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "error",
          "input",
          "detail"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "invalid_request",
              "not_found",
              "invalid_key"
            ],
            "examples": [
              "invalid_request"
            ]
          },
          "input": {
            "type": [
              "string",
              "null"
            ],
            "examples": [
              "day"
            ]
          },
          "detail": {
            "type": [
              "string",
              "null"
            ],
            "examples": [
              "is not a date, YYYY-MM-DD"
            ]
          }
        }
      },
      "FlaggedRider": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "handle",
          "name"
        ],
        "properties": {
          "handle": {
            "type": "string",
            "examples": [
              "samrides"
            ]
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "examples": [
              "Sam"
            ]
          }
        },
        "description": "A rider the reader flagged: their display name and handle, and nothing else."
      },
      "FlaggedNight": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "ride",
          "day",
          "riders"
        ],
        "properties": {
          "ride": {
            "type": "string",
            "pattern": "^[a-z0-9]{4,32}$",
            "examples": [
              "zdh5pnp"
            ]
          },
          "day": {
            "type": "string",
            "format": "date",
            "examples": [
              "2026-10-03"
            ]
          },
          "riders": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/FlaggedRider"
            }
          }
        },
        "description": "One night with riders the reader flagged going to it or confirmed. Only ever answered to the rider who made the flags; a flag shows even when the flagged rider blocked the reader."
      },
      "FlaggedNights": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "nights"
        ],
        "properties": {
          "nights": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FlaggedNight"
            }
          }
        }
      },
      "Friend": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "handle",
          "name",
          "close",
          "following",
          "flagged"
        ],
        "properties": {
          "handle": {
            "type": "string",
            "examples": [
              "samrides"
            ]
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "examples": [
              "Sam"
            ]
          },
          "close": {
            "type": "boolean",
            "description": "The rider marked them a close friend.",
            "examples": [
              true
            ]
          },
          "following": {
            "type": "boolean",
            "description": "The rider follows them.",
            "examples": [
              true
            ]
          },
          "flagged": {
            "type": "boolean",
            "description": "The rider flagged them.",
            "examples": [
              false
            ]
          }
        },
        "description": "A friend going: somebody the rider follows or marked a close friend."
      },
      "FriendNight": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "night",
          "friends"
        ],
        "properties": {
          "night": {
            "$ref": "#/components/schemas/Night"
          },
          "friends": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/Friend"
            }
          }
        },
        "description": "One night and the rider's friends going to it, close friends first."
      },
      "FriendNights": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "nights",
          "flagged"
        ],
        "properties": {
          "nights": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FriendNight"
            }
          },
          "flagged": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FlaggedNight"
            }
          }
        },
        "description": "The nights the rider's friends are going to, nights with a close friend first, and the riders the rider flagged on those nights."
      }
    },
    "securitySchemes": {
      "bearer": {
        "type": "oauth2",
        "description": "OAuth 2.1 with PKCE, issued by this origin's authorization server for the resource /api (ADR-0027).",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "/oauth/authorize",
            "tokenUrl": "/oauth/token",
            "refreshUrl": "/oauth/token",
            "scopes": {
              "read": "The calendar, the rider's nights, which of their friends are going, and anyone they flagged who is going"
            }
          }
        }
      },
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "A developer key, made on your Biiikes account under Developer keys and shown once (decisions 1202, 1204)."
      }
    }
  }
}
