{
  "openapi": "3.1.0",
  "info": {
    "title": "HotelMonitor agent API",
    "version": "1.2.0",
    "description": "Read-only answers about hotel prices, for AI assistants. Prices are tax-inclusive nightly figures from HotelMonitor's own price scans (the Agoda share is an estimate of tax). They are past observations, not live quotes. Results include page_url, HotelMonitor's price-history page, and where available book_url, a booking-site link with the dates filled in, so the person can confirm the current price. Booking links are affiliate links; HotelMonitor may earn a commission at no extra cost to the person. Rate limits per client (IP address, IPv6 by /64, plus user agent), shared with the MCP server: 60 requests a minute and 2,000 a day; over either you get 429 with Retry-After. X-RateLimit-* headers describe the per-minute window and RateLimit-Policy states both. The same tools are available over MCP at https://hotelmonitor.co.uk/mcp.",
    "contact": { "name": "HotelMonitor", "url": "https://hotelmonitor.co.uk/developers#mcp" }
  },
  "servers": [{ "url": "https://hotelmonitor.co.uk/api/agent/v1" }],
  "components": {
    "parameters": {
      "via": {
        "name": "via", "in": "query", "required": false,
        "schema": { "type": "string", "maxLength": 32 },
        "description": "Optional short name of your app or assistant (lower-case letters, digits, - or _), used in booking links so we can see where visits come from. Anything else is ignored and agent_api is used."
      }
    },
    "schemas": {
      "UnknownDestination": {
        "type": "object",
        "properties": {
          "error": { "type": "string", "const": "unknown_destination" },
          "requested": { "type": "string" },
          "suggestions": { "type": "array", "items": { "type": "string" }, "description": "Up to 5 valid slugs close to the one asked for." },
          "valid_destinations": { "type": "array", "items": { "type": "string" } },
          "hint": { "type": "string" }
        },
        "required": ["error", "suggestions", "valid_destinations", "hint"]
      },
      "Error": {
        "type": "object",
        "properties": { "error": { "type": "string" }, "hint": { "type": "string" } },
        "required": ["error"]
      }
    }
  },
  "paths": {
    "/price-check": {
      "get": {
        "operationId": "checkHotelPrice",
        "summary": "Is this price high, low or usual for this hotel in that month?",
        "description": "Compares a price with the hotel's own usual price for the check-in month. Returns verdict null (reason hotel_not_tracked or not_enough_data) when there are not enough scans of that hotel, with the destination's month figure as labelled context. book_url is included when checkout is given.",
        "parameters": [
          { "name": "hotel", "in": "query", "required": true, "schema": { "type": "string", "maxLength": 120 } },
          { "name": "destination", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Destination slug, e.g. tenerife, new-york. \"New York\" and new_york are accepted too." },
          { "name": "checkin", "in": "query", "required": true, "schema": { "type": "string", "format": "date" } },
          { "name": "checkout", "in": "query", "required": false, "schema": { "type": "string", "format": "date" }, "description": "Required when per=stay. At most 30 nights." },
          { "name": "price", "in": "query", "required": true, "schema": { "type": "number", "exclusiveMinimum": 0 }, "description": "In GBP." },
          { "name": "per", "in": "query", "required": true, "schema": { "type": "string", "enum": ["night", "stay"] }, "description": "Whether price is per night or for the whole stay." },
          { "name": "adults", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 8, "default": 2 } },
          { "$ref": "#/components/parameters/via" }
        ],
        "responses": {
          "200": { "description": "verdict below_usual, about_usual, above_usual, or null with reason. Includes page_url, and book_url when checkout is given." },
          "400": { "description": "Invalid input. An unknown destination returns the UnknownDestination shape; other errors the Error shape.", "content": { "application/json": { "schema": { "oneOf": [ { "$ref": "#/components/schemas/UnknownDestination" }, { "$ref": "#/components/schemas/Error" } ] } } } },
          "429": { "description": "Rate limited. See Retry-After." }
        }
      }
    },
    "/destination/{slug}": {
      "get": {
        "operationId": "getDestinationPrices",
        "summary": "Typical nightly price by month for a destination, and its cheapest months.",
        "description": "Months with too few scans are left out. The figures are the same as the destination month pages. Includes page_url; each month has checkin, checkout and book_url (a booking-site search for the destination, two adults, 7 nights from the 15th, or from today once the 15th has passed; omitted when no date is left in the month), and the top-level book_url is the cheapest month's.",
        "parameters": [
          { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/via" }
        ],
        "responses": {
          "200": { "description": "Months ahead with typical and lowest-seen nightly prices and dated book_url links.", "content": { "application/json": { "schema": { "type": "object", "properties": { "destination": { "type": "string" }, "slug": { "type": "string" }, "months": { "type": "array", "items": { "type": "object", "properties": { "month": { "type": "string" }, "label": { "type": "string" }, "typical_nightly_gbp": { "type": "number" }, "lowest_seen_nightly_gbp": { "type": ["number", "null"] }, "scans": { "type": "integer" }, "checkin": { "type": "string", "format": "date" }, "checkout": { "type": "string", "format": "date" }, "book_url": { "type": "string" } }, "required": ["month", "label", "typical_nightly_gbp", "lowest_seen_nightly_gbp", "scans"] } }, "cheapest_months": { "type": "array" }, "page_url": { "type": "string" }, "book_url": { "type": "string" }, "book_url_month": { "type": "string" }, "watch_url": { "type": "string" }, "notes": { "type": "array", "items": { "type": "string" } }, "data_as_of": { "type": "string" } }, "required": ["destination", "slug", "months", "cheapest_months", "page_url", "watch_url", "notes", "data_as_of"] } } } },
          "404": { "description": "Unknown destination.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UnknownDestination" } } } },
          "429": { "description": "Rate limited." }
        }
      }
    },
    "/deals": {
      "get": {
        "operationId": "findHotelDeals",
        "summary": "Current hotel deals from the latest scans (at most 20).",
        "description": "Hotels priced below their usual level. Each deal has page_url and a dated book_url. The response also has page_url: the destination page when a destination was given, otherwise the deals page, so there is somewhere to browse when nothing matches.",
        "parameters": [
          { "name": "destination", "in": "query", "schema": { "type": "string" } },
          { "name": "month", "in": "query", "schema": { "type": "string", "pattern": "^\\d{4}-(0[1-9]|1[0-2])$" } },
          { "name": "max_price", "in": "query", "schema": { "type": "number" }, "description": "Maximum price per night in GBP." },
          { "$ref": "#/components/parameters/via" }
        ],
        "responses": {
          "200": { "description": "Deals, each with page_url and a dated book_url." },
          "400": { "description": "Invalid input.", "content": { "application/json": { "schema": { "oneOf": [ { "$ref": "#/components/schemas/UnknownDestination" }, { "$ref": "#/components/schemas/Error" } ] } } } },
          "429": { "description": "Rate limited." }
        }
      }
    }
  }
}
