{
  "openapi": "3.1.0",
  "info": {
    "title": "TheBoatDB API",
    "version": "1.0.0",
    "summary": "Boat and manufacturer reference data for partner integrations.",
    "description": "A read-only REST API over TheBoatDB's catalogue of boats and manufacturers.\n\nEvery catalogue endpoint requires an API key issued from the dashboard, which requires an active **Pro subscription**. Send it as `Authorization: Bearer <key>`; a credential in the query string is refused outright, because a URL is logged and cached in places we cannot scrub. The two endpoints under **Service** need no key.\n\nResponses are versioned under `/v1` and shaped by us, not by our CMS — see the [versioning and deprecation policy](https://theboatdb.com/docs/api/versioning) for what that guarantees and how much notice a breaking change gets.\n\n- [Getting started](https://theboatdb.com/docs/api)\n- [Authentication](https://theboatdb.com/docs/api/authentication)\n- [Conventions](https://theboatdb.com/docs/api/conventions) — envelope, pagination, sorting\n- [Errors](https://theboatdb.com/docs/api/errors)\n- [Rate limits](https://theboatdb.com/docs/api/rate-limits)",
    "contact": {
      "name": "TheBoatApp support",
      "email": "support@theboatapp.com",
      "url": "https://theboatdb.com/docs/api"
    }
  },
  "servers": [
    {
      "url": "https://api.theboatdb.com"
    }
  ],
  "tags": [
    {
      "name": "Boats",
      "description": "The catalogue of boat models. Requires the `boats.read` scope."
    },
    {
      "name": "Manufacturers",
      "description": "Builders, and the boats attributed to them. Requires the `manufacturers.read` scope."
    },
    {
      "name": "Service",
      "description": "Unauthenticated endpoints for checking reachability. Neither counts against a rate limit."
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "paths": {
    "/v1/boats": {
      "get": {
        "operationId": "listBoats",
        "summary": "List boats",
        "description": "A page of boat summaries, filtered and sorted. Length filters are unit-aware: your bounds are expressed in `lengthUnit` and compared against each boat in its own declared system.\n\n**Scopes:** `boats.read`.",
        "tags": [
          "Boats"
        ],
        "parameters": [
          {
            "name": "manufacturer",
            "in": "query",
            "required": false,
            "description": "Manufacturer slug, e.g. `albin-marine`.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 120,
              "pattern": "^[a-zA-Z0-9][a-zA-Z0-9_-]*$"
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Boat type slug, e.g. `sail`.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 120,
              "pattern": "^[a-zA-Z0-9][a-zA-Z0-9_-]*$"
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Category slug, e.g. `cruiser-racer-boats`.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 120,
              "pattern": "^[a-zA-Z0-9][a-zA-Z0-9_-]*$"
            }
          },
          {
            "name": "activity",
            "in": "query",
            "required": false,
            "description": "Activity slug, e.g. `day_cruising`.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 120,
              "pattern": "^[a-zA-Z0-9][a-zA-Z0-9_-]*$"
            }
          },
          {
            "name": "yearFrom",
            "in": "query",
            "required": false,
            "description": "Production-window overlap, not a filter on the first year: a boat built 1971–1982 matches `1975`.",
            "schema": {
              "type": "integer",
              "minimum": 1800,
              "maximum": 2100
            }
          },
          {
            "name": "yearTo",
            "in": "query",
            "required": false,
            "description": "Upper bound of the production window. Must be >= `yearFrom`.",
            "schema": {
              "type": "integer",
              "minimum": 1800,
              "maximum": 2100
            }
          },
          {
            "name": "lengthMin",
            "in": "query",
            "required": false,
            "description": "Minimum length overall, expressed in `lengthUnit`.",
            "schema": {
              "type": "number",
              "exclusiveMinimum": 0,
              "maximum": 1000
            }
          },
          {
            "name": "lengthMax",
            "in": "query",
            "required": false,
            "description": "Maximum length overall, expressed in `lengthUnit`. Must be >= `lengthMin`.",
            "schema": {
              "type": "number",
              "exclusiveMinimum": 0,
              "maximum": 1000
            }
          },
          {
            "name": "lengthUnit",
            "in": "query",
            "required": false,
            "description": "Unit **your** length bounds are in. Does not filter on, or change, the units in the response.",
            "schema": {
              "type": "string",
              "enum": [
                "m",
                "ft"
              ],
              "default": "m"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Term-prefix match on the model name.",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 100
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows per page, up to 100.",
            "schema": {
              "type": "integer",
              "exclusiveMinimum": 0,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip before the page. Page by advancing this until you have seen `pagination.total` rows.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Field to sort on. Any other value is a 400.",
            "schema": {
              "type": "string",
              "enum": [
                "model",
                "year",
                "length",
                "manufacturer"
              ]
            }
          },
          {
            "name": "order",
            "in": "query",
            "required": false,
            "description": "Sort direction.",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            }
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "description": "An `ETag` from a previous response. Matching it answers 304 with no body, and does not count against your rate limit any differently.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              },
              "X-Cache": {
                "$ref": "#/components/headers/X-Cache"
              },
              "Age": {
                "$ref": "#/components/headers/Age"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BoatListResponse"
                },
                "example": {
                  "data": [
                    {
                      "id": "a3f9c1a2-6d41-4f0e-9d0b-2c7b8f1e5a10",
                      "slug": "albin-vega-27",
                      "model": "Vega 27",
                      "version": null,
                      "manufacturer": {
                        "id": "71b0c3d4-2e88-4a13-9f5c-6b2a0d9e4c31",
                        "name": "Albin Marine",
                        "slug": "albin-marine",
                        "website": "https://albinmarine.example"
                      },
                      "type": {
                        "id": "c1d2e3f4-1111-4222-8333-444455556666",
                        "name": "Sail",
                        "slug": "sail"
                      },
                      "production": {
                        "firstYear": 1966,
                        "lastYear": 1979
                      },
                      "unit": "metric",
                      "lengthOverall": 8.25,
                      "lengthOverallMetres": 8.25,
                      "referencePrice": {
                        "amount": 12000,
                        "currency": {
                          "code": "EUR",
                          "symbol": "€"
                        }
                      },
                      "image": {
                        "url": "https://cdn.sanity.io/images/theboatdb/production/9f3c1a-1600x1067.jpg",
                        "alt": "Albin Vega 27 under sail",
                        "width": 1600,
                        "height": 1067,
                        "format": "jpg"
                      }
                    }
                  ],
                  "pagination": {
                    "total": 1482,
                    "limit": 20,
                    "offset": 0
                  }
                }
              }
            }
          },
          "304": {
            "description": "Your `If-None-Match` matched the current body. No payload is sent.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              }
            }
          },
          "400": {
            "description": "A parameter is malformed, out of range, or not one we recognise. Unknown parameters are rejected rather than ignored — fix the request; do not retry it unchanged.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "VALIDATION_ERROR",
                    "message": "Invalid value for \"limit\": Number must be less than or equal to 100",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq",
                    "details": {
                      "field": "limit",
                      "issues": [
                        {
                          "field": "limit",
                          "message": "Number must be less than or equal to 100"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No API key, a malformed `Authorization` header, or a key that is unknown, revoked, disabled, expired or issued for the other environment. A credential in the query string is refused here too.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "MISSING_API_KEY",
                    "message": "Provide your API key as `Authorization: Bearer <key>`.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key is valid but lacks a scope this endpoint requires. Retrying, or rotating the key, will not help.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "INSUFFICIENT_PERMISSIONS",
                    "message": "This API key is missing the required permission `boats.read`.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — branch on `error.code` to tell which limit you met.\n\n`RATE_LIMITED` is the per-minute rate limit on this key: wait for `Retry-After` seconds, and add jitter if several workers share the key.\n\n`QUOTA_EXCEEDED` is your subscription’s monthly request quota, shared by every key you hold. Retrying will not help until it renews, which it does automatically — `X-Quota-Reset` says when.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "RATE_LIMITED",
                    "message": "Rate limit exceeded for this API key.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure on our side. Retry with backoff and report the `requestId` if it persists.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "INTERNAL_ERROR",
                    "message": "Something went wrong.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "503": {
            "description": "A dependency we refuse to guess about was unreachable. The request was rejected, not served stale — retry after `Retry-After`.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "SERVICE_UNAVAILABLE",
                    "message": "Unable to verify API keys right now. Please retry shortly.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/boats/{idOrSlug}": {
      "get": {
        "operationId": "getBoat",
        "summary": "Get a boat",
        "description": "One boat in full, addressable by slug or document id. Takes no query parameters — `?fields=model` is a 400, not a partial response.\n\n**Scopes:** `boats.read`.",
        "tags": [
          "Boats"
        ],
        "parameters": [
          {
            "name": "idOrSlug",
            "in": "path",
            "required": true,
            "description": "The boat’s slug (`albin-vega-27`) or its document id.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200,
              "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]*$"
            }
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "description": "An `ETag` from a previous response. Matching it answers 304 with no body, and does not count against your rate limit any differently.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              },
              "X-Cache": {
                "$ref": "#/components/headers/X-Cache"
              },
              "Age": {
                "$ref": "#/components/headers/Age"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BoatResponse"
                },
                "example": {
                  "data": {
                    "id": "a3f9c1a2-6d41-4f0e-9d0b-2c7b8f1e5a10",
                    "slug": "albin-vega-27",
                    "model": "Vega 27",
                    "version": null,
                    "manufacturer": {
                      "id": "71b0c3d4-2e88-4a13-9f5c-6b2a0d9e4c31",
                      "name": "Albin Marine",
                      "slug": "albin-marine",
                      "website": "https://albinmarine.example"
                    },
                    "type": {
                      "id": "c1d2e3f4-1111-4222-8333-444455556666",
                      "name": "Sail",
                      "slug": "sail"
                    },
                    "production": {
                      "firstYear": 1966,
                      "lastYear": 1979
                    },
                    "unit": "metric",
                    "lengthOverall": 8.25,
                    "lengthOverallMetres": 8.25,
                    "referencePrice": {
                      "amount": 12000,
                      "currency": {
                        "code": "EUR",
                        "symbol": "€"
                      }
                    },
                    "image": {
                      "url": "https://cdn.sanity.io/images/theboatdb/production/9f3c1a-1600x1067.jpg",
                      "alt": "Albin Vega 27 under sail",
                      "width": 1600,
                      "height": 1067,
                      "format": "jpg"
                    },
                    "description": "A Swedish long-keel cruiser designed by Per Brohäll, produced from 1966 to 1979 and one of the most widely built small offshore yachts of its era.",
                    "categories": [
                      {
                        "id": "d4e5f6a7-2222-4333-8444-555566667777",
                        "name": "Cruiser",
                        "slug": "cruiser-boats"
                      }
                    ],
                    "activities": [
                      {
                        "id": "e5f6a7b8-3333-4444-8555-666677778888",
                        "name": "Day cruising",
                        "slug": "day_cruising"
                      }
                    ],
                    "hull": {
                      "type": {
                        "id": "f6a7b8c9-4444-4555-8666-777788889999",
                        "name": "Monohull",
                        "slug": "monohull"
                      },
                      "material": {
                        "id": "a7b8c9d0-5555-4666-8777-888899990000",
                        "name": "GRP",
                        "slug": "grp"
                      },
                      "keelType": "Long keel",
                      "unsinkable": false,
                      "trailerable": false,
                      "appendages": [
                        {
                          "name": "Rudder",
                          "value": "Transom hung"
                        }
                      ]
                    },
                    "dimensions": {
                      "lengthOverall": 8.25,
                      "lengthWaterline": 7,
                      "beamOverall": 2.46,
                      "beamCabinLine": null,
                      "beamHull": 2.46,
                      "draftMax": 1.17,
                      "draftMin": null,
                      "headroom": 1.75,
                      "bridgedeckClearance": null,
                      "nominalBridgedeckClearance": null,
                      "displacement": 2100,
                      "ballastWeight": 900,
                      "ballastType": "Iron",
                      "tonnageGross": null,
                      "tonnageNet": null
                    },
                    "accommodation": {
                      "cabins": 1,
                      "berths": 4,
                      "heads": 1
                    },
                    "steering": {
                      "helm": "Tiller",
                      "rudder": "Transom hung",
                      "cockpitType": "Aft",
                      "cockpitLocation": "Aft"
                    },
                    "performance": {
                      "cruisingSpeed": null,
                      "topSpeed": null,
                      "displacementHullSpeed": 6.5
                    },
                    "rig": {
                      "mastHeight": 10.7,
                      "mastConfiguration": "Masthead sloop",
                      "riggingType": "Sloop",
                      "standingRigging": "Stainless steel wire",
                      "spars": "Aluminium",
                      "rotatingSpars": false,
                      "spreadersLevel": 1,
                      "spreadersAngle": null
                    },
                    "sails": {
                      "upwindArea": 33.1,
                      "downwindArea": null,
                      "mainsail": [
                        {
                          "name": "Area",
                          "value": "13.4 m²"
                        }
                      ],
                      "headsail": [
                        {
                          "name": "Area",
                          "value": "19.7 m²"
                        }
                      ],
                      "spinnaker": []
                    },
                    "tanks": {
                      "fuel": [
                        "30 l"
                      ],
                      "water": [
                        "60 l"
                      ],
                      "holding": [],
                      "waterHeaters": []
                    },
                    "engines": [
                      {
                        "maker": "Volvo Penta",
                        "model": "MD6A",
                        "type": "Inboard",
                        "drive": "Shaft",
                        "fuel": "Diesel",
                        "power": "10",
                        "powerType": "hp"
                      }
                    ],
                    "designers": {
                      "naval": [
                        {
                          "id": "b8c9d0e1-6666-4777-8888-999900001111",
                          "name": "Per Brohäll",
                          "website": null
                        }
                      ],
                      "interior": [],
                      "exterior": []
                    },
                    "ceClassification": {
                      "a": 4,
                      "b": null,
                      "c": null,
                      "d": null
                    },
                    "gallery": [
                      {
                        "url": "https://cdn.sanity.io/images/theboatdb/production/9f3c1a-1600x1067.jpg",
                        "alt": "Albin Vega 27 under sail",
                        "width": 1600,
                        "height": 1067,
                        "format": "jpg"
                      }
                    ]
                  }
                }
              }
            }
          },
          "304": {
            "description": "Your `If-None-Match` matched the current body. No payload is sent.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              }
            }
          },
          "400": {
            "description": "A parameter is malformed, out of range, or not one we recognise. Unknown parameters are rejected rather than ignored — fix the request; do not retry it unchanged.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "VALIDATION_ERROR",
                    "message": "Invalid value for \"limit\": Number must be less than or equal to 100",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq",
                    "details": {
                      "field": "limit",
                      "issues": [
                        {
                          "field": "limit",
                          "message": "Number must be less than or equal to 100"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No API key, a malformed `Authorization` header, or a key that is unknown, revoked, disabled, expired or issued for the other environment. A credential in the query string is refused here too.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "MISSING_API_KEY",
                    "message": "Provide your API key as `Authorization: Bearer <key>`.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key is valid but lacks a scope this endpoint requires. Retrying, or rotating the key, will not help.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "INSUFFICIENT_PERMISSIONS",
                    "message": "This API key is missing the required permission `boats.read`.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "404": {
            "description": "No boat with that id or slug is published. Unpublished and non-existent are deliberately indistinguishable.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "NOT_FOUND",
                    "message": "No boat found for \"albin-vega-72\".",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — branch on `error.code` to tell which limit you met.\n\n`RATE_LIMITED` is the per-minute rate limit on this key: wait for `Retry-After` seconds, and add jitter if several workers share the key.\n\n`QUOTA_EXCEEDED` is your subscription’s monthly request quota, shared by every key you hold. Retrying will not help until it renews, which it does automatically — `X-Quota-Reset` says when.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "RATE_LIMITED",
                    "message": "Rate limit exceeded for this API key.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure on our side. Retry with backoff and report the `requestId` if it persists.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "INTERNAL_ERROR",
                    "message": "Something went wrong.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "503": {
            "description": "A dependency we refuse to guess about was unreachable. The request was rejected, not served stale — retry after `Retry-After`.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "SERVICE_UNAVAILABLE",
                    "message": "Unable to verify API keys right now. Please retry shortly.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/manufacturers": {
      "get": {
        "operationId": "listManufacturers",
        "summary": "List manufacturers",
        "description": "A page of manufacturer summaries.\n\n**Scopes:** `manufacturers.read`.",
        "tags": [
          "Manufacturers"
        ],
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Term-prefix match on the manufacturer name.",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 100
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows per page, up to 100.",
            "schema": {
              "type": "integer",
              "exclusiveMinimum": 0,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip before the page. Page by advancing this until you have seen `pagination.total` rows.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Field to sort on. Any other value is a 400.",
            "schema": {
              "type": "string",
              "enum": [
                "name",
                "updatedAt"
              ]
            }
          },
          {
            "name": "order",
            "in": "query",
            "required": false,
            "description": "Sort direction.",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            }
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "description": "An `ETag` from a previous response. Matching it answers 304 with no body, and does not count against your rate limit any differently.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              },
              "X-Cache": {
                "$ref": "#/components/headers/X-Cache"
              },
              "Age": {
                "$ref": "#/components/headers/Age"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManufacturerListResponse"
                },
                "example": {
                  "data": [
                    {
                      "id": "71b0c3d4-2e88-4a13-9f5c-6b2a0d9e4c31",
                      "slug": "albin-marine",
                      "name": "Albin Marine",
                      "website": "https://albinmarine.example",
                      "logo": null,
                      "updatedAt": "2026-01-08T11:02:47Z"
                    }
                  ],
                  "pagination": {
                    "total": 213,
                    "limit": 20,
                    "offset": 0
                  }
                }
              }
            }
          },
          "304": {
            "description": "Your `If-None-Match` matched the current body. No payload is sent.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              }
            }
          },
          "400": {
            "description": "A parameter is malformed, out of range, or not one we recognise. Unknown parameters are rejected rather than ignored — fix the request; do not retry it unchanged.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "VALIDATION_ERROR",
                    "message": "Invalid value for \"limit\": Number must be less than or equal to 100",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq",
                    "details": {
                      "field": "limit",
                      "issues": [
                        {
                          "field": "limit",
                          "message": "Number must be less than or equal to 100"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No API key, a malformed `Authorization` header, or a key that is unknown, revoked, disabled, expired or issued for the other environment. A credential in the query string is refused here too.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "MISSING_API_KEY",
                    "message": "Provide your API key as `Authorization: Bearer <key>`.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key is valid but lacks a scope this endpoint requires. Retrying, or rotating the key, will not help.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "INSUFFICIENT_PERMISSIONS",
                    "message": "This API key is missing the required permission `boats.read`.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — branch on `error.code` to tell which limit you met.\n\n`RATE_LIMITED` is the per-minute rate limit on this key: wait for `Retry-After` seconds, and add jitter if several workers share the key.\n\n`QUOTA_EXCEEDED` is your subscription’s monthly request quota, shared by every key you hold. Retrying will not help until it renews, which it does automatically — `X-Quota-Reset` says when.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "RATE_LIMITED",
                    "message": "Rate limit exceeded for this API key.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure on our side. Retry with backoff and report the `requestId` if it persists.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "INTERNAL_ERROR",
                    "message": "Something went wrong.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "503": {
            "description": "A dependency we refuse to guess about was unreachable. The request was rejected, not served stale — retry after `Retry-After`.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "SERVICE_UNAVAILABLE",
                    "message": "Unable to verify API keys right now. Please retry shortly.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/manufacturers/{idOrSlug}": {
      "get": {
        "operationId": "getManufacturer",
        "summary": "Get a manufacturer",
        "description": "One manufacturer, addressable by slug or document id, with a count of the boats attributed to it. Takes no query parameters.\n\n**Scopes:** `manufacturers.read`.",
        "tags": [
          "Manufacturers"
        ],
        "parameters": [
          {
            "name": "idOrSlug",
            "in": "path",
            "required": true,
            "description": "The manufacturer’s slug (`albin-marine`) or its document id.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200,
              "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]*$"
            }
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "description": "An `ETag` from a previous response. Matching it answers 304 with no body, and does not count against your rate limit any differently.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              },
              "X-Cache": {
                "$ref": "#/components/headers/X-Cache"
              },
              "Age": {
                "$ref": "#/components/headers/Age"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManufacturerResponse"
                },
                "example": {
                  "data": {
                    "id": "71b0c3d4-2e88-4a13-9f5c-6b2a0d9e4c31",
                    "slug": "albin-marine",
                    "name": "Albin Marine",
                    "website": "https://albinmarine.example",
                    "logo": null,
                    "updatedAt": "2026-01-08T11:02:47Z",
                    "description": "Swedish builder of small cruising yachts and motorboats, founded in Kristinehamn.",
                    "boatCount": 34
                  }
                }
              }
            }
          },
          "304": {
            "description": "Your `If-None-Match` matched the current body. No payload is sent.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              }
            }
          },
          "400": {
            "description": "A parameter is malformed, out of range, or not one we recognise. Unknown parameters are rejected rather than ignored — fix the request; do not retry it unchanged.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "VALIDATION_ERROR",
                    "message": "Invalid value for \"limit\": Number must be less than or equal to 100",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq",
                    "details": {
                      "field": "limit",
                      "issues": [
                        {
                          "field": "limit",
                          "message": "Number must be less than or equal to 100"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No API key, a malformed `Authorization` header, or a key that is unknown, revoked, disabled, expired or issued for the other environment. A credential in the query string is refused here too.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "MISSING_API_KEY",
                    "message": "Provide your API key as `Authorization: Bearer <key>`.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key is valid but lacks a scope this endpoint requires. Retrying, or rotating the key, will not help.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "INSUFFICIENT_PERMISSIONS",
                    "message": "This API key is missing the required permission `boats.read`.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "404": {
            "description": "No manufacturer with that id or slug is published.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "NOT_FOUND",
                    "message": "No boat found for \"albin-vega-72\".",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — branch on `error.code` to tell which limit you met.\n\n`RATE_LIMITED` is the per-minute rate limit on this key: wait for `Retry-After` seconds, and add jitter if several workers share the key.\n\n`QUOTA_EXCEEDED` is your subscription’s monthly request quota, shared by every key you hold. Retrying will not help until it renews, which it does automatically — `X-Quota-Reset` says when.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "RATE_LIMITED",
                    "message": "Rate limit exceeded for this API key.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure on our side. Retry with backoff and report the `requestId` if it persists.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "INTERNAL_ERROR",
                    "message": "Something went wrong.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "503": {
            "description": "A dependency we refuse to guess about was unreachable. The request was rejected, not served stale — retry after `Retry-After`.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "SERVICE_UNAVAILABLE",
                    "message": "Unable to verify API keys right now. Please retry shortly.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/manufacturers/{idOrSlug}/boats": {
      "get": {
        "operationId": "listManufacturerBoats",
        "summary": "List a manufacturer’s boats",
        "description": "The same page shape and filters as `/v1/boats`, minus `manufacturer` — the path already names it. Requires **both** scopes: a key holding only `manufacturers.read` cannot reach boat data through this path.\n\nAn unknown manufacturer is a 404 here rather than an empty page, which would read as \"this manufacturer builds nothing\".\n\n**Scopes:** `manufacturers.read`, `boats.read`.",
        "tags": [
          "Manufacturers"
        ],
        "parameters": [
          {
            "name": "idOrSlug",
            "in": "path",
            "required": true,
            "description": "The manufacturer’s slug (`albin-marine`) or its document id.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200,
              "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]*$"
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Boat type slug, e.g. `sail`.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 120,
              "pattern": "^[a-zA-Z0-9][a-zA-Z0-9_-]*$"
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Category slug, e.g. `cruiser-racer-boats`.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 120,
              "pattern": "^[a-zA-Z0-9][a-zA-Z0-9_-]*$"
            }
          },
          {
            "name": "activity",
            "in": "query",
            "required": false,
            "description": "Activity slug, e.g. `day_cruising`.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 120,
              "pattern": "^[a-zA-Z0-9][a-zA-Z0-9_-]*$"
            }
          },
          {
            "name": "yearFrom",
            "in": "query",
            "required": false,
            "description": "Production-window overlap, not a filter on the first year: a boat built 1971–1982 matches `1975`.",
            "schema": {
              "type": "integer",
              "minimum": 1800,
              "maximum": 2100
            }
          },
          {
            "name": "yearTo",
            "in": "query",
            "required": false,
            "description": "Upper bound of the production window. Must be >= `yearFrom`.",
            "schema": {
              "type": "integer",
              "minimum": 1800,
              "maximum": 2100
            }
          },
          {
            "name": "lengthMin",
            "in": "query",
            "required": false,
            "description": "Minimum length overall, expressed in `lengthUnit`.",
            "schema": {
              "type": "number",
              "exclusiveMinimum": 0,
              "maximum": 1000
            }
          },
          {
            "name": "lengthMax",
            "in": "query",
            "required": false,
            "description": "Maximum length overall, expressed in `lengthUnit`. Must be >= `lengthMin`.",
            "schema": {
              "type": "number",
              "exclusiveMinimum": 0,
              "maximum": 1000
            }
          },
          {
            "name": "lengthUnit",
            "in": "query",
            "required": false,
            "description": "Unit **your** length bounds are in. Does not filter on, or change, the units in the response.",
            "schema": {
              "type": "string",
              "enum": [
                "m",
                "ft"
              ],
              "default": "m"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Term-prefix match on the model name.",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 100
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows per page, up to 100.",
            "schema": {
              "type": "integer",
              "exclusiveMinimum": 0,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip before the page. Page by advancing this until you have seen `pagination.total` rows.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Field to sort on. Any other value is a 400.",
            "schema": {
              "type": "string",
              "enum": [
                "model",
                "year",
                "length",
                "manufacturer"
              ]
            }
          },
          {
            "name": "order",
            "in": "query",
            "required": false,
            "description": "Sort direction.",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            }
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "description": "An `ETag` from a previous response. Matching it answers 304 with no body, and does not count against your rate limit any differently.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              },
              "X-Cache": {
                "$ref": "#/components/headers/X-Cache"
              },
              "Age": {
                "$ref": "#/components/headers/Age"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BoatListResponse"
                },
                "example": {
                  "data": [
                    {
                      "id": "a3f9c1a2-6d41-4f0e-9d0b-2c7b8f1e5a10",
                      "slug": "albin-vega-27",
                      "model": "Vega 27",
                      "version": null,
                      "manufacturer": {
                        "id": "71b0c3d4-2e88-4a13-9f5c-6b2a0d9e4c31",
                        "name": "Albin Marine",
                        "slug": "albin-marine",
                        "website": "https://albinmarine.example"
                      },
                      "type": {
                        "id": "c1d2e3f4-1111-4222-8333-444455556666",
                        "name": "Sail",
                        "slug": "sail"
                      },
                      "production": {
                        "firstYear": 1966,
                        "lastYear": 1979
                      },
                      "unit": "metric",
                      "lengthOverall": 8.25,
                      "lengthOverallMetres": 8.25,
                      "referencePrice": {
                        "amount": 12000,
                        "currency": {
                          "code": "EUR",
                          "symbol": "€"
                        }
                      },
                      "image": {
                        "url": "https://cdn.sanity.io/images/theboatdb/production/9f3c1a-1600x1067.jpg",
                        "alt": "Albin Vega 27 under sail",
                        "width": 1600,
                        "height": 1067,
                        "format": "jpg"
                      }
                    }
                  ],
                  "pagination": {
                    "total": 1482,
                    "limit": 20,
                    "offset": 0
                  }
                }
              }
            }
          },
          "304": {
            "description": "Your `If-None-Match` matched the current body. No payload is sent.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              }
            }
          },
          "400": {
            "description": "A parameter is malformed, out of range, or not one we recognise. Unknown parameters are rejected rather than ignored — fix the request; do not retry it unchanged.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "VALIDATION_ERROR",
                    "message": "Invalid value for \"limit\": Number must be less than or equal to 100",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq",
                    "details": {
                      "field": "limit",
                      "issues": [
                        {
                          "field": "limit",
                          "message": "Number must be less than or equal to 100"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No API key, a malformed `Authorization` header, or a key that is unknown, revoked, disabled, expired or issued for the other environment. A credential in the query string is refused here too.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "MISSING_API_KEY",
                    "message": "Provide your API key as `Authorization: Bearer <key>`.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "403": {
            "description": "The key is valid but lacks a scope this endpoint requires. Retrying, or rotating the key, will not help.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "INSUFFICIENT_PERMISSIONS",
                    "message": "This API key is missing the required permission `boats.read`.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "404": {
            "description": "No manufacturer with that id or slug is published.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "NOT_FOUND",
                    "message": "No boat found for \"albin-vega-72\".",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many requests — branch on `error.code` to tell which limit you met.\n\n`RATE_LIMITED` is the per-minute rate limit on this key: wait for `Retry-After` seconds, and add jitter if several workers share the key.\n\n`QUOTA_EXCEEDED` is your subscription’s monthly request quota, shared by every key you hold. Retrying will not help until it renews, which it does automatically — `X-Quota-Reset` says when.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "RATE_LIMITED",
                    "message": "Rate limit exceeded for this API key.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure on our side. Retry with backoff and report the `requestId` if it persists.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "INTERNAL_ERROR",
                    "message": "Something went wrong.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          },
          "503": {
            "description": "A dependency we refuse to guess about was unreachable. The request was rejected, not served stale — retry after `Retry-After`.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "SERVICE_UNAVAILABLE",
                    "message": "Unable to verify API keys right now. Please retry shortly.",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1": {
      "get": {
        "operationId": "getVersion",
        "summary": "Version descriptor",
        "description": "Unauthenticated. Use it to separate \"the network and the service are fine\" from \"my key is not\" — if this responds and your authenticated calls do not, the problem is the key.",
        "tags": [
          "Service"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VersionDescriptor"
                },
                "example": {
                  "name": "theboatdb-api",
                  "apiVersion": "v1",
                  "status": "ok"
                }
              }
            }
          },
          "400": {
            "description": "An unknown query parameter was sent. This endpoint accepts none.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "VALIDATION_ERROR",
                    "message": "Invalid value for \"limit\": Number must be less than or equal to 100",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq",
                    "details": {
                      "field": "limit",
                      "issues": [
                        {
                          "field": "limit",
                          "message": "Number must be less than or equal to 100"
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Liveness probe",
        "description": "Unauthenticated uptime check for this instance. It does not call Sanity, Unkey or Stripe, so a 200 here does not promise that a catalogue read will succeed.",
        "tags": [
          "Service"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthStatus"
                },
                "example": {
                  "status": "ok",
                  "uptimeSeconds": 84213,
                  "timestamp": "2026-08-11T14:22:03Z"
                }
              }
            }
          },
          "400": {
            "description": "An unknown query parameter was sent. This endpoint accepts none.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "VALIDATION_ERROR",
                    "message": "Invalid value for \"limit\": Number must be less than or equal to 100",
                    "requestId": "01JD8K2M4Q7X9Vb3Rk6TzP2ANq",
                    "details": {
                      "field": "limit",
                      "issues": [
                        {
                          "field": "limit",
                          "message": "Number must be less than or equal to 100"
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Your API key, as `Authorization: Bearer <key>`. Keys are prefixed `tbdb_live_` in production and `tbdb_test_` in staging; the two environments are separate, and a key from one is rejected by the other."
      }
    },
    "headers": {
      "X-Request-Id": {
        "description": "Identifier for this request. Quote it in a support request — it is how we find the request in our logs.",
        "schema": {
          "type": "string"
        }
      },
      "X-RateLimit-Limit": {
        "description": "Requests allowed in the current window for this key.",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Remaining": {
        "description": "Requests left in the current window.",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Reset": {
        "description": "Unix timestamp (seconds) at which the window resets.",
        "schema": {
          "type": "integer"
        }
      },
      "X-Quota-Limit": {
        "description": "Requests your subscription includes each month. Shared across every API key you hold — a second key does not raise it.",
        "schema": {
          "type": "integer"
        }
      },
      "X-Quota-Remaining": {
        "description": "Requests left in the current monthly quota period, across all of your keys.",
        "schema": {
          "type": "integer"
        }
      },
      "X-Quota-Reset": {
        "description": "Unix timestamp (seconds) at which the quota renews. Renewal is automatic.",
        "schema": {
          "type": "integer"
        }
      },
      "Retry-After": {
        "description": "Seconds to wait before retrying. Honour this rather than choosing your own interval.",
        "schema": {
          "type": "integer"
        }
      },
      "ETag": {
        "description": "Strong validator for this body. Send it back as `If-None-Match` to get a 304 instead of the payload.",
        "schema": {
          "type": "string"
        }
      },
      "Cache-Control": {
        "description": "`private`, with the resource’s max-age. Cache it in your own process if you like; a shared proxy must not.",
        "schema": {
          "type": "string"
        }
      },
      "X-Cache": {
        "description": "Whether this body came from our response cache: `HIT`, `MISS`, or `BYPASS` when caching is disabled.",
        "schema": {
          "type": "string",
          "enum": [
            "HIT",
            "MISS",
            "BYPASS"
          ]
        }
      },
      "Age": {
        "description": "Seconds the cached body has been held. Present on a `HIT`.",
        "schema": {
          "type": "integer"
        }
      }
    },
    "schemas": {
      "Image": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Absolute CDN URL. Stable for the lifetime of the asset."
          },
          "alt": {
            "type": [
              "string",
              "null"
            ],
            "description": "Alternative text, where an editor has provided one."
          },
          "width": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "description": "Intrinsic width in pixels."
          },
          "height": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "description": "Intrinsic height in pixels."
          },
          "format": {
            "type": [
              "string",
              "null"
            ],
            "description": "File format, e.g. `jpg`, `png`, `webp`."
          }
        },
        "required": [
          "url",
          "alt",
          "width",
          "height",
          "format"
        ],
        "description": "An image asset."
      },
      "Reference": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable document id."
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string",
            "description": "The value to pass to the matching filter, e.g. `?type=sail`."
          }
        },
        "required": [
          "id",
          "name",
          "slug"
        ],
        "description": "A reference to another catalogue entity."
      },
      "NameValue": {
        "type": "object",
        "properties": {
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "value": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "name",
          "value"
        ]
      },
      "ManufacturerRef": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable document id."
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string",
            "description": "The value to pass to the matching filter, e.g. `?type=sail`."
          },
          "website": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "name",
          "slug",
          "website"
        ],
        "description": "Thin reference to the manufacturer. Fetch `/v1/manufacturers/{slug}` for the full record."
      },
      "Designer": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "website": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "name",
          "website"
        ]
      },
      "Price": {
        "type": "object",
        "properties": {
          "amount": {
            "type": [
              "number",
              "null"
            ]
          },
          "currency": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "string",
                    "description": "ISO 4217 code, e.g. `EUR`."
                  },
                  "symbol": {
                    "type": "string"
                  }
                },
                "required": [
                  "code",
                  "symbol"
                ]
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "amount",
          "currency"
        ],
        "description": "Indicative price as published by the manufacturer. Not a live market price."
      },
      "Engine": {
        "type": "object",
        "properties": {
          "maker": {
            "type": [
              "string",
              "null"
            ]
          },
          "model": {
            "type": [
              "string",
              "null"
            ]
          },
          "type": {
            "type": [
              "string",
              "null"
            ]
          },
          "drive": {
            "type": [
              "string",
              "null"
            ]
          },
          "fuel": {
            "type": [
              "string",
              "null"
            ]
          },
          "power": {
            "type": [
              "string",
              "null"
            ],
            "description": "Free text — may describe multiple engines, e.g. `2 x 40`."
          },
          "powerType": {
            "type": [
              "string",
              "null"
            ],
            "description": "Unit the power figure is expressed in, e.g. `hp`, `kW`."
          }
        },
        "required": [
          "maker",
          "model",
          "type",
          "drive",
          "fuel",
          "power",
          "powerType"
        ]
      },
      "CeClassification": {
        "type": "object",
        "properties": {
          "a": {
            "type": [
              "number",
              "null"
            ]
          },
          "b": {
            "type": [
              "number",
              "null"
            ]
          },
          "c": {
            "type": [
              "number",
              "null"
            ]
          },
          "d": {
            "type": [
              "number",
              "null"
            ]
          }
        },
        "required": [
          "a",
          "b",
          "c",
          "d"
        ],
        "description": "Maximum persons per CE design category, or `null` where the boat carries no classification."
      },
      "Pagination": {
        "type": "object",
        "properties": {
          "total": {
            "type": "integer",
            "minimum": 0,
            "description": "Rows matching your filters, not rows returned."
          },
          "limit": {
            "type": "integer",
            "exclusiveMinimum": 0,
            "description": "Page size that was applied."
          },
          "offset": {
            "type": "integer",
            "minimum": 0,
            "description": "Rows skipped before this page."
          }
        },
        "required": [
          "total",
          "limit",
          "offset"
        ]
      },
      "VersionDescriptor": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "const": "theboatdb-api"
          },
          "apiVersion": {
            "type": "string",
            "const": "v1"
          },
          "status": {
            "type": "string",
            "const": "ok"
          }
        },
        "required": [
          "name",
          "apiVersion",
          "status"
        ],
        "description": "Identifies the service and the API version served under this prefix."
      },
      "HealthStatus": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "const": "ok"
          },
          "uptimeSeconds": {
            "type": "integer",
            "minimum": 0,
            "description": "Seconds since this instance started."
          },
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "description": "Server time when the probe was answered, ISO-8601."
          }
        },
        "required": [
          "status",
          "uptimeSeconds",
          "timestamp"
        ],
        "description": "Liveness of this instance. Says nothing about upstream dependencies."
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "VALIDATION_ERROR",
                  "UNAUTHORIZED",
                  "MISSING_API_KEY",
                  "INVALID_API_KEY",
                  "FORBIDDEN",
                  "INSUFFICIENT_PERMISSIONS",
                  "PRO_SUBSCRIPTION_REQUIRED",
                  "INVALID_SIGNATURE",
                  "NOT_FOUND",
                  "METHOD_NOT_ALLOWED",
                  "KEY_LIMIT_REACHED",
                  "PAYLOAD_TOO_LARGE",
                  "RATE_LIMITED",
                  "QUOTA_EXCEEDED",
                  "INTERNAL_ERROR",
                  "SERVICE_UNAVAILABLE"
                ],
                "description": "Machine-readable code. Branch on this, never on `message`."
              },
              "message": {
                "type": "string",
                "description": "Human-readable explanation. May be reworded at any time."
              },
              "requestId": {
                "type": "string",
                "description": "Quote this in a support request — it identifies the request in our logs."
              },
              "details": {
                "description": "Present on validation failures, naming the offending field."
              }
            },
            "required": [
              "code",
              "message",
              "requestId"
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "BoatSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable document id. Store this rather than the slug."
          },
          "slug": {
            "type": "string",
            "description": "Readable identifier, also accepted by the detail endpoint."
          },
          "model": {
            "type": "string"
          },
          "version": {
            "type": [
              "string",
              "null"
            ],
            "description": "Variant name, where the model has one."
          },
          "manufacturer": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ManufacturerRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Thin reference to the manufacturer. Fetch `/v1/manufacturers/{slug}` for the full record."
          },
          "type": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Reference"
              },
              {
                "type": "null"
              }
            ],
            "description": "Boat type, e.g. sail or power."
          },
          "production": {
            "type": "object",
            "properties": {
              "firstYear": {
                "anyOf": [
                  {
                    "type": "integer"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "lastYear": {
                "anyOf": [
                  {
                    "type": "integer"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "`null` means still in production."
              }
            },
            "required": [
              "firstYear",
              "lastYear"
            ]
          },
          "unit": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ],
            "description": "Which system every measurement on this record is expressed in."
          },
          "lengthOverall": {
            "type": [
              "number",
              "null"
            ],
            "description": "Length overall, in the system `unit` declares."
          },
          "lengthOverallMetres": {
            "type": [
              "number",
              "null"
            ],
            "description": "Length overall in metres, always. The comparable length across a mixed-unit catalogue."
          },
          "referencePrice": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Price"
              },
              {
                "type": "null"
              }
            ],
            "description": "Indicative price as published by the manufacturer. Not a live market price."
          },
          "image": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Image"
              },
              {
                "type": "null"
              }
            ],
            "description": "Primary image."
          }
        },
        "required": [
          "id",
          "slug",
          "model",
          "version",
          "manufacturer",
          "type",
          "production",
          "unit",
          "lengthOverall",
          "lengthOverallMetres",
          "referencePrice",
          "image"
        ],
        "description": "A boat as returned by list endpoints."
      },
      "Boat": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable document id. Store this rather than the slug."
          },
          "slug": {
            "type": "string",
            "description": "Readable identifier, also accepted by the detail endpoint."
          },
          "model": {
            "type": "string"
          },
          "version": {
            "type": [
              "string",
              "null"
            ],
            "description": "Variant name, where the model has one."
          },
          "manufacturer": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ManufacturerRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Thin reference to the manufacturer. Fetch `/v1/manufacturers/{slug}` for the full record."
          },
          "type": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Reference"
              },
              {
                "type": "null"
              }
            ],
            "description": "Boat type, e.g. sail or power."
          },
          "production": {
            "type": "object",
            "properties": {
              "firstYear": {
                "anyOf": [
                  {
                    "type": "integer"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "lastYear": {
                "anyOf": [
                  {
                    "type": "integer"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "`null` means still in production."
              }
            },
            "required": [
              "firstYear",
              "lastYear"
            ]
          },
          "unit": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ],
            "description": "Which system every measurement on this record is expressed in."
          },
          "lengthOverall": {
            "type": [
              "number",
              "null"
            ],
            "description": "Length overall, in the system `unit` declares."
          },
          "lengthOverallMetres": {
            "type": [
              "number",
              "null"
            ],
            "description": "Length overall in metres, always. The comparable length across a mixed-unit catalogue."
          },
          "referencePrice": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Price"
              },
              {
                "type": "null"
              }
            ],
            "description": "Indicative price as published by the manufacturer. Not a live market price."
          },
          "image": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Image"
              },
              {
                "type": "null"
              }
            ],
            "description": "Primary image."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Plain text. `null` until an editor publishes the long-form description."
          },
          "categories": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Reference"
            }
          },
          "activities": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Reference"
            }
          },
          "hull": {
            "type": "object",
            "properties": {
              "type": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/Reference"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "A reference to another catalogue entity."
              },
              "material": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/Reference"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "A reference to another catalogue entity."
              },
              "keelType": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "unsinkable": {
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "trailerable": {
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "appendages": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/NameValue"
                }
              }
            },
            "required": [
              "type",
              "material",
              "keelType",
              "unsinkable",
              "trailerable",
              "appendages"
            ]
          },
          "dimensions": {
            "type": "object",
            "properties": {
              "lengthOverall": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "lengthWaterline": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "beamOverall": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "beamCabinLine": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "beamHull": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "draftMax": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "draftMin": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "headroom": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "bridgedeckClearance": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "nominalBridgedeckClearance": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "displacement": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "ballastWeight": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "ballastType": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "tonnageGross": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "tonnageNet": {
                "type": [
                  "number",
                  "null"
                ]
              }
            },
            "required": [
              "lengthOverall",
              "lengthWaterline",
              "beamOverall",
              "beamCabinLine",
              "beamHull",
              "draftMax",
              "draftMin",
              "headroom",
              "bridgedeckClearance",
              "nominalBridgedeckClearance",
              "displacement",
              "ballastWeight",
              "ballastType",
              "tonnageGross",
              "tonnageNet"
            ],
            "description": "Expressed in the system `unit` declares — metres and kilograms, or feet and pounds."
          },
          "accommodation": {
            "type": "object",
            "properties": {
              "cabins": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "berths": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "heads": {
                "type": [
                  "number",
                  "null"
                ]
              }
            },
            "required": [
              "cabins",
              "berths",
              "heads"
            ]
          },
          "steering": {
            "type": "object",
            "properties": {
              "helm": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "rudder": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "cockpitType": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "cockpitLocation": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "helm",
              "rudder",
              "cockpitType",
              "cockpitLocation"
            ]
          },
          "performance": {
            "type": "object",
            "properties": {
              "cruisingSpeed": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "topSpeed": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "displacementHullSpeed": {
                "type": [
                  "number",
                  "null"
                ]
              }
            },
            "required": [
              "cruisingSpeed",
              "topSpeed",
              "displacementHullSpeed"
            ]
          },
          "rig": {
            "type": "object",
            "properties": {
              "mastHeight": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "mastConfiguration": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "riggingType": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "standingRigging": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "spars": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "rotatingSpars": {
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "spreadersLevel": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "spreadersAngle": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "mastHeight",
              "mastConfiguration",
              "riggingType",
              "standingRigging",
              "spars",
              "rotatingSpars",
              "spreadersLevel",
              "spreadersAngle"
            ]
          },
          "sails": {
            "type": "object",
            "properties": {
              "upwindArea": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "downwindArea": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "mainsail": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/NameValue"
                }
              },
              "headsail": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/NameValue"
                }
              },
              "spinnaker": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/NameValue"
                }
              }
            },
            "required": [
              "upwindArea",
              "downwindArea",
              "mainsail",
              "headsail",
              "spinnaker"
            ]
          },
          "tanks": {
            "type": "object",
            "properties": {
              "fuel": {
                "type": "array",
                "items": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "water": {
                "type": "array",
                "items": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "holding": {
                "type": "array",
                "items": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "waterHeaters": {
                "type": "array",
                "items": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            },
            "required": [
              "fuel",
              "water",
              "holding",
              "waterHeaters"
            ],
            "description": "One entry per tank, holding its capacity as free text. Parse as text, not as a number."
          },
          "engines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Engine"
            }
          },
          "designers": {
            "type": "object",
            "properties": {
              "naval": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Designer"
                }
              },
              "interior": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Designer"
                }
              },
              "exterior": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Designer"
                }
              }
            },
            "required": [
              "naval",
              "interior",
              "exterior"
            ]
          },
          "ceClassification": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/CeClassification"
              },
              {
                "type": "null"
              }
            ],
            "description": "Maximum persons per CE design category, or `null` where the boat carries no classification."
          },
          "gallery": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Image"
            }
          }
        },
        "required": [
          "id",
          "slug",
          "model",
          "version",
          "manufacturer",
          "type",
          "production",
          "unit",
          "lengthOverall",
          "lengthOverallMetres",
          "referencePrice",
          "image",
          "description",
          "categories",
          "activities",
          "hull",
          "dimensions",
          "accommodation",
          "steering",
          "performance",
          "rig",
          "sails",
          "tanks",
          "engines",
          "designers",
          "ceClassification",
          "gallery"
        ],
        "description": "A boat in full, as returned by the detail endpoint."
      },
      "ManufacturerSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable document id. Store this rather than the slug."
          },
          "slug": {
            "type": "string",
            "description": "Readable identifier, also accepted by the detail endpoint."
          },
          "name": {
            "type": "string"
          },
          "website": {
            "type": [
              "string",
              "null"
            ]
          },
          "logo": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Image"
              },
              {
                "type": "null"
              }
            ],
            "description": "An image asset."
          },
          "updatedAt": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "description": "ISO-8601. Sortable."
          }
        },
        "required": [
          "id",
          "slug",
          "name",
          "website",
          "logo",
          "updatedAt"
        ],
        "description": "A manufacturer as returned by list endpoints."
      },
      "Manufacturer": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable document id. Store this rather than the slug."
          },
          "slug": {
            "type": "string",
            "description": "Readable identifier, also accepted by the detail endpoint."
          },
          "name": {
            "type": "string"
          },
          "website": {
            "type": [
              "string",
              "null"
            ]
          },
          "logo": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Image"
              },
              {
                "type": "null"
              }
            ],
            "description": "An image asset."
          },
          "updatedAt": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "description": "ISO-8601. Sortable."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Plain text, or `null` where no description is published."
          },
          "boatCount": {
            "type": "integer",
            "minimum": 0,
            "description": "Published boats attributed to this manufacturer. Fetch them from `/boats` on this resource."
          }
        },
        "required": [
          "id",
          "slug",
          "name",
          "website",
          "logo",
          "updatedAt",
          "description",
          "boatCount"
        ],
        "description": "A manufacturer in full, as returned by the detail endpoint."
      },
      "BoatListResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BoatSummary"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        },
        "required": [
          "data",
          "pagination"
        ]
      },
      "BoatResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Boat"
          }
        },
        "required": [
          "data"
        ]
      },
      "ManufacturerListResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManufacturerSummary"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        },
        "required": [
          "data",
          "pagination"
        ]
      },
      "ManufacturerResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Manufacturer"
          }
        },
        "required": [
          "data"
        ]
      }
    }
  }
}
