{
  "openapi": "3.1.0",
  "info": {
    "title": "RideMyPark Public API",
    "summary": "Read-only access to the RideMyPark catalogue of skateparks, street spots, pumptracks, shops and events.",
    "description": "RideMyPark is the community map of skateparks, street spots and pumptracks: over 30,000 listings rated and mapped by riders, worldwide, deepest in France.\n\nThis document describes the **public, read-only** surface. No account, no API key, no token: every operation below answers an anonymous GET. Requests are rate limited per IP; a rejected request answers `429` with a `Retry-After` header.\n\n**What this API is good for.** Finding WHERE TO RIDE: spots around a place, filtered by radius, discipline, features, spot type, indoor/outdoor and minimum rating; the full record of one listing (address, photos, modules, opening hours, /5 community rating, reviews); map clusters for a viewport; and address geocoding.\n\n**What it is not.** Nothing here is bookable or for sale, and nothing can be written back. Creating or editing a listing, reading account data or posting a review all require an authenticated session; those surfaces are exposed to agents through the MCP server (see externalDocs) behind an OAuth consent, not through this document.\n\n**Response envelopes are historical.** The `/wp-json/rmp/v1` and `/wp-json/rmp/v2` prefixes and their envelopes come from the frozen mobile contract and are documented here exactly as they behave, quirks included: `v1` reports success as the STRING `\"true\"`/`\"false\"` and answers `200` even when a listing is missing (the `code` field carries the real status), `v2` reports success as a BOOLEAN, and latitude/longitude always travel as STRINGS. Do not assume; read each operation.",
    "version": "1.0.0",
    "license": {
      "name": "Terms of use",
      "url": "https://ridemypark.com/terms"
    },
    "contact": {
      "name": "RideMyPark",
      "url": "https://ridemypark.com/developers",
      "email": "contact@ridemypark.com"
    }
  },
  "externalDocs": {
    "description": "Developers and AI agents: the MCP server, the Markdown twin of every page, llms.txt and this spec.",
    "url": "https://ridemypark.com/developers"
  },
  "servers": [
    {
      "url": "https://ridemypark.com",
      "description": "Production. Serves the website and the public API on the same origin."
    },
    {
      "url": "https://api.ridemypark.com",
      "description": "Production API host. Same routes, never HTML."
    }
  ],
  "tags": [
    {
      "name": "Discovery",
      "description": "Machine-readable entry points: this spec, the MCP server manifest, llms.txt."
    },
    {
      "name": "Spots",
      "description": "The catalogue: search listings, read one listing in full."
    },
    {
      "name": "Map",
      "description": "Viewport endpoints for a map: markers and server-side clusters."
    },
    {
      "name": "Geocoding",
      "description": "Address and zone lookup, so a caller can turn a place name into coordinates before searching."
    },
    {
      "name": "Service",
      "description": "Liveness of the service itself."
    }
  ],
  "paths": {
    "/.well-known/mcp.json": {
      "get": {
        "operationId": "getMcpServerManifest",
        "summary": "Fetch the MCP server manifest",
        "description": "Returns the Model Context Protocol server manifest (registry `server.json` format) describing the RideMyPark MCP server and its Streamable HTTP endpoint. An agent that reads this can connect without being told the URL.",
        "tags": [
          "Discovery"
        ],
        "responses": {
          "200": {
            "description": "The MCP server manifest.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/McpServerManifest"
                }
              }
            }
          },
          "default": {
            "description": "Anything else answers the modern error envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "externalDocs": {
          "description": "MCP server",
          "url": "https://ridemypark.com/mcp"
        }
      }
    },
    "/health": {
      "get": {
        "operationId": "getServiceHealth",
        "summary": "Check service health",
        "description": "Liveness of the API and of its database. Answers `200` with `status: \"ok\"` whenever the process serves traffic; the `db` field reports the database separately, so a degraded read path is visible without guessing.",
        "tags": [
          "Service"
        ],
        "responses": {
          "200": {
            "description": "The service is answering.",
            "headers": {
              "X-App-Revision": {
                "description": "Git revision of the running build.",
                "schema": {
                  "type": "string",
                  "description": "Git SHA, or \"dev\" outside a Docker build."
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            }
          },
          "405": {
            "description": "This endpoint only answers GET and HEAD.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/llms.txt": {
      "get": {
        "operationId": "getLlmsTxt",
        "summary": "Fetch llms.txt",
        "description": "Returns the llmstxt.org guide for AI agents: what RideMyPark is, when to call it, when not to, and the key sections of the site as absolute links.",
        "tags": [
          "Discovery"
        ],
        "responses": {
          "200": {
            "description": "Markdown, served as text/plain per llmstxt.org.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "The llms.txt body."
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApiDocument",
        "summary": "Fetch this OpenAPI document",
        "description": "Returns this specification. It is generated from the running server, so the description an agent reads is always the description of the code that answers it. Also served as YAML at /openapi.yaml.",
        "tags": [
          "Discovery"
        ],
        "responses": {
          "200": {
            "description": "The OpenAPI 3.1 document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "An OpenAPI 3.1 document."
                }
              }
            }
          },
          "default": {
            "description": "Anything else — a write verb on this read-only endpoint, for instance — answers the modern error envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "externalDocs": {
          "description": "Developer resources",
          "url": "https://ridemypark.com/developers"
        }
      }
    },
    "/wp-json/rmp/v1/explore": {
      "get": {
        "operationId": "getMapMarkers",
        "summary": "Get raw map markers for a viewport",
        "description": "The unclustered viewport endpoint: every marker inside the rectangle, up to `countLimit`. Kept for clients that do their own clustering; for anything at country scale, `getMapClusters` is the cheaper call. ⚠️ Frozen v1 envelope: `success` is the STRING `\"true\"` and `data` is an OBJECT keyed by listing id, not an array.",
        "tags": [
          "Map"
        ],
        "parameters": [
          {
            "name": "topLatitude",
            "in": "query",
            "description": "Viewport north edge. `topLeftLatitude` is accepted as a legacy alias.",
            "schema": {
              "type": "number",
              "description": "Latitude in degrees."
            },
            "example": 48.91
          },
          {
            "name": "bottomLatitude",
            "in": "query",
            "description": "Viewport south edge. Alias: `bottomRightLatitude`.",
            "schema": {
              "type": "number",
              "description": "Latitude in degrees."
            },
            "example": 48.81
          },
          {
            "name": "leftLongitude",
            "in": "query",
            "description": "Viewport west edge. Alias: `topLeftLongitude`.",
            "schema": {
              "type": "number",
              "description": "Longitude in degrees."
            },
            "example": 2.22
          },
          {
            "name": "rightLongitude",
            "in": "query",
            "description": "Viewport east edge. Alias: `bottomRightLongitude`.",
            "schema": {
              "type": "number",
              "description": "Longitude in degrees."
            },
            "example": 2.47
          },
          {
            "name": "listing_type",
            "in": "query",
            "description": "Universe to plot.",
            "schema": {
              "type": "string",
              "enum": [
                "spot",
                "event",
                "shop"
              ],
              "default": "spot"
            }
          },
          {
            "name": "countLimit",
            "in": "query",
            "description": "Maximum number of markers returned.",
            "schema": {
              "type": "integer",
              "default": 500,
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Markers inside the viewport, keyed by listing id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExploreResponse"
                }
              }
            }
          }
        }
      }
    },
    "/wp-json/rmp/v1/listing": {
      "get": {
        "operationId": "getListing",
        "summary": "Read one listing in full",
        "description": "The complete record of a spot, event or shop: description, address, coordinates, taxonomies (spot type, materials, modules, sports), photo gallery, opening hours, community rating, reviews and related listings.\n\nIdentify the listing by `listing_slug` (preferred — it is the slug in the page URL, `/spots/\u003cslug\u003e`) or by `listing_id`.\n\n⚠️ Frozen contract: **this operation answers `200` even when the listing does not exist**. On failure the body is the legacy v1 error envelope, where `success` is the STRING `\"false\"` and `code` carries the real status (`400` missing parameter, `404` unknown listing, `500` internal). Always branch on the body, never on the HTTP status.",
        "tags": [
          "Spots"
        ],
        "parameters": [
          {
            "name": "listing_slug",
            "in": "query",
            "description": "Permalink slug of the listing, as it appears in its page URL.",
            "schema": {
              "type": "string",
              "description": "Slug."
            },
            "example": "cosanostra-skatepark"
          },
          {
            "name": "listing_id",
            "in": "query",
            "description": "Numeric id of the listing. Ignored when `listing_slug` is given.",
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "example": 43249
          }
        ],
        "responses": {
          "200": {
            "description": "The listing, or the legacy v1 error envelope (see the operation description).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Listing"
                }
              }
            }
          }
        }
      }
    },
    "/wp-json/rmp/v1/listing/{id}/ical": {
      "get": {
        "operationId": "getListingCalendar",
        "summary": "Download a listing's upcoming dates as iCalendar",
        "description": "Returns the upcoming dates of an event listing as an iCalendar (`.ics`) file, ready to import into a calendar. Answers `422` with the legacy v1 error envelope when the listing carries no upcoming date, and `404` when it does not exist.",
        "tags": [
          "Spots"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric id of the listing.",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "example": 43249
          }
        ],
        "responses": {
          "200": {
            "description": "An iCalendar document.",
            "content": {
              "text/calendar": {
                "schema": {
                  "type": "string",
                  "description": "iCalendar (RFC 5545) body."
                }
              }
            }
          },
          "404": {
            "description": "No such listing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyV1Error"
                }
              }
            }
          },
          "422": {
            "description": "The listing has no upcoming event date.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyV1Error"
                }
              }
            }
          }
        }
      }
    },
    "/wp-json/rmp/v2/explore/clusters": {
      "get": {
        "operationId": "getMapClusters",
        "summary": "Get map clusters for a viewport",
        "description": "Server-side clustering for a map viewport: below a zoom threshold the server returns aggregated cells (`type: \"cluster\"`, with the count and the bounding box to zoom into), above it individual points (`type: \"point\"`, with the listing id and title).\n\nPrefer this over `searchListings` to paint a map: a whole country answers in one call instead of thousands of markers. Accepts the same filters as the search.\n\nThe viewport is required, either as `bbox` or as the four corner parameters. `zoom` is required — the server needs it to pick the grid.",
        "tags": [
          "Map"
        ],
        "parameters": [
          {
            "name": "zoom",
            "in": "query",
            "description": "Map zoom level. Decides clusters versus individual points.",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 22
            },
            "example": 10
          },
          {
            "name": "bbox",
            "in": "query",
            "description": "Viewport as `minLng,minLat,maxLng,maxLat` in decimal degrees.",
            "schema": {
              "type": "string",
              "pattern": "^-?\\d+(\\.\\d+)?(,-?\\d+(\\.\\d+)?){3}$"
            },
            "example": "2.22,48.81,2.47,48.91"
          },
          {
            "name": "topLatitude",
            "in": "query",
            "description": "Viewport north edge. Alternative to `bbox`; all four corners must be given together.",
            "schema": {
              "type": "number",
              "description": "Latitude in degrees."
            }
          },
          {
            "name": "bottomLatitude",
            "in": "query",
            "description": "Viewport south edge.",
            "schema": {
              "type": "number",
              "description": "Latitude in degrees."
            }
          },
          {
            "name": "leftLongitude",
            "in": "query",
            "description": "Viewport west edge.",
            "schema": {
              "type": "number",
              "description": "Longitude in degrees."
            }
          },
          {
            "name": "rightLongitude",
            "in": "query",
            "description": "Viewport east edge.",
            "schema": {
              "type": "number",
              "description": "Longitude in degrees."
            }
          },
          {
            "name": "listing_type",
            "in": "query",
            "description": "Universe to plot. `type` is accepted as an alias.",
            "schema": {
              "type": "string",
              "enum": [
                "spot",
                "event",
                "shop"
              ],
              "default": "spot"
            }
          },
          {
            "name": "type",
            "in": "query",
            "description": "Universe to search.",
            "schema": {
              "type": "string",
              "enum": [
                "spot",
                "event",
                "shop"
              ],
              "default": "spot"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Title contains this text.",
            "schema": {
              "type": "string",
              "description": "Free text."
            },
            "example": "bowl"
          },
          {
            "name": "search",
            "in": "query",
            "description": "Broad keyword match over title, description and address. `search_keywords` is accepted as an alias.",
            "schema": {
              "type": "string",
              "description": "Free text."
            }
          },
          {
            "name": "spot_type",
            "in": "query",
            "description": "Spot type slugs. `spot-type` is accepted as an alias.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "skatepark",
                  "street",
                  "pumptrack",
                  "diy",
                  "bowl",
                  "skateshop"
                ]
              }
            },
            "example": "skatepark"
          },
          {
            "name": "categories",
            "in": "query",
            "description": "Category slugs. Aliases: `job_category`, `job_listing_category`.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "indoor",
                  "outdoor",
                  "closed"
                ]
              }
            },
            "example": "indoor"
          },
          {
            "name": "sport",
            "in": "query",
            "description": "Discipline slugs. Synonyms are expanded server-side (`skate` also matches `skateboard`).",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "skate",
                  "bmx",
                  "inline",
                  "scooter"
                ]
              }
            },
            "example": "bmx"
          },
          {
            "name": "elements",
            "in": "query",
            "description": "Module slugs the spot must have (rails, ledges, bowls…). API display names are normalised to slugs.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "example": "rail,quarters"
          },
          {
            "name": "made_of",
            "in": "query",
            "description": "Surface material slugs.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "example": "concrete"
          },
          {
            "name": "regions",
            "in": "query",
            "description": "Region slugs the listing belongs to.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "example": "ile-de-france"
          },
          {
            "name": "tags",
            "in": "query",
            "description": "Free tag slugs. `job_tags` is accepted as an alias.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "region",
            "in": "query",
            "description": "Free-text region name, matched against the listing's stored region.",
            "schema": {
              "type": "string",
              "description": "Region name."
            }
          },
          {
            "name": "country",
            "in": "query",
            "description": "Free-text country name, matched against the listing's stored country.",
            "schema": {
              "type": "string",
              "description": "Country name."
            },
            "example": "France"
          },
          {
            "name": "rating_min",
            "in": "query",
            "description": "Minimum community rating, on the STORAGE scale 0-10 (the site shows /5, so 7 here is 3.5 stars). `0` or absent means no filter.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10
            },
            "example": 7
          },
          {
            "name": "pictures",
            "in": "query",
            "description": "Restrict to listings that do or do not carry a photo.",
            "schema": {
              "type": "string",
              "enum": [
                "with",
                "without"
              ]
            }
          },
          {
            "name": "open_now",
            "in": "query",
            "description": "Drop the listings whose opening hours say they are CLOSED right now. Listings with no opening hours on file are kept: nothing is known about them, and hiding them would claim they are closed.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "featured",
            "in": "query",
            "description": "Keep only (or exclude) featured listings.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "date_from",
            "in": "query",
            "description": "Events universe only: lower bound of the event date range. `YYYY-MM-DD` (start of that day) or RFC 3339.",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-09-01"
          },
          {
            "name": "date_to",
            "in": "query",
            "description": "Events universe only: upper bound. A bare `YYYY-MM-DD` means the END of that day.",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-09-30"
          },
          {
            "name": "event_date",
            "in": "query",
            "description": "Events universe only: a named date window, translated server-side into `date_from`/`date_to`. Explicit bounds always win.",
            "schema": {
              "type": "string",
              "enum": [
                "any",
                "all",
                "today",
                "this-week",
                "next-week"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Clusters or points for the viewport.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClustersResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing or malformed viewport, or missing `zoom`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyV2Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                }
              }
            }
          }
        }
      }
    },
    "/wp-json/rmp/v2/geocode": {
      "get": {
        "operationId": "geocodeAddress",
        "summary": "Turn an address into coordinates",
        "description": "Forward geocoding: an address or place name in, a short list of candidates out, each with a display label and decimal coordinates. Use it to resolve a user's phrasing (\"skateparks near Bordeaux\") into the `lat`/`lng` that `searchListings` expects. Unlike search markers, coordinates here are NUMBERS. Missing or unmatched queries answer `200` with an empty list.",
        "tags": [
          "Geocoding"
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "The address or place to resolve. `address` is accepted as an alias.",
            "schema": {
              "type": "string",
              "description": "Free text."
            },
            "example": "12 rue de Bercy, Paris"
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of candidates.",
            "schema": {
              "type": "integer",
              "default": 5,
              "minimum": 1,
              "maximum": 10
            }
          },
          {
            "name": "language",
            "in": "query",
            "description": "Preferred language of the labels (ISO 639-1).",
            "schema": {
              "type": "string",
              "description": "Language code."
            },
            "example": "fr"
          }
        ],
        "responses": {
          "200": {
            "description": "Candidate places, best first. Possibly empty.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GeocodeResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited — this endpoint proxies an upstream geocoder and is capped tightly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                }
              }
            }
          }
        }
      }
    },
    "/wp-json/rmp/v2/geocode/reverse": {
      "get": {
        "operationId": "reverseGeocodeCoordinates",
        "summary": "Turn coordinates into an address",
        "description": "Reverse geocoding: a point in, the address that covers it out. `result` is `null` when nothing resolves — that is a `200`, not an error.",
        "tags": [
          "Geocoding"
        ],
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "description": "Latitude in decimal degrees.",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": -90,
              "maximum": 90
            },
            "example": 48.8584
          },
          {
            "name": "lng",
            "in": "query",
            "description": "Longitude in decimal degrees.",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": -180,
              "maximum": 180
            },
            "example": 2.2945
          },
          {
            "name": "language",
            "in": "query",
            "description": "Preferred language of the label (ISO 639-1).",
            "schema": {
              "type": "string",
              "description": "Language code."
            },
            "example": "fr"
          }
        ],
        "responses": {
          "200": {
            "description": "The address covering the point, or `null`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReverseGeocodeResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                }
              }
            }
          }
        }
      }
    },
    "/wp-json/rmp/v2/geocode/zone": {
      "get": {
        "operationId": "geocodeZone",
        "summary": "Resolve a city or region to its boundary",
        "description": "Zone geocoding: a city, department, region or country name in, its centre, bounding box and GeoJSON boundary out. Feed the bounding box to `getMapClusters` or `searchListings` to scope a search to an administrative area rather than to a radius. `data` is `null` when no zone matches.",
        "tags": [
          "Geocoding"
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Name of the city, department, region or country.",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Place name."
            },
            "example": "Bordeaux"
          },
          {
            "name": "language",
            "in": "query",
            "description": "Preferred language of the labels (ISO 639-1).",
            "schema": {
              "type": "string",
              "default": "en"
            },
            "example": "fr"
          }
        ],
        "responses": {
          "200": {
            "description": "The zone, or `data: null` when nothing matches.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GeocodeZoneResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                }
              }
            }
          }
        }
      }
    },
    "/wp-json/rmp/v2/quick-search": {
      "get": {
        "operationId": "quickSearchListings",
        "summary": "Autocomplete listings, places and riders",
        "description": "Type-ahead search, built for a search box: a short list of listings whose title matches, optional place suggestions from the geocoder, and optional rider accounts. Returns empty arrays rather than an error when the query is missing, so a caller can fire it on every keystroke.",
        "tags": [
          "Spots"
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "The typed text. `search_term` is accepted as a legacy alias.",
            "schema": {
              "type": "string",
              "description": "Free text."
            },
            "example": "bercy"
          },
          {
            "name": "type",
            "in": "query",
            "description": "Universe to search.",
            "schema": {
              "type": "string",
              "enum": [
                "spot",
                "event",
                "shop"
              ],
              "default": "spot"
            }
          },
          {
            "name": "listings_limit",
            "in": "query",
            "description": "How many listings to return.",
            "schema": {
              "type": "integer",
              "default": 10,
              "minimum": 1,
              "maximum": 50
            }
          },
          {
            "name": "places_limit",
            "in": "query",
            "description": "How many place suggestions to return. Five is the ceiling: one Autocomplete call never returns more.",
            "schema": {
              "type": "integer",
              "default": 5,
              "minimum": 0,
              "maximum": 5
            }
          },
          {
            "name": "language",
            "in": "query",
            "description": "Language of the place suggestions (ISO 639-1).",
            "schema": {
              "type": "string",
              "default": "en"
            },
            "example": "fr"
          },
          {
            "name": "skip_places",
            "in": "query",
            "description": "Set to `1` to skip the geocoder entirely — the cheapest call.",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            }
          },
          {
            "name": "skip_users",
            "in": "query",
            "description": "Set to `1` to skip rider accounts.",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching listings, places and riders. Every block is an array, possibly empty.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuickSearchResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                }
              }
            }
          }
        }
      }
    },
    "/wp-json/rmp/v2/search": {
      "get": {
        "operationId": "searchListings",
        "summary": "Search spots, events and shops",
        "description": "The main entry point of the catalogue. Returns a page of listings, each with its coordinates, address, spot type, modules, photo, and community rating.\n\nTwo ways to scope a search, combinable with every filter below:\n- **around a point**: pass `lat` and `lng`; results are ordered by distance and cut at `proximity` (default 50 km). Without both, no radius is applied.\n- **inside a viewport**: pass `bbox` as `minLng,minLat,maxLng,maxLat`.\n\nList parameters accept three forms: comma separated (`elements=rail,bowl`), repeated (`elements=rail\u0026elements=bowl`) and bracketed (`elements[]=rail`). A malformed value is treated as absent rather than rejected — the grammar is deliberately forgiving, inherited from the mobile contract.\n\nLatitude and longitude come back as STRINGS, and `page` is 1-based in the response.",
        "tags": [
          "Spots"
        ],
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "description": "Latitude of the search centre. Must be sent with `lng`; without both, no radius applies.",
            "schema": {
              "type": "number",
              "minimum": -90,
              "maximum": 90
            },
            "example": 48.8584
          },
          {
            "name": "lng",
            "in": "query",
            "description": "Longitude of the search centre.",
            "schema": {
              "type": "number",
              "minimum": -180,
              "maximum": 180
            },
            "example": 2.2945
          },
          {
            "name": "proximity",
            "in": "query",
            "description": "Search radius around `lat`/`lng`, in `proximity_units`.",
            "schema": {
              "type": "number",
              "default": 50,
              "minimum": 0
            }
          },
          {
            "name": "proximity_units",
            "in": "query",
            "description": "Unit of `proximity`.",
            "schema": {
              "type": "string",
              "enum": [
                "km",
                "mi"
              ],
              "default": "km"
            }
          },
          {
            "name": "bbox",
            "in": "query",
            "description": "Viewport as `minLng,minLat,maxLng,maxLat` in decimal degrees.",
            "schema": {
              "type": "string",
              "pattern": "^-?\\d+(\\.\\d+)?(,-?\\d+(\\.\\d+)?){3}$"
            },
            "example": "2.22,48.81,2.47,48.91"
          },
          {
            "name": "type",
            "in": "query",
            "description": "Universe to search.",
            "schema": {
              "type": "string",
              "enum": [
                "spot",
                "event",
                "shop"
              ],
              "default": "spot"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Title contains this text.",
            "schema": {
              "type": "string",
              "description": "Free text."
            },
            "example": "bowl"
          },
          {
            "name": "search",
            "in": "query",
            "description": "Broad keyword match over title, description and address. `search_keywords` is accepted as an alias.",
            "schema": {
              "type": "string",
              "description": "Free text."
            }
          },
          {
            "name": "spot_type",
            "in": "query",
            "description": "Spot type slugs. `spot-type` is accepted as an alias.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "skatepark",
                  "street",
                  "pumptrack",
                  "diy",
                  "bowl",
                  "skateshop"
                ]
              }
            },
            "example": "skatepark"
          },
          {
            "name": "categories",
            "in": "query",
            "description": "Category slugs. Aliases: `job_category`, `job_listing_category`.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "indoor",
                  "outdoor",
                  "closed"
                ]
              }
            },
            "example": "indoor"
          },
          {
            "name": "sport",
            "in": "query",
            "description": "Discipline slugs. Synonyms are expanded server-side (`skate` also matches `skateboard`).",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "skate",
                  "bmx",
                  "inline",
                  "scooter"
                ]
              }
            },
            "example": "bmx"
          },
          {
            "name": "elements",
            "in": "query",
            "description": "Module slugs the spot must have (rails, ledges, bowls…). API display names are normalised to slugs.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "example": "rail,quarters"
          },
          {
            "name": "made_of",
            "in": "query",
            "description": "Surface material slugs.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "example": "concrete"
          },
          {
            "name": "regions",
            "in": "query",
            "description": "Region slugs the listing belongs to.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "example": "ile-de-france"
          },
          {
            "name": "tags",
            "in": "query",
            "description": "Free tag slugs. `job_tags` is accepted as an alias.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "region",
            "in": "query",
            "description": "Free-text region name, matched against the listing's stored region.",
            "schema": {
              "type": "string",
              "description": "Region name."
            }
          },
          {
            "name": "country",
            "in": "query",
            "description": "Free-text country name, matched against the listing's stored country.",
            "schema": {
              "type": "string",
              "description": "Country name."
            },
            "example": "France"
          },
          {
            "name": "rating_min",
            "in": "query",
            "description": "Minimum community rating, on the STORAGE scale 0-10 (the site shows /5, so 7 here is 3.5 stars). `0` or absent means no filter.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 10
            },
            "example": 7
          },
          {
            "name": "pictures",
            "in": "query",
            "description": "Restrict to listings that do or do not carry a photo.",
            "schema": {
              "type": "string",
              "enum": [
                "with",
                "without"
              ]
            }
          },
          {
            "name": "open_now",
            "in": "query",
            "description": "Drop the listings whose opening hours say they are CLOSED right now. Listings with no opening hours on file are kept: nothing is known about them, and hiding them would claim they are closed.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "featured",
            "in": "query",
            "description": "Keep only (or exclude) featured listings.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "date_from",
            "in": "query",
            "description": "Events universe only: lower bound of the event date range. `YYYY-MM-DD` (start of that day) or RFC 3339.",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-09-01"
          },
          {
            "name": "date_to",
            "in": "query",
            "description": "Events universe only: upper bound. A bare `YYYY-MM-DD` means the END of that day.",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-09-30"
          },
          {
            "name": "event_date",
            "in": "query",
            "description": "Events universe only: a named date window, translated server-side into `date_from`/`date_to`. Explicit bounds always win.",
            "schema": {
              "type": "string",
              "enum": [
                "any",
                "all",
                "today",
                "this-week",
                "next-week"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number.",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Results per page. Values above 500 are clamped to 500.",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 500
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Sort key (rating, newest, name, showcase). `showcase` is the directory order used by /region pages: credible rating first (2+ reviews), then photos, review count, raw rating, freshness, id. Defaults to distance when a centre is given, otherwise to the catalogue order.",
            "schema": {
              "type": "string",
              "description": "Sort key."
            }
          },
          {
            "name": "order",
            "in": "query",
            "description": "Sort direction.",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of results. On an internal failure the same `200` carries the legacy v2 error envelope (`success: false`) instead — check `success` before reading `data`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Retry after the number of seconds in the `Retry-After` header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Author": {
        "type": "object",
        "description": "The rider behind a listing.",
        "properties": {
          "ID": {
            "type": "string",
            "description": "Account id, as a STRING (frozen contract)."
          },
          "display_name": {
            "type": "string",
            "description": "Name shown on the profile."
          },
          "private_account": {
            "type": "boolean",
            "description": "Whether the rider hides their profile."
          },
          "profile_picture": {
            "type": [
              "string",
              "null"
            ],
            "description": "Absolute avatar URL, or null."
          },
          "wax_points": {
            "type": "integer",
            "description": "Contribution score."
          }
        },
        "required": [
          "ID",
          "display_name"
        ]
      },
      "BoundingBox": {
        "type": "object",
        "description": "A geographic envelope. Coordinates are STRINGS (frozen contract).",
        "properties": {
          "maxLat": {
            "type": "string",
            "description": "North edge."
          },
          "maxLng": {
            "type": "string",
            "description": "East edge."
          },
          "minLat": {
            "type": "string",
            "description": "South edge."
          },
          "minLng": {
            "type": "string",
            "description": "West edge."
          }
        },
        "required": [
          "minLng",
          "minLat",
          "maxLng",
          "maxLat"
        ]
      },
      "ClusterEntry": {
        "type": "object",
        "description": "An aggregated cell (`type: \"cluster\"`) or a single listing (`type: \"point\"`).",
        "properties": {
          "ID": {
            "type": "string",
            "description": "Listing id, as a STRING (frozen contract). Points only.",
            "examples": [
              "43249"
            ]
          },
          "bbox": {
            "$ref": "#/components/schemas/BoundingBox",
            "description": "Envelope of the cluster, to zoom into on click. Clusters only."
          },
          "categories": {
            "type": "array",
            "description": "Points only.",
            "items": {
              "type": "string",
              "description": "Category slug."
            }
          },
          "count": {
            "type": "integer",
            "description": "Listings aggregated in this entry. `1` for a point."
          },
          "latitude": {
            "type": "string",
            "description": "Latitude of the cluster centroid or of the point in decimal degrees, as a STRING (frozen contract)."
          },
          "longitude": {
            "type": "string",
            "description": "Longitude of the cluster centroid or of the point in decimal degrees, as a STRING (frozen contract)."
          },
          "spot_type": {
            "type": "array",
            "description": "Points only, drives the marker icon.",
            "items": {
              "type": "string",
              "description": "Spot type slug."
            }
          },
          "title": {
            "type": "string",
            "description": "Listing name. Points only."
          },
          "type": {
            "type": "string",
            "description": "Discriminator.",
            "enum": [
              "cluster",
              "point"
            ]
          }
        },
        "required": [
          "type",
          "count",
          "latitude",
          "longitude"
        ]
      },
      "ClustersResponse": {
        "type": "object",
        "description": "Clusters or points for a map viewport.",
        "properties": {
          "data": {
            "type": "array",
            "description": "The entries. Heterogeneous — branch on `type`.",
            "items": {
              "$ref": "#/components/schemas/ClusterEntry"
            }
          },
          "mode": {
            "type": "string",
            "description": "Whether entries are aggregated cells or individual listings.",
            "enum": [
              "clusters",
              "points"
            ]
          },
          "numbers": {
            "type": "integer",
            "description": "Number of entries in `data`."
          },
          "success": {
            "type": "boolean",
            "description": "`false` means the request was rejected; read `message`."
          },
          "zoom": {
            "type": "integer",
            "description": "Zoom level the server used."
          }
        },
        "required": [
          "success",
          "zoom",
          "mode",
          "numbers",
          "data"
        ]
      },
      "Error": {
        "type": "object",
        "description": "The modern error envelope. It is what an unrouted path and the machine-readable files answer with. It is NOT what `/wp-json/rmp/v1` and `/wp-json/rmp/v2` answer with — those keep their frozen envelopes, described by `LegacyV1Error` and `LegacyV2Error`.",
        "properties": {
          "error": {
            "type": "object",
            "description": "The failure.",
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable reason. Branch on this, never on the message.",
                "enum": [
                  "not_found",
                  "gone",
                  "method_not_allowed"
                ]
              },
              "documentation_url": {
                "type": "string",
                "format": "uri",
                "description": "Where the surface is documented."
              },
              "hint": {
                "type": "string",
                "description": "What to do next. Present whenever there is a useful next step.",
                "examples": [
                  "Read https://ridemypark.com/openapi.json for the routes this API serves."
                ]
              },
              "message": {
                "type": "string",
                "description": "One sentence in English, for a human reading a log.",
                "examples": [
                  "No API route matches this path."
                ]
              },
              "status": {
                "type": "integer",
                "description": "The HTTP status, repeated in the body so a client that only reads JSON still knows.",
                "examples": [
                  404
                ]
              }
            },
            "required": [
              "code",
              "message",
              "status"
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "ExploreMarker": {
        "type": "object",
        "description": "One marker of the explore endpoint.",
        "properties": {
          "ID": {
            "type": "string",
            "description": "Listing id, as a STRING (frozen contract)."
          },
          "categories": {
            "type": "array",
            "description": "Indoor / outdoor / closed.",
            "items": {
              "type": "string",
              "description": "Category slug."
            }
          },
          "latitude": {
            "type": "string",
            "description": "Latitude in decimal degrees, as a STRING (frozen contract)."
          },
          "logo": {
            "type": "array",
            "description": "Card photo.",
            "items": {
              "type": "string",
              "description": "Absolute image URL."
            }
          },
          "longitude": {
            "type": "string",
            "description": "Longitude in decimal degrees, as a STRING (frozen contract)."
          },
          "spot_type": {
            "type": "array",
            "description": "Skatepark, street, pumptrack…",
            "items": {
              "type": "string",
              "description": "Spot type slug."
            }
          },
          "title": {
            "type": "string",
            "description": "Listing name."
          }
        },
        "required": [
          "ID",
          "title",
          "latitude",
          "longitude"
        ]
      },
      "ExploreResponse": {
        "type": "object",
        "description": "Raw markers for a viewport. ⚠️ Frozen v1 envelope: `success` is a STRING and `data` is an OBJECT keyed by listing id.",
        "properties": {
          "code": {
            "type": "integer",
            "description": "Legacy status code carried in the body."
          },
          "data": {
            "type": "object",
            "description": "Markers keyed by listing id (the id is the key, and is repeated in the value).",
            "additionalProperties": {
              "$ref": "#/components/schemas/ExploreMarker"
            }
          },
          "numbers": {
            "type": "integer",
            "description": "Number of entries in `data`."
          },
          "success": {
            "type": "string",
            "description": "The STRING \"true\" or \"false\".",
            "enum": [
              "true",
              "false"
            ]
          },
          "totalCount": {
            "type": "integer",
            "description": "Total markers inside the viewport before `countLimit`."
          }
        },
        "required": [
          "success",
          "code",
          "numbers",
          "totalCount",
          "data"
        ]
      },
      "GalleryImage": {
        "type": "object",
        "description": "A photo attached to a review.",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Media id."
          },
          "url": {
            "type": "string",
            "description": "Absolute image URL."
          }
        },
        "required": [
          "id",
          "url"
        ]
      },
      "GeocodeResponse": {
        "type": "object",
        "description": "Forward geocoding candidates.",
        "properties": {
          "results": {
            "type": "array",
            "description": "Candidates, best first. Possibly empty.",
            "items": {
              "$ref": "#/components/schemas/GeocodeResult"
            }
          }
        },
        "required": [
          "results"
        ]
      },
      "GeocodeResult": {
        "type": "object",
        "description": "One geocoding hit. Coordinates are NUMBERS here, unlike catalogue markers.",
        "properties": {
          "label": {
            "type": "string",
            "description": "Human-readable place label.",
            "examples": [
              "Paris, France"
            ]
          },
          "lat": {
            "type": "number",
            "description": "Latitude in decimal degrees.",
            "examples": [
              48.8575475
            ]
          },
          "lng": {
            "type": "number",
            "description": "Longitude in decimal degrees.",
            "examples": [
              2.3513765
            ]
          }
        },
        "required": [
          "label",
          "lat",
          "lng"
        ]
      },
      "GeocodeZone": {
        "type": "object",
        "description": "An administrative zone with its boundary.",
        "properties": {
          "bbox": {
            "$ref": "#/components/schemas/ZoneBoundingBox",
            "description": "Envelope of the zone. Feed it to `getMapClusters` or `searchListings`."
          },
          "center": {
            "$ref": "#/components/schemas/ZoneCenter",
            "description": "Centre of the zone."
          },
          "display_name": {
            "type": "string",
            "description": "Full name with its parents.",
            "examples": [
              "Bordeaux, Gironde, Nouvelle-Aquitaine, France"
            ]
          },
          "name": {
            "type": "string",
            "description": "Short name of the zone.",
            "examples": [
              "Bordeaux"
            ]
          },
          "polygon": {
            "type": [
              "object",
              "null"
            ],
            "description": "GeoJSON Polygon or MultiPolygon boundary, simplified. Null when the geocoder has no geometry."
          },
          "type": {
            "type": "string",
            "description": "Kind of zone, as reported by the geocoder.",
            "examples": [
              "administrative"
            ]
          }
        },
        "required": [
          "name",
          "display_name",
          "type",
          "center",
          "bbox",
          "polygon"
        ]
      },
      "GeocodeZoneResponse": {
        "type": "object",
        "description": "A geocoded administrative zone.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/GeocodeZone",
            "description": "Null when no zone matches."
          },
          "success": {
            "type": "boolean",
            "description": "Whether the lookup ran."
          }
        },
        "required": [
          "success",
          "data"
        ]
      },
      "HealthResponse": {
        "type": "object",
        "description": "Liveness of the service.",
        "properties": {
          "db": {
            "type": "string",
            "description": "Database reachability.",
            "enum": [
              "ok",
              "error"
            ]
          },
          "revision": {
            "type": "string",
            "description": "Git revision of the running build.",
            "examples": [
              "ac7b59dfe394f0478ed112fa43bce8006d0ce82a"
            ]
          },
          "status": {
            "type": "string",
            "description": "Always \"ok\" when the process answers.",
            "const": "ok"
          }
        },
        "required": [
          "status",
          "db",
          "revision"
        ]
      },
      "LegacyV1Error": {
        "type": "object",
        "description": "The frozen v1 error envelope. `success` is the STRING \"false\" and `code` carries the real status — which the HTTP status does not always repeat.",
        "properties": {
          "code": {
            "type": "integer",
            "description": "The real status of the failure.",
            "examples": [
              404
            ]
          },
          "message": {
            "type": "string",
            "description": "What went wrong, in English.",
            "examples": [
              "Listing not found"
            ]
          },
          "success": {
            "type": "string",
            "description": "The STRING \"false\".",
            "const": "false"
          }
        },
        "required": [
          "success",
          "code",
          "message"
        ]
      },
      "LegacyV2Error": {
        "type": "object",
        "description": "The frozen v2 error envelope.",
        "properties": {
          "message": {
            "type": "string",
            "description": "What went wrong.",
            "examples": [
              "Paramètre bbox invalide"
            ]
          },
          "success": {
            "type": "boolean",
            "description": "Always `false` here."
          }
        },
        "required": [
          "success",
          "message"
        ]
      },
      "Listing": {
        "type": "object",
        "description": "A listing in full. ⚠️ On failure this same `200` carries a `LegacyV1Error` body instead.",
        "properties": {
          "ID": {
            "type": "string",
            "description": "Listing id, as a STRING (frozen contract).",
            "examples": [
              "43249"
            ]
          },
          "author": {
            "$ref": "#/components/schemas/Author",
            "description": "The rider who created the listing."
          },
          "average_rating": {
            "type": "number",
            "description": "Community rating on the STORAGE scale 0-10. The site shows it out of 5.",
            "examples": [
              7
            ]
          },
          "categories": {
            "type": "array",
            "description": "Indoor / outdoor / closed.",
            "items": {
              "type": "string",
              "description": "Category display name."
            }
          },
          "claimable": {
            "type": "boolean",
            "description": "Whether the listing can still be claimed by its owner."
          },
          "contributionAuthors": {
            "type": "array",
            "description": "Riders who enriched the listing.",
            "items": {
              "type": "string",
              "description": "Display name."
            }
          },
          "cover": {
            "type": "array",
            "description": "Cover photo.",
            "items": {
              "type": "string",
              "description": "Absolute image URL."
            }
          },
          "date": {
            "type": "integer",
            "description": "Publication date, Unix seconds."
          },
          "description": {
            "type": "string",
            "description": "Rider-written description. Mostly French — this is user-generated content."
          },
          "elements": {
            "type": "array",
            "description": "Modules on the spot.",
            "items": {
              "type": "string",
              "description": "Module display name."
            }
          },
          "email": {
            "type": "string",
            "description": "Contact email, when a contributor filled it in."
          },
          "event_date": {
            "type": [
              "string",
              "array",
              "object",
              "null"
            ],
            "description": "Event dates. Shape varies by listing — frozen contract."
          },
          "formattedAddress": {
            "type": "string",
            "description": "Postal address as displayed."
          },
          "gallery": {
            "type": "array",
            "description": "Photo gallery.",
            "items": {
              "type": "string",
              "description": "Absolute image URL."
            }
          },
          "job_phone": {
            "type": "string",
            "description": "Phone number, when a contributor filled it in."
          },
          "job_website": {
            "type": "string",
            "description": "Website, when a contributor filled it in."
          },
          "latitude": {
            "type": "string",
            "description": "Latitude in decimal degrees, as a STRING (frozen contract)."
          },
          "linked_spot_id": {
            "type": "string",
            "description": "For an event: the id of the spot it takes place on. Omitted otherwise."
          },
          "listing_type": {
            "type": "string",
            "description": "Universe of the listing.",
            "enum": [
              "spot",
              "event",
              "shop"
            ]
          },
          "logo": {
            "type": "array",
            "description": "Card photo.",
            "items": {
              "type": "string",
              "description": "Absolute image URL."
            }
          },
          "longitude": {
            "type": "string",
            "description": "Longitude in decimal degrees, as a STRING (frozen contract)."
          },
          "made_of": {
            "type": "array",
            "description": "Surface materials.",
            "items": {
              "type": "string",
              "description": "Material display name."
            }
          },
          "rating_count": {
            "type": "integer",
            "description": "Number of reviews."
          },
          "regions": {
            "type": "array",
            "description": "Administrative areas the listing belongs to.",
            "items": {
              "type": "string",
              "description": "Region display name."
            }
          },
          "related_listing": {
            "type": "array",
            "description": "Nearby listings.",
            "items": {
              "type": "object",
              "description": "A nearby listing, shaped like a `SearchMarker`."
            }
          },
          "reviews": {
            "type": "array",
            "description": "Community reviews, newest first.",
            "items": {
              "$ref": "#/components/schemas/Review"
            }
          },
          "slug": {
            "type": "string",
            "description": "Permalink slug. The page lives at `/spots/\u003cslug\u003e`."
          },
          "social_networks": {
            "type": "array",
            "description": "Social links. Always emitted, possibly empty.",
            "items": {
              "$ref": "#/components/schemas/SocialLink"
            }
          },
          "sports": {
            "type": "array",
            "description": "Disciplines the spot suits.",
            "items": {
              "type": "string",
              "description": "Discipline display name."
            }
          },
          "spot_type": {
            "type": "array",
            "description": "Skatepark, street, pumptrack…",
            "items": {
              "type": "string",
              "description": "Spot type display name."
            }
          },
          "status": {
            "type": "string",
            "description": "Publication status.",
            "examples": [
              "publish"
            ]
          },
          "title": {
            "type": "string",
            "description": "Listing name."
          },
          "type": {
            "type": "string",
            "description": "Primary spot type, as displayed."
          },
          "video_url": {
            "type": "string",
            "description": "Video URL, when a contributor filled it in."
          },
          "work_hours": {
            "type": [
              "object",
              "array",
              "null"
            ],
            "description": "Opening hours. Shape varies by listing — frozen contract."
          }
        },
        "required": [
          "ID",
          "listing_type",
          "slug",
          "title",
          "latitude",
          "longitude",
          "status"
        ]
      },
      "McpServerManifest": {
        "type": "object",
        "description": "Model Context Protocol server manifest, in the registry `server.json` format.",
        "properties": {
          "$schema": {
            "type": "string",
            "description": "URI of the server.json schema this document follows."
          },
          "description": {
            "type": "string",
            "description": "What the server does."
          },
          "name": {
            "type": "string",
            "description": "Server name in reverse-DNS form.",
            "examples": [
              "com.ridemypark/rmp"
            ]
          },
          "remotes": {
            "type": "array",
            "description": "Remote transports. RideMyPark publishes one Streamable HTTP endpoint.",
            "items": {
              "type": "object",
              "description": "A remote transport: `type` and `url`."
            }
          },
          "repository": {
            "type": "object",
            "description": "Where the server source lives."
          },
          "title": {
            "type": "string",
            "description": "Human-readable display name."
          },
          "version": {
            "type": "string",
            "description": "Version of this manifest."
          },
          "websiteUrl": {
            "type": "string",
            "description": "Where to read more about the server."
          }
        },
        "required": [
          "name",
          "description",
          "version"
        ]
      },
      "QuickSearchListing": {
        "type": "object",
        "description": "One listing in an autocomplete result.",
        "properties": {
          "ID": {
            "type": "integer",
            "description": "Listing id."
          },
          "categories": {
            "type": "array",
            "description": "Indoor / outdoor / closed.",
            "items": {
              "type": "string",
              "description": "Category slug."
            }
          },
          "formattedAddress": {
            "type": "string",
            "description": "Postal address as displayed."
          },
          "latitude": {
            "type": "string",
            "description": "Latitude in decimal degrees, as a STRING (frozen contract)."
          },
          "listing_type": {
            "type": "string",
            "description": "Universe of the listing.",
            "enum": [
              "spot",
              "event",
              "shop"
            ]
          },
          "logo": {
            "type": "array",
            "description": "Card photo.",
            "items": {
              "type": "string",
              "description": "Absolute image URL."
            }
          },
          "longitude": {
            "type": "string",
            "description": "Longitude in decimal degrees, as a STRING (frozen contract)."
          },
          "slug": {
            "type": "string",
            "description": "Permalink slug."
          },
          "spot_type": {
            "type": "array",
            "description": "Skatepark, street, pumptrack…",
            "items": {
              "type": "string",
              "description": "Spot type slug."
            }
          },
          "title": {
            "type": "string",
            "description": "Listing name."
          }
        },
        "required": [
          "ID",
          "title",
          "listing_type",
          "latitude",
          "longitude"
        ]
      },
      "QuickSearchResponse": {
        "type": "object",
        "description": "Autocomplete results.",
        "properties": {
          "listings": {
            "type": "array",
            "description": "Matching listings.",
            "items": {
              "$ref": "#/components/schemas/QuickSearchListing"
            }
          },
          "places": {
            "type": "array",
            "description": "Place suggestions. Empty when `skip_places=1`.",
            "items": {
              "type": "object",
              "description": "A place suggestion from the geocoder, passed through as received."
            }
          },
          "users": {
            "type": "array",
            "description": "Matching rider accounts. Omitted when empty or when `skip_users=1`.",
            "items": {
              "$ref": "#/components/schemas/QuickSearchUser"
            }
          }
        },
        "required": [
          "listings",
          "places"
        ]
      },
      "QuickSearchUser": {
        "type": "object",
        "description": "A rider account matching the query.",
        "properties": {
          "ID": {
            "type": "integer",
            "description": "Account id."
          },
          "avatar": {
            "type": "string",
            "description": "Absolute avatar URL, or an empty string when no picture resolves."
          },
          "display_name": {
            "type": "string",
            "description": "Name shown on the profile."
          },
          "username": {
            "type": "string",
            "description": "Handle, as it appears in `/rider/\u003chandle\u003e`."
          }
        },
        "required": [
          "ID",
          "username",
          "display_name",
          "avatar"
        ]
      },
      "RateLimitError": {
        "type": "object",
        "description": "Answered when the per-IP rate limit rejects a request. The `Retry-After` header carries the wait in seconds.",
        "properties": {
          "message": {
            "type": "string",
            "description": "Why the request was rejected."
          },
          "status": {
            "type": "string",
            "description": "Always \"error\".",
            "const": "error"
          },
          "success": {
            "type": "boolean",
            "description": "Always `false`."
          }
        },
        "required": [
          "success",
          "status",
          "message"
        ]
      },
      "ReverseGeocodeResponse": {
        "type": "object",
        "description": "The address covering a point.",
        "properties": {
          "result": {
            "$ref": "#/components/schemas/GeocodeResult",
            "description": "Null when nothing resolves."
          }
        },
        "required": [
          "result"
        ]
      },
      "Review": {
        "type": "object",
        "description": "One community review.",
        "properties": {
          "approved": {
            "type": "boolean",
            "description": "Whether moderation approved the review."
          },
          "author": {
            "type": "string",
            "description": "Author display name."
          },
          "author_id": {
            "type": "string",
            "description": "Author account id, as a STRING."
          },
          "avatar": {
            "type": "string",
            "description": "Absolute avatar URL. Always emitted, possibly empty."
          },
          "content": {
            "type": "string",
            "description": "Review body."
          },
          "date": {
            "type": "string",
            "format": "date-time",
            "description": "When the review was posted (RFC 3339)."
          },
          "gallery_images": {
            "type": "array",
            "description": "Photos attached to the review. Always emitted, possibly empty.",
            "items": {
              "$ref": "#/components/schemas/GalleryImage"
            }
          },
          "has_parent": {
            "type": "boolean",
            "description": "Whether this review answers another one."
          },
          "id": {
            "type": "string",
            "description": "Review id, as a STRING (frozen contract)."
          },
          "max_rating": {
            "type": "integer",
            "description": "Scale of `rating`.",
            "examples": [
              10
            ]
          },
          "parent_id": {
            "type": "string",
            "description": "Id of the review this one answers."
          },
          "rating": {
            "type": "number",
            "description": "Rating on the STORAGE scale, out of `max_rating`.",
            "examples": [
              8
            ]
          }
        },
        "required": [
          "id",
          "author_id",
          "author",
          "date",
          "content",
          "rating",
          "avatar",
          "gallery_images",
          "max_rating",
          "has_parent",
          "approved"
        ]
      },
      "SearchMarker": {
        "type": "object",
        "description": "One listing in a search result page.",
        "properties": {
          "ID": {
            "type": "integer",
            "description": "Listing id.",
            "examples": [
              43249
            ]
          },
          "average_rating": {
            "type": "number",
            "description": "Community rating on the STORAGE scale 0-10. Omitted when the listing has no rating — never rendered as 0.",
            "examples": [
              7.5
            ]
          },
          "categories": {
            "type": "array",
            "description": "Indoor / outdoor / closed.",
            "items": {
              "type": "string",
              "description": "Category slug."
            }
          },
          "elements": {
            "type": "array",
            "description": "Modules on the spot: rails, ledges, bowls…",
            "items": {
              "type": "string",
              "description": "Module slug."
            }
          },
          "formattedAddress": {
            "type": "string",
            "description": "Postal address as displayed."
          },
          "latitude": {
            "type": "string",
            "description": "Latitude in decimal degrees, as a STRING (frozen contract)."
          },
          "listing_type": {
            "type": "string",
            "description": "Universe of the listing.",
            "enum": [
              "spot",
              "event",
              "shop"
            ]
          },
          "logo": {
            "type": "array",
            "description": "Card photo, wrapped in an array for parity with the explore endpoint.",
            "items": {
              "type": "string",
              "description": "Absolute image URL."
            }
          },
          "longitude": {
            "type": "string",
            "description": "Longitude in decimal degrees, as a STRING (frozen contract)."
          },
          "rating_count": {
            "type": "integer",
            "description": "Number of reviews behind `average_rating`."
          },
          "slug": {
            "type": "string",
            "description": "Permalink slug. Build the page URL as `/spots/\u003cslug\u003e`.",
            "examples": [
              "barbes-la-chapelle-skatepark"
            ]
          },
          "spot_type": {
            "type": "array",
            "description": "Skatepark, street, pumptrack…",
            "items": {
              "type": "string",
              "description": "Spot type slug."
            }
          },
          "title": {
            "type": "string",
            "description": "Listing name.",
            "examples": [
              "Barbès - La Chapelle skatepark"
            ]
          },
          "work_hours": {
            "type": [
              "object",
              "array",
              "null"
            ],
            "description": "Opening hours. Shape varies by listing (map, array or null) — frozen contract."
          }
        },
        "required": [
          "ID",
          "title",
          "latitude",
          "longitude"
        ]
      },
      "SearchResponse": {
        "type": "object",
        "description": "A page of search results.",
        "properties": {
          "data": {
            "type": "array",
            "description": "The listings on this page.",
            "items": {
              "$ref": "#/components/schemas/SearchMarker"
            }
          },
          "numbers": {
            "type": "integer",
            "description": "Number of entries in `data`."
          },
          "page": {
            "type": "integer",
            "description": "Current page, 1-based."
          },
          "pages": {
            "type": "integer",
            "description": "Total number of pages."
          },
          "success": {
            "type": "boolean",
            "description": "`false` means the search failed; `data` is then absent or empty."
          },
          "totalCount": {
            "type": "integer",
            "description": "Total number of listings matching the filters, across all pages."
          }
        },
        "required": [
          "success",
          "numbers",
          "totalCount",
          "page",
          "pages",
          "data"
        ]
      },
      "SocialLink": {
        "type": "object",
        "description": "A social profile linked from a listing.",
        "properties": {
          "name": {
            "type": "string",
            "description": "Network name."
          },
          "url": {
            "type": "string",
            "description": "Absolute profile URL."
          }
        }
      },
      "ZoneBoundingBox": {
        "type": "object",
        "description": "Envelope of a zone. Coordinates are STRINGS (frozen contract).",
        "properties": {
          "maxLat": {
            "type": "string",
            "description": "North edge."
          },
          "maxLng": {
            "type": "string",
            "description": "East edge."
          },
          "minLat": {
            "type": "string",
            "description": "South edge."
          },
          "minLng": {
            "type": "string",
            "description": "West edge."
          }
        },
        "required": [
          "minLat",
          "minLng",
          "maxLat",
          "maxLng"
        ]
      },
      "ZoneCenter": {
        "type": "object",
        "description": "Centre of a zone. Coordinates are STRINGS (frozen contract).",
        "properties": {
          "latitude": {
            "type": "string",
            "description": "Latitude in decimal degrees."
          },
          "longitude": {
            "type": "string",
            "description": "Longitude in decimal degrees."
          }
        },
        "required": [
          "latitude",
          "longitude"
        ]
      }
    }
  }
}
