{
  "openapi": "3.1.0",
  "info": {
    "title": "Quinta Picouto de Cima Agent API",
    "version": "1.2.0",
    "description": "Live catalogue, party-aware indicative availability and double-opt-in booking requests. Use availability-summary when the visitor has not chosen an exact offering; use the offering-specific availability route after they choose. /api/availability is legacy compatibility only. A booking request is not a confirmed booking."
  },
  "servers": [
    { "url": "https://quintapicoutocima.pt" }
  ],
  "paths": {
    "/api/offerings/{type}": {
      "get": {
        "operationId": "listOfferings",
        "summary": "List the live offerings for one type",
        "parameters": [
          { "$ref": "#/components/parameters/Type" }
        ],
        "responses": {
          "200": {
            "description": "Live offerings",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OfferingsResponse" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/api/v2/availability/{type}/{offering_id}/{adults}/next": {
      "get": {
        "operationId": "checkNextAvailability",
        "summary": "Find the next available dates without supplying a date range",
        "description": "Searches from today in Europe/Lisbon through 30 days later. Indicative only; nothing is held or booked.",
        "parameters": [
          { "$ref": "#/components/parameters/Type" },
          { "$ref": "#/components/parameters/OfferingId" },
          { "$ref": "#/components/parameters/Adults" }
        ],
        "responses": {
          "200": {
            "description": "Next indicative availability for this offering and party",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PartyAvailability" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        },
        "x-html-mirror": "/availability/{type}/{offering_id}/{adults}/next"
      }
    },
    "/api/v2/availability-summary/{type}/{adults}/next": {
      "get": {
        "operationId": "listNextAvailabilityByParty",
        "summary": "Find the next availability for every online-bookable offering of one type",
        "description": "Use this when the visitor knows the type and number of adults but has not chosen an exact offering. It avoids a separate catalogue lookup and returns only offerings enabled for online booking.",
        "parameters": [
          { "$ref": "#/components/parameters/Type" },
          { "$ref": "#/components/parameters/Adults" }
        ],
        "responses": {
          "200": {
            "description": "Party-aware indicative availability grouped by offering",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AvailabilitySummary" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        },
        "x-html-page": "/disponibilidade-provas"
      }
    },
    "/api/v2/availability/{type}/{offering_id}/{adults}/{from}/{to}": {
      "get": {
        "operationId": "checkAvailabilityForParty",
        "summary": "Check availability for an exact offering and adult count",
        "description": "Indicative only. The adult count is significant because some offerings allow otherwise closed weekdays for groups of 4 or more adults. Nothing is held or booked.",
        "parameters": [
          { "$ref": "#/components/parameters/Type" },
          { "$ref": "#/components/parameters/OfferingId" },
          { "$ref": "#/components/parameters/Adults" },
          { "$ref": "#/components/parameters/From" },
          { "$ref": "#/components/parameters/To" }
        ],
        "responses": {
          "200": {
            "description": "Indicative availability for this offering and party",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PartyAvailability" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/api/availability": {
      "get": {
        "operationId": "checkAvailabilityLegacy",
        "deprecated": true,
        "summary": "Type-level compatibility availability endpoint",
        "description": "Legacy compatibility only. New agents must use checkAvailabilityForParty or checkNextAvailability because this type-level endpoint cannot apply the exact offering's adult-dependent weekday rules.",
        "parameters": [
          { "name": "type", "in": "query", "schema": { "$ref": "#/components/schemas/OfferingType" } },
          { "name": "from", "in": "query", "schema": { "$ref": "#/components/schemas/Date" } },
          { "name": "to", "in": "query", "schema": { "$ref": "#/components/schemas/Date" } },
          { "name": "unit", "in": "query", "schema": { "type": "string" }, "description": "Accommodation unit, for stay only." }
        ],
        "responses": {
          "200": {
            "description": "Type-level indicative availability",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LegacyAvailability" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/api/booking-request": {
      "post": {
        "operationId": "requestBooking",
        "summary": "Submit a double-opt-in booking request",
        "description": "Sends a verification email to the visitor. It does not confirm a booking and does not hold availability.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BookingRequest" } } }
        },
        "responses": {
          "202": {
            "description": "Request accepted; visitor confirmation is pending",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BookingRequestAccepted" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "429": {
            "description": "Rate limited",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "Type": {
        "name": "type",
        "in": "path",
        "required": true,
        "schema": { "$ref": "#/components/schemas/OfferingType" }
      },
      "OfferingId": {
        "name": "offering_id",
        "in": "path",
        "required": true,
        "schema": { "type": "string", "pattern": "^[A-Za-z0-9_-]{1,64}$" }
      },
      "Adults": {
        "name": "adults",
        "in": "path",
        "required": true,
        "description": "Number of adults, not total people. Four or more may activate a group weekday override.",
        "schema": { "type": "integer", "minimum": 1, "maximum": 30 }
      },
      "From": {
        "name": "from",
        "in": "path",
        "required": true,
        "schema": { "$ref": "#/components/schemas/Date" }
      },
      "To": {
        "name": "to",
        "in": "path",
        "required": true,
        "description": "Inclusive end date; the window is capped at 180 days.",
        "schema": { "$ref": "#/components/schemas/Date" }
      }
    },
    "schemas": {
      "OfferingType": { "type": "string", "enum": ["tasting", "stay", "experience"] },
      "Date": { "type": "string", "format": "date", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" },
      "Offering": {
        "type": "object",
        "required": ["offering_id", "type", "name"],
        "properties": {
          "offering_id": { "type": "string" },
          "type": { "$ref": "#/components/schemas/OfferingType" },
          "name": { "type": "string", "description": "Canonical Portuguese display name." },
          "description": { "type": "string" },
          "price": { "type": "string" },
          "capacity": { "type": "integer" },
          "beds": { "type": "string" },
          "includes": { "type": "array", "items": { "type": "string" } },
          "available_weekdays": { "type": "array", "items": { "type": "string", "enum": ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] } },
          "group_override": { "type": "string", "description": "Present when 4+ adults may ignore the weekday restriction." },
          "bookable_online": { "type": "boolean", "description": "False means the offering is informational only and cannot be requested online." },
          "unavailable": { "type": "boolean" }
        }
      },
      "OfferingsResponse": {
        "type": "object",
        "required": ["type", "offerings"],
        "properties": {
          "type": { "$ref": "#/components/schemas/OfferingType" },
          "offerings": { "type": "array", "items": { "$ref": "#/components/schemas/Offering" } }
        }
      },
      "AvailabilityDay": {
        "type": "object",
        "required": ["date", "times"],
        "properties": {
          "date": { "$ref": "#/components/schemas/Date" },
          "times": { "type": "array", "items": { "type": "string", "pattern": "^\\d{2}h\\d{2}$" } }
        }
      },
      "PartyAvailability": {
        "type": "object",
        "required": ["type", "offering_id", "adults", "from", "to", "status", "confirmationRequired", "generatedAt"],
        "properties": {
          "type": { "$ref": "#/components/schemas/OfferingType" },
          "offering_id": { "type": "string" },
          "adults": { "type": "integer", "minimum": 1 },
          "from": { "$ref": "#/components/schemas/Date" },
          "to": { "$ref": "#/components/schemas/Date" },
          "status": { "const": "indicative" },
          "confirmationRequired": { "const": true },
          "unit": { "type": ["string", "null"] },
          "days": { "type": "array", "items": { "$ref": "#/components/schemas/AvailabilityDay" } },
          "nights": { "type": "object", "additionalProperties": { "type": "string", "enum": ["free", "taken"] } },
          "generatedAt": { "type": "string", "format": "date-time" }
        },
        "oneOf": [
          { "required": ["days"] },
          { "required": ["nights", "unit"] }
        ]
      },
      "AvailabilitySummaryOffering": {
        "type": "object",
        "required": ["offering_id", "name"],
        "properties": {
          "offering_id": { "type": "string" },
          "name": { "type": "string" },
          "description": { "type": "string" },
          "price": { "type": "string" },
          "includes": { "type": "array", "items": { "type": "string" } },
          "available_weekdays": { "type": "array", "items": { "type": "string", "enum": ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] }, "description": "The weekday rule of the offering. Present only when it is restricted; omitted means every day. The days list already applies it for the requested number of adults." },
          "group_override": { "type": "string", "description": "Present when 4+ adults may ignore the weekday restriction." },
          "days": { "type": "array", "items": { "$ref": "#/components/schemas/AvailabilityDay" } },
          "nights": { "type": "object", "additionalProperties": { "type": "string", "enum": ["free", "taken"] } }
        }
      },
      "AvailabilitySummary": {
        "type": "object",
        "required": ["type", "adults", "from", "to", "status", "confirmationRequired", "offerings", "generatedAt"],
        "properties": {
          "type": { "$ref": "#/components/schemas/OfferingType" },
          "adults": { "type": "integer", "minimum": 1, "maximum": 30 },
          "from": { "$ref": "#/components/schemas/Date" },
          "to": { "$ref": "#/components/schemas/Date" },
          "status": { "const": "indicative" },
          "confirmationRequired": { "const": true },
          "offerings": { "type": "array", "items": { "$ref": "#/components/schemas/AvailabilitySummaryOffering" } },
          "generatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "LegacyAvailability": {
        "type": "object",
        "required": ["type", "from", "to", "generatedAt"],
        "properties": {
          "type": { "$ref": "#/components/schemas/OfferingType" },
          "from": { "$ref": "#/components/schemas/Date" },
          "to": { "$ref": "#/components/schemas/Date" },
          "unit": { "type": ["string", "null"] },
          "days": { "type": "array", "items": { "$ref": "#/components/schemas/AvailabilityDay" } },
          "nights": { "type": "object", "additionalProperties": { "type": "string", "enum": ["free", "taken"] } },
          "generatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "BookingRequest": {
        "type": "object",
        "required": ["type", "numPeople", "name", "email", "phone"],
        "properties": {
          "type": { "$ref": "#/components/schemas/OfferingType" },
          "offering_id": { "type": "string" },
          "item": { "type": "string", "description": "Exact Portuguese name; alternative to offering_id." },
          "date": { "$ref": "#/components/schemas/Date" },
          "time": { "type": "string", "pattern": "^\\d{2}h\\d{2}$" },
          "checkin": { "$ref": "#/components/schemas/Date" },
          "checkout": { "$ref": "#/components/schemas/Date" },
          "numPeople": { "type": "integer", "minimum": 1, "maximum": 30 },
          "numAdults": { "type": "integer", "minimum": 1, "maximum": 30 },
          "numChildren": { "type": "integer", "minimum": 0, "maximum": 29 },
          "childrenAges": { "type": "array", "maxItems": 10, "items": { "type": "integer", "minimum": 0, "maximum": 17 } },
          "name": { "type": "string", "minLength": 2, "maxLength": 120 },
          "email": { "type": "string", "format": "email", "maxLength": 200 },
          "phone": { "type": "string" },
          "lang": { "type": "string", "enum": ["pt", "en", "es", "fr", "de"], "default": "en" },
          "source": { "type": "string", "maxLength": 80 },
          "message": { "type": "string", "maxLength": 2000 }
        },
        "allOf": [
          { "anyOf": [ { "required": ["offering_id"] }, { "required": ["item"] } ] },
          {
            "if": { "properties": { "type": { "enum": ["tasting", "experience"] } } },
            "then": { "required": ["date", "time"] }
          },
          {
            "if": { "properties": { "type": { "const": "stay" } } },
            "then": { "required": ["checkin", "checkout"] }
          }
        ]
      },
      "BookingRequestAccepted": {
        "type": "object",
        "required": ["ok", "status"],
        "properties": {
          "ok": { "const": true },
          "status": { "const": "awaiting_customer_confirmation" },
          "expires_in_hours": { "type": "integer" },
          "duplicate": { "type": "boolean" },
          "message": { "type": "string" },
          "composition_stored": { "type": "boolean" }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string" },
          "message": { "type": "string" },
          "hint": { "type": "string" },
          "allowed": { "type": "array", "items": { "type": "string" } }
        },
        "additionalProperties": true
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid input",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Conflict": {
        "description": "Offering or requested availability is unavailable",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Unavailable": {
        "description": "Availability could not be determined or agent booking is disabled",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    }
  },
  "x-availability-refresh-seconds": 120,
  "x-timezone": "Europe/Lisbon"
}
