{
  "openapi": "3.0.3",
  "info": {
    "title": "The Buoy API",
    "version": "2.0.0",
    "description": "**The Buoy API** — programmatic access to ocean wave buoys on The Surf Kit platform.\n\nAll operations are **read-only** (`GET`). Data includes live and historical readings, geographic filters, chart time series, and **`GET /conditions`** (combined forecast, buoy reading, and tide for a point).\n\n## Authentication\n\nPass your API key in the `Authorization` header:\n\n```\nAuthorization: Bearer YOUR_API_KEY\n```\n\n`GET /buoys/search` and `GET /buoys/nearest` are public (no key required).\n\n## Rate limiting\n\nResponses include `X-RateLimit-*` headers. Exceeding your hourly quota returns `429 Too Many Requests`.",
    "contact": {
      "name": "API Support",
      "email": "thomas@thebuoy.app"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://api.thebuoy.app/v2",
      "description": "Production server (V2)"
    },
    {
      "url": "http://localhost:3000/api/v2",
      "description": "Development server (V2)"
    }
  ],
  "tags": [
    {
      "name": "Buoys",
      "description": "Real-time ocean buoy data"
    },
    {
      "name": "Satellite Passes",
      "description": "Satellite altimeter passes over virtual buoys, and the missions behind them"
    },
    {
      "name": "Conditions",
      "description": "All-in-one conditions endpoint"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "paths": {
    "/buoys": {
      "get": {
        "tags": [
          "Buoys"
        ],
        "summary": "List all buoys",
        "description": "Get a paginated list of buoys with optional filtering by geographic bounds, source, or active status.\n\nReturns buoys in a standardized V2 format with pagination metadata.\n",
        "operationId": "listBuoys",
        "parameters": [
          {
            "name": "bounds",
            "in": "query",
            "description": "Geographic bounding box as JSON string `{\"north\":44,\"south\":43,\"east\":-1,\"west\":-2}`",
            "schema": {
              "type": "string"
            },
            "example": "{\"north\":44,\"south\":43,\"east\":-1,\"west\":-2}"
          },
          {
            "name": "near",
            "in": "query",
            "description": "Coordinates and radius in format `lat,lng` (e.g., \"43.5,-1.5\"). Use with `radius` parameter.",
            "schema": {
              "type": "string"
            },
            "example": "43.5,-1.5"
          },
          {
            "name": "radius",
            "in": "query",
            "description": "Search radius in kilometers (used with `near` parameter, default: 100)",
            "schema": {
              "type": "integer",
              "default": 100
            }
          },
          {
            "name": "source",
            "in": "query",
            "description": "Filter by source (e.g., \"Candhis\", \"Meteo France\", \"Sofar Ocean\")",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "country",
            "in": "query",
            "description": "Filter by ISO 3166-1 alpha-2 country code (e.g., \"FR\", \"ES\", \"PT\"). When set, up to 500 buoys are returned per page.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Z]{2}$"
            },
            "example": "FR"
          },
          {
            "name": "active_only",
            "in": "query",
            "description": "Only return buoys with recent readings (default: true)",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "success"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "buoys": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Buoy"
                          }
                        },
                        "count": {
                          "type": "integer",
                          "description": "Total number of buoys matching filters"
                        }
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PaginationMeta"
                    }
                  }
                },
                "example": {
                  "status": "success",
                  "data": {
                    "buoys": [
                      {
                        "id": 10,
                        "name": "Anglet",
                        "lat": 43.4832,
                        "lng": -1.5586,
                        "source": "Candhis",
                        "source_identifier": "64002",
                        "last_reading_time": "2025-11-01T10:00:00Z",
                        "readings_count": 125430,
                        "last_reading": {
                          "uuid": "abc-123-def",
                          "significient_height": 1.5,
                          "maximum_height": 2.0,
                          "period": 8.5,
                          "time": "2025-11-01T10:00:00Z",
                          "water_temperature": 18.5,
                          "direction": 270,
                          "unit": "m"
                        }
                      }
                    ],
                    "count": 1
                  },
                  "meta": {
                    "page": 1,
                    "per_page": 50,
                    "total_pages": 1,
                    "timestamp": "2025-11-01T10:00:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          }
        },
        "security": [
          {
            "ApiKeyAuth": []
          }
        ]
      }
    },
    "/buoys/{buoy_id}": {
      "get": {
        "tags": [
          "Buoys"
        ],
        "summary": "Get buoy details",
        "description": "Get detailed information about a specific buoy.\n\nThe buoy can be identified by ID or slug (friendly ID).\n",
        "operationId": "getBuoy",
        "parameters": [
          {
            "name": "buoy_id",
            "in": "path",
            "required": true,
            "description": "Buoy ID or slug (friendly ID)",
            "schema": {
              "type": "string"
            },
            "example": "anglet"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "success"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "buoy": {
                          "$ref": "#/components/schemas/BuoyDetail"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "status": "success",
                  "data": {
                    "buoy": {
                      "id": 10,
                      "name": "Anglet",
                      "slug": "anglet",
                      "lat": 43.4832,
                      "lng": -1.5586,
                      "source": "Candhis",
                      "source_identifier": "64002",
                      "country": "France",
                      "last_reading_time": "2025-11-01T10:00:00Z",
                      "readings_count": 125430,
                      "last_reading": {
                        "uuid": "abc-123-def",
                        "significient_height": 1.5,
                        "maximum_height": 2.0,
                        "period": 8.5,
                        "time": "2025-11-01T10:00:00Z",
                        "water_temperature": 18.5,
                        "direction": 270,
                        "unit": "m"
                      }
                    }
                  },
                  "meta": {
                    "timestamp": "2025-11-01T10:00:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          }
        },
        "security": [
          {
            "ApiKeyAuth": []
          }
        ]
      }
    },
    "/buoys/last_readings": {
      "get": {
        "tags": [
          "Buoys"
        ],
        "summary": "Get last readings for multiple buoys",
        "description": "Bulk fetch the latest reading for a list of buoys by ID.\n\nBy default returns the first 3 IDs supplied. Pass `limit` to fetch up to 100 buoys in a single request.\n\n**Tip:** To get last readings for all buoys in a country in one call, use `GET /buoys?country=FR` instead — the index response already includes `last_reading` for each buoy.\n",
        "operationId": "getLastReadings",
        "parameters": [
          {
            "name": "ids",
            "in": "query",
            "required": true,
            "description": "Comma-separated or array-style buoy IDs (e.g., `ids=1,2,3` or `ids[]=1&ids[]=2`)",
            "schema": {
              "type": "string"
            },
            "example": "1,2,3"
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of buoys to return from the supplied ids list (default: 3, max: 100)",
            "schema": {
              "type": "integer",
              "default": 3,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "success"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "buoys": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/BuoyWithLastReading"
                          }
                        },
                        "missing_ids": {
                          "type": "array",
                          "items": {
                            "type": "integer"
                          },
                          "description": "IDs that were requested but not found"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "ids parameter required"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          }
        },
        "security": [
          {
            "ApiKeyAuth": []
          }
        ]
      }
    },
    "/buoys/search": {
      "get": {
        "tags": [
          "Buoys"
        ],
        "summary": "Search buoys",
        "description": "Search for buoys by name, slug, or source identifier.\n\nReturns matching buoys with minimal information.\n",
        "operationId": "searchBuoys",
        "parameters": [
          {
            "name": "query",
            "in": "query",
            "required": true,
            "description": "Search term (minimum 2 characters)",
            "schema": {
              "type": "string",
              "minLength": 2
            },
            "example": "anglet"
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of results (default 10, max 50)",
            "schema": {
              "type": "integer",
              "default": 10,
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SuccessResponse"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "buoys": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/BuoySearch"
                              }
                            },
                            "count": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "status": "success",
                  "data": {
                    "buoys": [
                      {
                        "id": 10,
                        "name": "Anglet",
                        "slug": "anglet",
                        "lat": 43.4832,
                        "lng": -1.5586,
                        "source": "Candhis",
                        "source_identifier": "64002",
                        "country": "France"
                      }
                    ],
                    "count": 1
                  },
                  "meta": {
                    "timestamp": "2025-11-01T10:00:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          }
        },
        "security": [
          {
            "ApiKeyAuth": []
          }
        ]
      }
    },
    "/buoys/nearest": {
      "get": {
        "tags": [
          "Buoys"
        ],
        "summary": "Find nearest buoy to coordinates",
        "description": "Find the closest active buoy(s) to a given latitude and longitude within a maximum distance.\n\nUse the `limit` parameter to control how many results are returned (default: 1, max: 20).\n",
        "operationId": "nearestBuoy",
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "description": "Latitude in decimal degrees (-90 to 90)",
            "schema": {
              "type": "number",
              "format": "float",
              "minimum": -90,
              "maximum": 90
            },
            "example": 43.5
          },
          {
            "name": "lng",
            "in": "query",
            "required": true,
            "description": "Longitude in decimal degrees (-180 to 180)",
            "schema": {
              "type": "number",
              "format": "float",
              "minimum": -180,
              "maximum": 180
            },
            "example": -1.5
          },
          {
            "name": "max_distance",
            "in": "query",
            "description": "Maximum search radius in kilometers (default: 150)",
            "schema": {
              "type": "number",
              "format": "float",
              "default": 150.0,
              "minimum": 0
            },
            "example": 150
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of results to return (default: 1, max: 20)",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1,
              "maximum": 20
            },
            "example": 5
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response with nearest buoy(s)",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "description": "Response when limit=1 (single buoy)",
                      "properties": {
                        "status": {
                          "type": "string",
                          "example": "success"
                        },
                        "data": {
                          "type": "object",
                          "properties": {
                            "buoy": {
                              "$ref": "#/components/schemas/BuoyWithDistance"
                            }
                          }
                        }
                      }
                    },
                    {
                      "type": "object",
                      "description": "Response when limit>1 (array of buoys)",
                      "properties": {
                        "status": {
                          "type": "string",
                          "example": "success"
                        },
                        "data": {
                          "type": "object",
                          "properties": {
                            "buoys": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/BuoyWithDistance"
                              }
                            },
                            "count": {
                              "type": "integer",
                              "description": "Number of buoys returned"
                            }
                          }
                        }
                      }
                    }
                  ]
                },
                "examples": {
                  "single_buoy": {
                    "summary": "Single buoy (limit=1 or default)",
                    "value": {
                      "status": "success",
                      "data": {
                        "buoy": {
                          "id": 10,
                          "name": "Anglet",
                          "lat": 43.4832,
                          "lng": -1.5586,
                          "source": "Candhis",
                          "source_identifier": "64002",
                          "distance_km": 2.5
                        }
                      },
                      "meta": {
                        "timestamp": "2025-11-01T10:00:00Z"
                      }
                    }
                  },
                  "multiple_buoys": {
                    "summary": "Multiple buoys (limit>1)",
                    "value": {
                      "status": "success",
                      "data": {
                        "buoys": [
                          {
                            "id": 10,
                            "name": "Anglet",
                            "lat": 43.4832,
                            "lng": -1.5586,
                            "source": "Candhis",
                            "source_identifier": "64002",
                            "distance_km": 2.5
                          },
                          {
                            "id": 11,
                            "name": "Biarritz",
                            "lat": 43.48,
                            "lng": -1.56,
                            "source": "Candhis",
                            "source_identifier": "64003",
                            "distance_km": 5.8
                          }
                        ],
                        "count": 2
                      },
                      "meta": {
                        "timestamp": "2025-11-01T10:00:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "Missing or invalid lat/lng parameters"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "No buoy found within max_distance"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          }
        },
        "security": [
          {
            "ApiKeyAuth": []
          }
        ]
      }
    },
    "/buoys/{buoy_id}/readings": {
      "get": {
        "tags": [
          "Buoys"
        ],
        "summary": "Get historical readings for a buoy",
        "description": "Get paginated historical readings for a buoy. Supports filtering by date range.\n\n**Pagination:** Default 20 per page, minimum 1, maximum 100 per page.\n",
        "operationId": "getBuoyReadings",
        "parameters": [
          {
            "name": "buoy_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "start_date",
            "in": "query",
            "description": "Start date in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "end_date",
            "in": "query",
            "description": "End date in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "date",
            "in": "query",
            "description": "Filter by single date (YYYY-MM-DD). Alternative to start_date/end_date.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "timestamp",
            "in": "query",
            "description": "Filter by unix timestamp (seconds). Returns readings within a 3-hour window.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "order",
            "in": "query",
            "description": "Sort order",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "success"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "buoy": {
                          "type": "object",
                          "properties": {
                            "name": {
                              "type": "string"
                            },
                            "lat": {
                              "type": "number"
                            },
                            "lng": {
                              "type": "number"
                            },
                            "source": {
                              "type": "string",
                              "description": "Buoy data source (e.g., \"Candhis\", \"Meteo France\", \"Sofar Ocean\")"
                            },
                            "dtz": {
                              "type": "string",
                              "description": "Data timezone identifier"
                            }
                          }
                        },
                        "readings": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/BuoyReading"
                          }
                        },
                        "count": {
                          "type": "integer"
                        }
                      }
                    },
                    "meta": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/PaginationMeta"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "date_range": {
                              "type": "object",
                              "properties": {
                                "start": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "end": {
                                  "type": "string",
                                  "format": "date-time"
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          }
        },
        "security": [
          {
            "ApiKeyAuth": []
          }
        ]
      }
    },
    "/buoys/{buoy_id}/readings/search": {
      "get": {
        "tags": [
          "Buoys"
        ],
        "summary": "Search for reading closest to a specific time",
        "description": "Find the reading closest to a target datetime within a tolerance window.\n\nUseful for finding conditions at a specific past time.\n",
        "operationId": "searchBuoyReadings",
        "parameters": [
          {
            "name": "buoy_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "date",
            "in": "query",
            "required": true,
            "description": "Target date (YYYY-MM-DD)",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "time",
            "in": "query",
            "required": true,
            "description": "Target time (HH:MM)",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{2}:[0-9]{2}$"
            }
          },
          {
            "name": "tolerance_hours",
            "in": "query",
            "description": "Hours to search around target time (default: 3, max: 24)",
            "schema": {
              "type": "integer",
              "default": 3,
              "minimum": 1,
              "maximum": 24
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "success"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "closest_reading": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/BuoyReading"
                            },
                            {
                              "type": "object",
                              "properties": {
                                "time_diff_minutes": {
                                  "type": "integer",
                                  "description": "Minutes difference from target time"
                                }
                              }
                            }
                          ]
                        },
                        "search_parameters": {
                          "type": "object",
                          "properties": {
                            "target_datetime": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "tolerance_hours": {
                              "type": "integer"
                            },
                            "search_range": {
                              "type": "object",
                              "properties": {
                                "start": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "end": {
                                  "type": "string",
                                  "format": "date-time"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "Missing date or time parameter, or invalid format"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "No reading found within tolerance window"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          }
        },
        "security": [
          {
            "ApiKeyAuth": []
          }
        ]
      }
    },
    "/buoys/{buoy_id}/readings/series": {
      "get": {
        "tags": [
          "Buoys"
        ],
        "summary": "Get seasonal buoy reading series",
        "description": "Returns grouped year-over-year seasonal time series for a single buoy without pagination.\n\nSupports Météo-France and Candhis buoys and is intended for server-side consumers\nthat need chart-ready seasonal comparison data. Metric availability is source-specific:\nMétéo-France supports wave, water temperature, and wind metrics; Candhis supports wave\nmetrics only. For Candhis, `significient_height` maps to H13D (H1/3),\n`period` maps to TH13D (significant wave period), and `peak_period` maps\nto TP (peak period).\n\nWhen `start_year` and `end_year` are omitted, the endpoint returns the last 10 complete\nseasons. Explicit year ranges can request up to 13 seasons, for example `2013..2025`.\n",
        "operationId": "getBuoyReadingSeries",
        "parameters": [
          {
            "name": "buoy_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "metric",
            "in": "query",
            "required": true,
            "description": "Metric to return for the seasonal series. Candhis supports `significient_height`, `maximum_height`, `period`, `peak_period`, and `direction`.",
            "schema": {
              "type": "string",
              "enum": [
                "significient_height",
                "maximum_height",
                "period",
                "peak_period",
                "direction",
                "water_temperature",
                "wind_speed",
                "wind_direction",
                "gust_speed"
              ]
            }
          },
          {
            "name": "bucket",
            "in": "query",
            "description": "Bucket size for grouping the seasonal series",
            "schema": {
              "type": "string",
              "enum": [
                "hour",
                "day",
                "week"
              ],
              "default": "hour"
            }
          },
          {
            "name": "aggregation",
            "in": "query",
            "description": "Aggregation used when bucket is day or week",
            "schema": {
              "type": "string",
              "enum": [
                "median",
                "avg",
                "min",
                "max"
              ],
              "default": "median"
            }
          },
          {
            "name": "start_year",
            "in": "query",
            "description": "Inclusive first season year. Explicit ranges can span up to 13 seasons.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "end_year",
            "in": "query",
            "description": "Inclusive last season year. Explicit ranges can span up to 13 seasons.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "season_start_month",
            "in": "query",
            "description": "Season start month",
            "schema": {
              "type": "integer",
              "default": 4,
              "minimum": 1,
              "maximum": 12
            }
          },
          {
            "name": "season_start_day",
            "in": "query",
            "description": "Season start day of month",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1,
              "maximum": 31
            }
          },
          {
            "name": "season_end_month",
            "in": "query",
            "description": "Season end month",
            "schema": {
              "type": "integer",
              "default": 6,
              "minimum": 1,
              "maximum": 12
            }
          },
          {
            "name": "season_end_day",
            "in": "query",
            "description": "Season end day of month",
            "schema": {
              "type": "integer",
              "default": 30,
              "minimum": 1,
              "maximum": 31
            }
          },
          {
            "name": "timezone",
            "in": "query",
            "description": "IANA timezone used for bucket boundaries and labels. Defaults to the buoy timezone or UTC.",
            "schema": {
              "type": "string",
              "example": "Europe/Paris"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Seasonal chart series grouped by year",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "success"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "buoy": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            },
                            "lat": {
                              "type": "number",
                              "format": "float"
                            },
                            "lng": {
                              "type": "number",
                              "format": "float"
                            },
                            "source": {
                              "type": "string"
                            },
                            "dtz": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "metric": {
                          "type": "string",
                          "enum": [
                            "significient_height",
                            "maximum_height",
                            "period",
                            "peak_period",
                            "direction",
                            "water_temperature",
                            "wind_speed",
                            "wind_direction",
                            "gust_speed"
                          ]
                        },
                        "bucket": {
                          "type": "string",
                          "enum": [
                            "hour",
                            "day",
                            "week"
                          ]
                        },
                        "aggregation": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "median",
                            "avg",
                            "min",
                            "max"
                          ]
                        },
                        "timezone": {
                          "type": "string"
                        },
                        "season": {
                          "$ref": "#/components/schemas/SeasonRange"
                        },
                        "years": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/BuoySeasonalSeriesYear"
                          }
                        },
                        "count": {
                          "type": "integer"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Non-paginated response metadata"
                    }
                  }
                },
                "example": {
                  "status": "success",
                  "data": {
                    "buoy": {
                      "id": 295,
                      "name": "Azur",
                      "lat": 43.38,
                      "lng": 7.83,
                      "source": "Météo France",
                      "dtz": "Europe/Paris"
                    },
                    "metric": "significient_height",
                    "bucket": "day",
                    "aggregation": "median",
                    "timezone": "Europe/Paris",
                    "season": {
                      "start": {
                        "month": 4,
                        "day": 1
                      },
                      "end": {
                        "month": 6,
                        "day": 30
                      }
                    },
                    "years": [
                      {
                        "year": 2025,
                        "complete": true,
                        "point_count": 91,
                        "points": [
                          {
                            "bucket_index": 1,
                            "label": "Apr 01",
                            "bucket_start": "2025-04-01T00:00:00+02:00",
                            "bucket_end": "2025-04-01T23:59:59+02:00",
                            "value": 1.12,
                            "sample_count": 24
                          }
                        ]
                      }
                    ],
                    "count": 91
                  },
                  "meta": {}
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "Invalid metric, bucket, aggregation, timezone, or season window"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "description": "Unsupported buoy source for seasonal series",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnprocessableEntity"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          }
        },
        "security": [
          {
            "ApiKeyAuth": []
          }
        ]
      }
    },
    "/buoys/{buoy_id}/readings/{reading_id}": {
      "get": {
        "tags": [
          "Buoys"
        ],
        "summary": "Get a specific buoy reading",
        "description": "Get detailed information about a specific reading from a buoy.",
        "operationId": "getBuoyReading",
        "parameters": [
          {
            "name": "buoy_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "reading_id",
            "in": "path",
            "required": true,
            "description": "Reading ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "success"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "reading": {
                          "$ref": "#/components/schemas/BuoyReading"
                        },
                        "buoy": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "name": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "status": "success",
                  "data": {
                    "reading": {
                      "id": 12345,
                      "uuid": "abc-123-def",
                      "significient_height": 1.5,
                      "maximum_height": 2.0,
                      "period": 8.5,
                      "time": "2025-11-01T10:00:00Z",
                      "water_temperature": 18.5,
                      "direction": 270,
                      "direction_compass": "W",
                      "unit": "m",
                      "energy_per_wave": 25.5
                    },
                    "buoy": {
                      "id": 10,
                      "name": "Anglet"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          }
        },
        "security": [
          {
            "ApiKeyAuth": []
          }
        ]
      }
    },
    "/buoys/{buoy_id}/readings/chart": {
      "get": {
        "tags": [
          "Buoys"
        ],
        "summary": "Get chart data for a buoy",
        "description": "Returns comprehensive chart data including readings, forecasts, and tide levels.\n",
        "operationId": "getBuoyChart",
        "parameters": [
          {
            "name": "buoy_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Chart data with time series arrays",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "success"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "buoy": {
                          "type": "object"
                        },
                        "fields": {
                          "type": "object",
                          "description": "Field name mappings"
                        },
                        "time": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "format": "date-time"
                          }
                        },
                        "significant_height": {
                          "type": "array",
                          "items": {
                            "type": "number"
                          }
                        },
                        "maximum_height": {
                          "type": "array",
                          "items": {
                            "type": "number"
                          }
                        },
                        "period": {
                          "type": "array",
                          "items": {
                            "type": "number"
                          }
                        },
                        "forecast_wave_height": {
                          "type": "array",
                          "items": {
                            "type": "number"
                          }
                        },
                        "tide_level": {
                          "type": "array",
                          "items": {
                            "type": "number"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          }
        },
        "security": [
          {
            "ApiKeyAuth": []
          }
        ]
      }
    },
    "/buoys/satellite_passes": {
      "get": {
        "tags": [
          "Satellite Passes"
        ],
        "summary": "List satellite altimeter passes",
        "description": "List recorded satellite altimeter passes over virtual buoys.\n\nFilter to a single satellite with `mission` — a slug returned by\n`GET /buoys/satellites` (e.g. `cfosat`). An unknown slug returns `404`.\n",
        "operationId": "listSatellitePasses",
        "parameters": [
          {
            "name": "mission",
            "in": "query",
            "description": "Filter passes to a single satellite mission, by slug (see `GET /buoys/satellites`).",
            "schema": {
              "type": "string"
            },
            "example": "cfosat"
          },
          {
            "name": "active_only",
            "in": "query",
            "description": "When true (default), return only passes that have fresh, displayable buoy readings.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "since",
            "in": "query",
            "description": "Only return passes at or after this ISO 8601 timestamp.",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "example": "2026-05-01T00:00:00Z"
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of passes to return (default: 20, max: 100).",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "success"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "satellite_passes": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SatellitePass"
                          }
                        },
                        "count": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "Unknown mission slug"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          }
        },
        "security": [
          {
            "ApiKeyAuth": []
          }
        ]
      }
    },
    "/buoys/satellites": {
      "get": {
        "tags": [
          "Satellite Passes"
        ],
        "summary": "List available satellites/missions",
        "description": "Discovery endpoint listing the satellite missions you can filter passes by,\nwith each mission's pass count and metadata.\n\nUse a returned `slug` as the `mission` filter on `GET /buoys/satellite_passes`.\n\nMission `description` is localized (English/French). Select the language\nwith `?locale=` or the `Accept-Language` header; defaults to English.\n",
        "operationId": "listSatellites",
        "parameters": [
          {
            "name": "locale",
            "in": "query",
            "description": "Language for localized text (the mission `description`). `Accept-Language` is also honored.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "fr"
              ],
              "default": "en"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "success"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "satellites": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SatelliteMission"
                          }
                        },
                        "count": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          }
        },
        "security": [
          {
            "ApiKeyAuth": []
          }
        ]
      }
    },
    "/conditions": {
      "get": {
        "tags": [
          "Conditions"
        ],
        "summary": "Get all-in-one conditions (V2)",
        "description": "Returns combined forecast, reading, and tide data for a location in a standardized V2 format.\n\nThis endpoint combines:\n- Wave forecast (height, period, direction, energy)\n- Wind forecast (speed, direction)\n- Swell forecast (height, period, direction)\n- Nearest buoy reading (if available within 50km)\n- Tide information (height, direction, next change)\n\n**Improvements over V1:**\n- ✅ Standardized `{status, data, meta}` response format\n- ✅ Structured JSON data (not human-readable string)\n- ✅ Includes nearby buoy/weather station information\n- ✅ Better error handling\n- ✅ Optional `spot_id` parameter for enhanced context\n- ✅ Forecast metadata (update times, sources)\n",
        "operationId": "getConditionsV2",
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "description": "Latitude in decimal degrees",
            "example": 43.47967255638358,
            "schema": {
              "type": "number",
              "format": "float",
              "example": 43.47967255638358
            }
          },
          {
            "name": "lng",
            "in": "query",
            "required": true,
            "description": "Longitude in decimal degrees",
            "example": -1.5692197193092723,
            "schema": {
              "type": "number",
              "format": "float",
              "example": -1.5692197193092723
            }
          },
          {
            "name": "time",
            "in": "query",
            "required": true,
            "description": "ISO 8601 timestamp for which to get conditions",
            "example": "2026-03-28T12:00:00Z",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-03-28T12:00:00Z"
            }
          },
          {
            "name": "spot_id",
            "in": "query",
            "description": "Optional spot ID for enhanced context and nearby data",
            "example": 1,
            "schema": {
              "type": "integer",
              "example": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response with conditions data",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "success"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "location": {
                          "type": "object",
                          "properties": {
                            "lat": {
                              "type": "number",
                              "format": "float",
                              "description": "Latitude",
                              "example": 43.47967255638358
                            },
                            "lng": {
                              "type": "number",
                              "format": "float",
                              "description": "Longitude",
                              "example": -1.5692197193092723
                            },
                            "timezone": {
                              "type": "string",
                              "description": "Timezone for the location",
                              "example": "Europe/Paris"
                            }
                          }
                        },
                        "forecast": {
                          "type": "object",
                          "description": "Wave, wind, and swell forecast data",
                          "properties": {
                            "waves_height": {
                              "type": "number",
                              "format": "float",
                              "description": "Forecast wave height in meters",
                              "example": 1.5
                            },
                            "waves_period": {
                              "type": "number",
                              "format": "float",
                              "description": "Forecast wave period in seconds",
                              "example": 8.5
                            },
                            "waves_direction": {
                              "type": "string",
                              "description": "Forecast wave direction (compass direction)",
                              "example": "SW"
                            },
                            "wind_speed": {
                              "type": "number",
                              "format": "float",
                              "description": "Forecast wind speed in km/h",
                              "example": 15
                            },
                            "wind_direction": {
                              "type": "string",
                              "description": "Forecast wind direction (compass direction)",
                              "example": "NW"
                            },
                            "swell_height": {
                              "type": "number",
                              "format": "float",
                              "description": "Forecast swell height in meters",
                              "example": 1.2
                            },
                            "swell_period": {
                              "type": "number",
                              "format": "float",
                              "description": "Forecast swell period in seconds",
                              "example": 10
                            },
                            "swell_direction": {
                              "type": "string",
                              "description": "Forecast swell direction (compass direction)",
                              "example": "SW"
                            },
                            "wave_energy": {
                              "type": "number",
                              "format": "float",
                              "description": "Wave energy in kJ per meter per wave",
                              "example": 25.5
                            }
                          }
                        },
                        "reading": {
                          "type": "object",
                          "description": "Nearest buoy reading (if available within 50km)",
                          "nullable": true,
                          "properties": {
                            "exists": {
                              "type": "boolean",
                              "description": "Whether a reading was found",
                              "example": true
                            },
                            "significient_height": {
                              "type": "number",
                              "format": "float",
                              "description": "Significant wave height in meters",
                              "example": 1.8
                            },
                            "maximum_height": {
                              "type": "number",
                              "format": "float",
                              "description": "Maximum wave height in meters",
                              "example": 2.5
                            },
                            "direction_degrees": {
                              "type": "integer",
                              "description": "Wave direction in degrees (0-360)",
                              "example": 225
                            },
                            "period": {
                              "type": "integer",
                              "description": "Wave period in seconds",
                              "example": 9
                            },
                            "time": {
                              "type": "string",
                              "format": "date-time",
                              "description": "Reading timestamp",
                              "example": "2026-03-28T11:45:00Z"
                            },
                            "wave_energy": {
                              "type": "number",
                              "format": "float",
                              "description": "Wave energy in kJ per meter per wave",
                              "example": 28.3
                            },
                            "buoy": {
                              "type": "object",
                              "description": "Information about the buoy providing the reading",
                              "properties": {
                                "id": {
                                  "type": "integer",
                                  "example": 10
                                },
                                "name": {
                                  "type": "string",
                                  "example": "Anglet"
                                },
                                "distance_km": {
                                  "type": "number",
                                  "format": "float",
                                  "description": "Distance from location in kilometers",
                                  "example": 2.5
                                }
                              }
                            }
                          }
                        },
                        "tide": {
                          "type": "object",
                          "description": "Tide information for the location",
                          "properties": {
                            "height": {
                              "type": "number",
                              "format": "float",
                              "description": "Current tide height in meters",
                              "example": 1.2
                            },
                            "time": {
                              "type": "string",
                              "format": "date-time",
                              "description": "Time of the tide reading",
                              "example": "2026-03-28T12:00:00Z"
                            },
                            "direction": {
                              "type": "string",
                              "enum": [
                                "rising",
                                "falling"
                              ],
                              "description": "Current tide direction",
                              "example": "rising"
                            },
                            "seconds_to_next_tide_change": {
                              "type": "integer",
                              "description": "Seconds until the next tide change (high or low)",
                              "example": 3600
                            },
                            "next_tide": {
                              "type": "string",
                              "enum": [
                                "high",
                                "low"
                              ],
                              "description": "Type of the next tide change",
                              "example": "high"
                            },
                            "next_tide_height": {
                              "type": "number",
                              "format": "float",
                              "description": "Height of the next tide in meters",
                              "example": 2.5
                            },
                            "harbor": {
                              "type": "string",
                              "description": "Harbor name used for tide calculations",
                              "example": "Biarritz"
                            }
                          }
                        },
                        "metadata": {
                          "type": "object",
                          "description": "Additional metadata about the data sources",
                          "properties": {
                            "forecast_updated_at": {
                              "type": "string",
                              "format": "date-time",
                              "description": "When the forecast was last updated",
                              "example": "2026-03-28T09:00:00Z"
                            },
                            "reading_age_minutes": {
                              "type": "integer",
                              "description": "Age of the reading in minutes (if reading exists)",
                              "nullable": true,
                              "example": 15
                            },
                            "tide_source": {
                              "type": "string",
                              "description": "Source of tide data",
                              "example": "shom"
                            }
                          }
                        },
                        "units": {
                          "type": "object",
                          "description": "Units used for measurements",
                          "properties": {
                            "height": {
                              "type": "string",
                              "example": "m"
                            },
                            "speed": {
                              "type": "string",
                              "example": "km/h"
                            }
                          }
                        },
                        "human_readable": {
                          "type": "string",
                          "description": "Optional human-readable conditions description (for compatibility)",
                          "example": "📍 Anglet buoy (2.5km away)..."
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "timestamp": {
                          "type": "string",
                          "format": "date-time",
                          "description": "Response timestamp",
                          "example": "2026-03-28T12:00:00Z"
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "with_reading": {
                    "summary": "Conditions with buoy reading",
                    "value": {
                      "status": "success",
                      "data": {
                        "location": {
                          "lat": 43.47967255638358,
                          "lng": -1.5692197193092723,
                          "timezone": "Europe/Paris"
                        },
                        "forecast": {
                          "waves_height": 1.5,
                          "waves_period": 8.5,
                          "waves_direction": "SW",
                          "wind_speed": 15,
                          "wind_direction": "NW",
                          "swell_height": 1.2,
                          "swell_period": 10,
                          "swell_direction": "SW",
                          "wave_energy": 25.5
                        },
                        "reading": {
                          "exists": true,
                          "significient_height": 1.8,
                          "maximum_height": 2.5,
                          "direction_degrees": 225,
                          "period": 9,
                          "time": "2026-03-28T11:45:00Z",
                          "wave_energy": 28.3,
                          "buoy": {
                            "id": 10,
                            "name": "Anglet",
                            "distance_km": 2.5
                          }
                        },
                        "tide": {
                          "height": 1.2,
                          "time": "2026-03-28T12:00:00Z",
                          "direction": "rising",
                          "seconds_to_next_tide_change": 3600,
                          "next_tide": "high",
                          "next_tide_height": 2.5,
                          "harbor": "Biarritz"
                        },
                        "metadata": {
                          "forecast_updated_at": "2026-03-28T09:00:00Z",
                          "reading_age_minutes": 15,
                          "tide_source": "shom"
                        },
                        "units": {
                          "height": "m",
                          "speed": "km/h"
                        }
                      },
                      "meta": {
                        "timestamp": "2026-03-28T12:00:00Z"
                      }
                    }
                  },
                  "forecast_only": {
                    "summary": "Conditions with forecast only (no nearby buoy)",
                    "value": {
                      "status": "success",
                      "data": {
                        "location": {
                          "lat": 43.47967255638358,
                          "lng": -1.5692197193092723,
                          "timezone": "Europe/Paris"
                        },
                        "forecast": {
                          "waves_height": 1.5,
                          "waves_period": 8.5,
                          "waves_direction": "SW",
                          "wind_speed": 15,
                          "wind_direction": "NW",
                          "swell_height": 1.2,
                          "swell_period": 10,
                          "swell_direction": "SW",
                          "wave_energy": 25.5
                        },
                        "reading": {
                          "exists": false
                        },
                        "tide": {
                          "height": 1.2,
                          "time": "2026-03-28T12:00:00Z",
                          "direction": "rising",
                          "seconds_to_next_tide_change": 3600,
                          "next_tide": "high",
                          "next_tide_height": 2.5,
                          "harbor": "Biarritz"
                        },
                        "metadata": {
                          "forecast_updated_at": "2026-03-28T09:00:00Z",
                          "reading_age_minutes": null,
                          "tide_source": "shom"
                        },
                        "units": {
                          "height": "m",
                          "speed": "km/h"
                        }
                      },
                      "meta": {
                        "timestamp": "2026-03-28T12:00:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": "Missing required parameters (lat, lng, or time) or invalid format"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          }
        },
        "security": [
          {
            "ApiKeyAuth": []
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "Buoy": {
        "type": "object",
        "description": "Basic buoy information",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "lat": {
            "type": "number",
            "format": "float"
          },
          "lng": {
            "type": "number",
            "format": "float"
          },
          "source": {
            "type": "string",
            "description": "Data source (e.g., \"Candhis\", \"Meteo France\", \"Sofar Ocean\")"
          },
          "source_identifier": {
            "type": "string",
            "description": "Source-specific identifier"
          },
          "last_reading_time": {
            "type": "string",
            "format": "date-time",
            "description": "Timestamp of most recent reading"
          },
          "readings_count": {
            "type": "integer",
            "description": "Total number of readings for this buoy"
          },
          "last_reading": {
            "$ref": "#/components/schemas/BuoyReading"
          }
        }
      },
      "PaginationMeta": {
        "type": "object",
        "description": "Pagination metadata included in list responses",
        "properties": {
          "page": {
            "type": "integer",
            "description": "Current page number",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "description": "Number of items per page",
            "example": 50
          },
          "total_pages": {
            "type": "integer",
            "description": "Total number of pages",
            "example": 5
          },
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "description": "Response timestamp in ISO 8601 format"
          }
        }
      },
      "BuoyDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Buoy"
          },
          {
            "type": "object",
            "properties": {
              "slug": {
                "type": "string",
                "description": "URL-friendly identifier"
              },
              "country": {
                "type": "string"
              }
            }
          }
        ]
      },
      "BuoyWithLastReading": {
        "type": "object",
        "description": "Buoy with last reading (used in bulk last_readings endpoint)",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "lat": {
            "type": "number",
            "format": "float"
          },
          "lng": {
            "type": "number",
            "format": "float"
          },
          "source": {
            "type": "string"
          },
          "last_reading": {
            "$ref": "#/components/schemas/BuoyReading"
          }
        }
      },
      "SuccessResponse": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "example": "success"
          },
          "data": {
            "type": "object"
          },
          "meta": {
            "type": "object",
            "properties": {
              "timestamp": {
                "type": "string",
                "format": "date-time"
              },
              "page": {
                "type": "integer"
              },
              "per_page": {
                "type": "integer"
              }
            }
          }
        }
      },
      "BuoySearch": {
        "type": "object",
        "description": "Minimal buoy information for search results",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "lat": {
            "type": "number",
            "format": "float"
          },
          "lng": {
            "type": "number",
            "format": "float"
          },
          "source": {
            "type": "string"
          },
          "source_identifier": {
            "type": "string"
          },
          "country": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "BuoyWithDistance": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "lat": {
            "type": "number",
            "format": "float"
          },
          "lng": {
            "type": "number",
            "format": "float"
          },
          "source": {
            "type": "string",
            "description": "Data source (e.g., \"météo-france\", \"noaa\")"
          },
          "source_identifier": {
            "type": "string",
            "description": "Source-specific identifier"
          },
          "distance_km": {
            "type": "number",
            "format": "float",
            "description": "Distance from query coordinates in kilometers"
          },
          "last_reading": {
            "type": "object",
            "nullable": true,
            "properties": {
              "significient_height": {
                "type": "number",
                "format": "float"
              },
              "period": {
                "type": "number",
                "format": "float"
              },
              "time": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "BuoyReading": {
        "type": "object",
        "description": "Individual buoy reading",
        "properties": {
          "id": {
            "type": "integer"
          },
          "uuid": {
            "type": "string",
            "description": "Unique identifier for the reading"
          },
          "significient_height": {
            "type": "number",
            "format": "float",
            "description": "Significant wave height in meters"
          },
          "maximum_height": {
            "type": "number",
            "format": "float",
            "description": "Maximum wave height in meters"
          },
          "period": {
            "type": "number",
            "format": "float",
            "description": "Wave period in seconds"
          },
          "time": {
            "type": "string",
            "format": "date-time",
            "description": "Reading timestamp"
          },
          "water_temperature": {
            "type": "number",
            "format": "float",
            "description": "Water temperature in Celsius"
          },
          "direction": {
            "type": "integer",
            "description": "Wave direction in degrees (0-360)"
          },
          "direction_compass": {
            "type": "string",
            "description": "Compass direction (e.g., \"SW\", \"NW\")"
          },
          "unit": {
            "type": "string",
            "description": "Unit abbreviation (e.g., \"m\")"
          },
          "energy_per_wave": {
            "type": "number",
            "format": "float",
            "description": "Energy per wave in kilojoules"
          }
        }
      },
      "SeasonRange": {
        "type": "object",
        "properties": {
          "start": {
            "type": "object",
            "properties": {
              "month": {
                "type": "integer"
              },
              "day": {
                "type": "integer"
              }
            }
          },
          "end": {
            "type": "object",
            "properties": {
              "month": {
                "type": "integer"
              },
              "day": {
                "type": "integer"
              }
            }
          }
        }
      },
      "BuoySeasonalSeriesYear": {
        "type": "object",
        "properties": {
          "year": {
            "type": "integer"
          },
          "complete": {
            "type": "boolean",
            "description": "Whether the requested season is complete for this year"
          },
          "point_count": {
            "type": "integer"
          },
          "points": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BuoySeasonalSeriesPoint"
            }
          }
        }
      },
      "UnprocessableEntity": {
        "type": "object",
        "description": "Validation error response (422)",
        "allOf": [
          {
            "$ref": "#/components/schemas/ErrorResponse"
          },
          {
            "type": "object",
            "properties": {
              "details": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        ]
      },
      "SatellitePass": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "example": "Bay of Biscay pass · CFOSAT · 14 May 10:00 UTC"
          },
          "external_id": {
            "type": "string"
          },
          "satellite_pass_id": {
            "type": "string",
            "description": "Alias of external_id."
          },
          "platform": {
            "type": "string",
            "nullable": true,
            "description": "Raw platform label stored on the pass.",
            "example": "CFOSAT"
          },
          "mission": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SatelliteMissionRef"
              }
            ],
            "nullable": true,
            "description": "Resolved satellite mission, or null when unknown."
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "ended_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "buoy_count": {
            "type": "integer"
          },
          "observation_count": {
            "type": "integer"
          },
          "bounds": {
            "type": "object",
            "nullable": true,
            "properties": {
              "south": {
                "type": "number",
                "nullable": true
              },
              "west": {
                "type": "number",
                "nullable": true
              },
              "north": {
                "type": "number",
                "nullable": true
              },
              "east": {
                "type": "number",
                "nullable": true
              }
            }
          }
        }
      },
      "SatelliteMission": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "Stable identifier; use as the `mission` filter on satellite_passes.",
            "example": "cfosat"
          },
          "name": {
            "type": "string",
            "example": "CFOSAT"
          },
          "agency": {
            "type": "string",
            "nullable": true,
            "example": "CNES / CNSA"
          },
          "instrument": {
            "type": "string",
            "nullable": true,
            "example": "SWIM"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "Localized (en/fr) via the `locale` query param / `Accept-Language`."
          },
          "image_url": {
            "type": "string",
            "nullable": true
          },
          "pass_count": {
            "type": "integer",
            "description": "Number of recorded passes for this mission."
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "description": "Standardized error response format for V2 API",
        "properties": {
          "error": {
            "type": "string",
            "description": "Error code (e.g., \"unauthorized\", \"bad_request\", \"resource_not_found\")",
            "example": "unauthorized"
          },
          "message": {
            "type": "string",
            "description": "Human-readable error message",
            "example": "Invalid API key"
          },
          "details": {
            "type": "object",
            "description": "Additional error details (optional, varies by error type)"
          }
        }
      },
      "BuoySeasonalSeriesPoint": {
        "type": "object",
        "properties": {
          "bucket_index": {
            "type": "integer",
            "description": "Season-relative bucket index"
          },
          "label": {
            "type": "string",
            "description": "Display label for the bucket"
          },
          "bucket_start": {
            "type": "string",
            "format": "date-time"
          },
          "bucket_end": {
            "type": "string",
            "format": "date-time"
          },
          "value": {
            "type": "number",
            "format": "float",
            "nullable": true
          },
          "sample_count": {
            "type": "integer",
            "description": "Number of raw readings contributing to the bucket"
          }
        }
      },
      "SatelliteMissionRef": {
        "type": "object",
        "description": "Compact reference to a satellite mission.",
        "properties": {
          "slug": {
            "type": "string",
            "example": "cfosat"
          },
          "name": {
            "type": "string",
            "example": "CFOSAT"
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Bad request (400 Bad Request)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "bad_request",
              "message": "Search term must be at least 2 characters",
              "timestamp": "2025-12-27T14:30:00Z",
              "request_id": "550e8400-e29b-41d4-a716-446655440000"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Invalid or missing API key (401 Unauthorized)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "unauthorized",
              "message": "Invalid API key",
              "timestamp": "2025-12-27T14:30:00Z",
              "request_id": "550e8400-e29b-41d4-a716-446655440000"
            }
          }
        }
      },
      "RateLimitExceeded": {
        "description": "Rate limit exceeded (429 Too Many Requests)",
        "headers": {
          "X-RateLimit-Limit": {
            "schema": {
              "type": "integer",
              "description": "Maximum requests allowed per hour",
              "example": 1000
            }
          },
          "X-RateLimit-Remaining": {
            "schema": {
              "type": "integer",
              "description": "Requests remaining in current hour",
              "example": 0
            }
          },
          "X-RateLimit-Reset": {
            "schema": {
              "type": "integer",
              "description": "Unix timestamp when rate limit resets (top of next hour)",
              "example": 1735326000
            }
          },
          "Retry-After": {
            "schema": {
              "type": "integer",
              "description": "Seconds to wait before retrying",
              "example": 3600
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "rate_limit_exceeded",
              "message": "You have exceeded 1000 requests per hour",
              "timestamp": "2025-12-27T14:30:00Z",
              "request_id": "550e8400-e29b-41d4-a716-446655440000"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found (404 Not Found)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "resource_not_found",
              "message": "Couldn't find Spot with 'id'=999",
              "timestamp": "2025-12-27T14:30:00Z",
              "request_id": "550e8400-e29b-41d4-a716-446655440000"
            }
          }
        }
      }
    },
    "headers": {
      "XRateLimitLimit": {
        "schema": {
          "type": "integer",
          "description": "Maximum requests allowed per hour",
          "example": 1000
        },
        "description": "Maximum requests allowed per hour"
      },
      "XRateLimitRemaining": {
        "schema": {
          "type": "integer",
          "description": "Requests remaining in current hour",
          "example": 987
        },
        "description": "Requests remaining in current hour"
      },
      "XRateLimitReset": {
        "schema": {
          "type": "integer",
          "description": "Unix timestamp when rate limit resets",
          "example": 1735326000
        },
        "description": "Unix timestamp when rate limit resets"
      }
    },
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "API Key",
        "description": "API key authentication. Pass your API key as a Bearer token in the Authorization header.\n\nFormat: `Authorization: Bearer YOUR_API_KEY`\n\nAlternative: Pass as query parameter `?api_key=YOUR_API_KEY`\n\n**Security:** API keys are stored as BCrypt hashes (never plain text). Only the hash is stored in the database.\n"
      }
    }
  }
}